Redis分布式锁解决方案:抢票、秒杀并发问题优化

silverwq
2022-10-16 / 0 评论 / 329 阅读 / 正在检测是否收录...

概述

锁是保护一些共享的资源,这个资源通常会发生竞争。

场景

以下代码,如果5秒内有多个请求,就会超卖

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$num = $redis->get('num');
if ($num < 1) {
    // 两个客户端并发执行的时候,因为前面的库存还没执行到增加num
    sleep(5);// 模拟购买逻辑时间
    $store = $redis->incr('num');
    var_dump($store);
} else {
    echo '已经卖完';
}

单机锁

上述的问题,我们可以用文件锁的方法,不过这文件通常只能放在一台机器中,如果有多台机器的话,就会有问题

if (flock($fp,LOCK_EX)) {// 加悲观排他锁阻塞等待
    fwrite($fp,"lock success\n");
    sleep(5);
    flock($fp,LOCK_UN);// 解锁
} else {
    echo "文件被其它进程占用";
}
fclose($fp);

我们还可以通过数据库的行锁进行锁定

select * from erp_storage where id = 1 for update;

分布式锁

分布式锁的基本条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。例如下例中,请求a的业务逻辑时间太长,导致锁缓存过期失效,请求b就获取到了锁,然后这个时候请求a业务逻辑执行完成了,要释放锁了,就把请求b的锁给释放了
    l9a01hn3.png

以下代码符合上述条件

<?php

class Lock {
    private $redis;
    private $clientUniqueId;

    public function __construct()
    {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
    }

    public function lock($scene = '小米11库存', $expire = 5, $retry = 5, $sleep = 1)
    {

        $res = false;
        while ($retry > 0) { // 获取锁,要多次尝试,不能一次返回
            $value = session_create_id(); // 获取唯一字符
            $this->clientUniqueId = $value;
            $res = $redis->set($scene, $value, [
                'NX', // 不存在则设置,也就是没有锁就加锁
                'EX' => $expire, // 防止死锁,万一加锁后没有解锁,会自动释放锁
            ]);
            if ($res) { // 加锁成功后返回
                break;
            }

            echo "尝试获取锁";
            sleep($sleep);
            $retry--;
        }
    }

    public function unlock($scene)
    {
        // $value = $this->redis->get($scene);
        // if ($value == $this->clientUniqueId) {// 不能删除掉别人的锁
        //     正好锁过期了,这个时候,客户端b能获取到锁,这里就会把客户端b的锁给删除
        //     sleep(5);//极端情况下,可能这里会有io阻塞,导致把别人的锁给删除了
        //     $this->redis->del($scene);
        // }

        // KEYS类似全局变量
        $script = <<<LUA
        local key=KEYS[1]
        local value=ARGV[1]
        if(redis.call('get','key') == value)
        then
            return redis.call('del',key)
        end
LUA;
        // 为了保证原子性,get和del一起执行,不能分开执行
        $this->redis->eval($script,[$scene,$this->clientUniqueId]);
    }

}

0

评论 (0)

取消