关于redis:Redis分布式锁实现方式

3次阅读

共计 2220 个字符,预计需要花费 6 分钟才能阅读完成。

为了避免分布式系统中的多个过程之间互相烦扰,须要一种分布式协调技术来对这些过程进行调度。而这个分布式协调技术的外围就是来实现这个分布式锁。

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 的个数,须要留神的是,LUA 的 key 下标从 1 开始,如果没有 key 参数,则写 0;
[key1 key2 key3…], 参数字段名列表(cmd 模式以空格宰割),总数量等于下面的 key-num 个数;
[value1 value2 value3 …], 参数字段对应的值(cmd 模式以空格宰割),总数量等于下面的 key-num 个数;

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);
    }

正文完
 0