美文网首页
秒杀随笔

秒杀随笔

作者: _不想翻身的咸鱼 | 来源:发表于2021-03-10 15:33 被阅读0次

    方法:

    1. mysql悲观锁
    2. mysql乐观锁
    3. PHP+redis分布式锁
    4. PHP+redis乐观锁(redis watch)
    mysql悲观锁

    悲观锁,正如其名,它指的是对数据被外界(包括当前系统的其他事务。以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库提供的锁机制才能真正保持数据的排它性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据。)

    准备数据

    DROP TABLE IF EXISTS goods;
    CREAT TABLE IF NOT EXISTS goods(
    id INTEGET NOT NULL,
    money INTEGET,
    version INTEGET,
    primary key (id)
    )ENGINE = INNODB;
    
    insert into goods value(1,0,1)
    
    select * from goods;
    set autocommit=0;
    
    #开两个客户端:
    session1:
    select * from goods where id =1 for update;
    
    之后在session2:
    select * from goods where id =1 for update;
    
    这种情况,session2会进入等待
    
    
    mysql乐观锁

    乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时,才会对数据的冲突与否进行检测。如果没有冲突那就ok;如果出现冲突了,则返回错误信息并让用户决定如何去做。

    乐观锁在数据库上的实现完全是逻辑的,数据库本身不提供支持而是需要开发者自己来实现

    -- goods_number表示库存
    update items set goods_number=goods_number-1,version=version+1 where id = 100 and version=#{version};
    
    <?php
    $version = select version from goods;    
        #省略业务逻辑
    
    update goods set money = 1, version=version+1 where version={$version};
    #上面这个只是用来表达意思的代码,并不是实际代码
    #成功的那个,mysql会返回更新成功,php就返回前端,恭喜你秒杀成功
    #失败的那个,mysql会返回更新失败,php就返回前端,不好意思,秒杀失败
    

    总结:

    乐观锁不锁数据,而是通过版本号控制,会有不同结果返回给php,把决策权交给后端.

    对比:乐观锁不需要锁数据,性能高于悲观锁

    PHP+redis分布式锁
    1. 分布式锁本质是占一个坑,当别的进程也要来占坑时发现已经被占,就会放弃或者稍后重试
    2. 占坑一般使用 setnx(set if not exists)指令,只允许一个客户端占坑
    3. 先来先占,用完了在调用del指令释放坑
    4. 但是这样有一个问题,如果逻辑执行到中间出现异常,可能导致del指令没有被调用,这样就会陷入死锁,锁永远无法释放
    5. 为了解决死锁问题,我们拿到锁时可以加上一个expire过期时间,这样即使出现异常,当到达过期时间也会自动释放锁
    6. 这样又有一个问题,setnx和expire是两条指令而不是原子指令,如果两条指令之间进程挂掉依然会出现死锁
    7. 为了治理上面乱象,在redis 2.8中加入了set指令的扩展参数,使setnx和expire指令可以一起执行

    相当于是php线程锁,100000个抢购请求并发过来,有100000个线程,但同一时刻只会有一个线程在执行业务代码,其他线程都在死循环当中等待。

    redis 分布式锁与原理

    EXISTS job      #job 不存在
    
    SETNX job "programmer"   #job设置成功
    
    SETNX job "code-farmer"   #尝试覆盖job失效
    
    get job           #查出programmer,没有被覆盖
    
    

    可见,SETNX和SET是有区别的,SETNX只能用1次,set是可以无数次的。redis分布式锁就是利用了这个机制。

    分布式锁实例代码:

    $expire = 10;  //有效期10秒
    $key = 'lock';
    $value = time() + $expire; //锁的值 = Unix时间戳 +锁的有效期
    $status = true;
    
    while($status){
        $lock = $redis->setnx($key, $value);
        if(empty($lock)){
            $value = $redis->get($key);
            #如果当前时间大于设置的有效期,意味着过期,所以删除key
            if( time() > $value){
                $redis->del($key);
            }
        }else{
            $status = false;
            //下面是具体的业务流程....
        }       
    }
    

    也可以封装成方法:

    class RedisMutexLock
    {
        /**
         * 缓存 Redis 连接。
         *
         * @return void
         */
        public static function getRedis()
        {
            // 这行代码请根据自己项目替换为自己的获取 Redis 连接。
            return YCache::getRedisClient();
        }
    
        /**
         * 获得锁,如果锁被占用,阻塞,直到获得锁或者超时。
         * -- 1、如果 $timeout 参数为 0,则立即返回锁。
         * -- 2、建议 timeout 设置为 0,避免 redis 因为阻塞导致性能下降。请根据实际需求进行设置。
         *
         * @param  string  $key         缓存KEY。
         * @param  int     $timeout     取锁超时时间。单位(秒)。等于0,如果当前锁被占用,则立即返回失败。如果大于0,则反复尝试获取锁直到达到该超时时间。
         * @param  int     $lockSecond  锁定时间。单位(秒)。
         * @param  int     $sleep       取锁间隔时间。单位(微秒)。当锁为占用状态时。每隔多久尝试去取锁。默认 0.1 秒一次取锁。
         * @return bool 成功:true、失败:false
         */
        public static function lock($key, $timeout = 0, $lockSecond = 20, $sleep = 100000)
        {
            if (strlen($key) === 0) {
                // 请更换为自己项目抛异常的方法。
                YCore::exception(500, '缓存KEY没有设置');
            }
            if (!is_int($timeout) || $timeout < 0) {
                YCore::exception(500, "timeout 参数设置有误");
            }
            $start = self::getMicroTime();
            $redis = self::getRedis();
            do {
                // [1] 锁的 KEY 不存在时设置其值并把过期时间设置为指定的时间。锁的值并不重要。重要的是利用 Redis 的特性。
                $acquired = $redis->set("Lock:{$key}", 1, ['NX', 'EX' => $lockSecond]);
                if ($acquired) {
                    break;
                }
                if ($timeout === 0) {
                    break;
                }
                usleep($sleep);
            } while ((self::getMicroTime()) < ($start + ($timeout * 1000000)));
            return $acquired ? true : false;
        }
    
        /**
         * 释放锁
         *
         * @param  mixed  $key  被加锁的KEY。
         * @return void
         */
        public static function release($key)
        {
            if (strlen($key) === 0) {
                // 请更换为自己项目抛异常的方法。
                YCore::exception(500, '缓存KEY没有设置');
            }
            $redis = self::getRedis();
            $redis->del("Lock:{$key}");
        }
    
        /**
         * 获取当前微秒。
         *
         * @return bigint
         */
        protected static function getMicroTime()
        {
            return bcmul(microtime(true), 1000000);
        }
    }
    
    PHP+redis乐观锁

    原理

    1. 当用户购买时,通过 WATCH 监听用户库存,如果库存在watch监听后发生改变,就会捕获异常而放弃对库存减一操作

    2. 如果库存没有监听到变化并且数量大于1,则库存数量减一,并执行任务

    **弊端 **

    1. Redis 在尝试完成一个事务的时候,可能会因为事务的失败而重复尝试重新执行

    2. 保证商品的库存量正确是一件很重要的事情,但是单纯的使用 WATCH 这样的机制对服务器压力过大

    <?php
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6397);
        //sales商品的销量(也就是卖了多少件)
        $redis->watch('sales')
        $reids->get('sales');
        //秒杀的库存
        $number = 100;
    
        if($sales >= $number){
            exit('秒杀结束');
        }
    
        //开启事务
        $redis->multi();
        $redis->incr('sales'); //将key中存储的数字值增1,如果key不存在,那么key的值会先被初始化为0,然后再执行incr操作.
    
        $res = $redis->exec();//成功1,失败0
        if($res){
            //秒杀成功
            $sql = "update goods set store=store-1 where id =1";
            
            if($sql){
                echo '秒杀成功';
            }
        }else{
            exit('抢购失败')
        }
    
    
    

    相关文章

      网友评论

          本文标题:秒杀随笔

          本文链接:https://www.haomeiwen.com/subject/ypmwqltx.html