美文网首页
Redis分布式锁在高并发场景的实现方式

Redis分布式锁在高并发场景的实现方式

作者: 后厂村村长 | 来源:发表于2021-09-13 20:06 被阅读0次

    为了防止分布式系统中的多个进程之间相互干扰,需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。

    Redis加锁

    原理很简单,set 一个 锁-key,如果成功则说明加锁成功,反之则失败。
    为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下几个条件:

    互斥性。在任意时刻,只有一个客户端能持有锁。
    不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
    解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

    基于以上条件,采用set扩展参数,保证原子性操作:SET lock-key "lock-client" EX 10086 NX
    lock-key "lock-client" 指定 加锁client,解锁时用于判断。
    EX 10010 指定过期时间
    NX 只在键不存在时,才对键进行设置操作。效果等同于SETNX 命令。

    只不过早期版本redis不支持set的扩展参数,这就需要用到 lua 脚本了
    加锁可以在高版本借助set命令实现原子操作,但解锁就不可以了,依然得用到lua脚本。

    Redis+Lua

    Redis在2.6版本推出了 lua 脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:

    1. 减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延。
    2. 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。
    3. 复用:客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。

    Redis 解锁

    需要在获得 lock-key 后判断加锁对象是否为当前client,是,则解锁。Lua 脚本如下:
    if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
    执行方式:eval;
    eval 参数列表:eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....],参数解析:

    eval 代表执行Lua语言的命令;
    lua-script 代表Lua语言脚本;
    key-num 表示参数中有多少个key,需要注意的是Redis中key是从1开始的,如果没有key的参数,那么写0;
    [key1 key2 key3…] 是key作为参数传递给Lua语言,也可以不填,但是需要和key-num的个数对应起来;
    [value1 value2 value3 …] 这些参数传递给Lua语言,他们是可填可不填的。

    eval执行示例:eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-val

    完整解锁执行脚本:
    eval "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock-key client-val

    为什么不优先考虑使用 Redis 事务

    简单提两句这个事情,redis 本身有提供事务功能,即保证一系列复合操作是原子性执行。不过事务有两个问题:
    1、Redis事务不支持Rollback(重点)
    2、基于上面1点,对于事务中已成功执行的操作,无法回滚。

    其实解锁操作,用事务倒是无所谓,因为是先get到key值,比较后再删除,即便第二步操作失败,第一步的get也没有实际影响;
    但如果加锁时,使用set、expire可能会有问题,比如set后未设置过期时间前进程异常挂掉,导致锁没有过期时间产生死锁。所以加锁尽量使用高版本(redis2.6及以上版本)的set附加expire参数执行吧。

    参考样例-PHP版

        // 加锁操作
        function lock($timeout = 3) {
            // 加锁的key
            $mtkey = 'lock:your_lock_key';
            // 随机生成id用于解锁操作,也可用自己业务中其它具有唯一性标识的数值
            $mtid = uniqid(mt_rand(1000, 9999));
            // 获取锁的超时时间
            $end = time() + $timeout;
            while (time() <= $end) {
                // NX: 不存在时设置;PX:过期时间(毫秒);
                if ($redis->set($mtkey, $mtid, array('NX', 'PX' => 1000))) {
                    return $mtid;
                }
                usleep(1000);
            }
            return '';
        }
    
        // 解操操作(将自己设置的锁删除)
        function unLock($mtkey = 'lock:your_lock_key', $mtid) {
            $script = <<<LUA
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        end
    LUA;
            /**
             * eval 第一个参数是要执行的LUA脚本内容
             * 第二个参数是传递的参数
             * 第三个参数是指传递的参数中前X个是放到LUA中的 KEYS 表,剩余的则放到LUA中的 ARGV 表
             * LUA中的“表”类似数组,索引以1开始。
             */
            $redis->eval($script, array($mtkey, $mtid), 1);
        }
    

    ----------End----------

    相关文章

      网友评论

          本文标题:Redis分布式锁在高并发场景的实现方式

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