大家春节在家抢红包玩的不亦乐乎,抢红包服务看起来非常简单,实际上要做好这个服务,特别是money相关服务是不允许出错的,想想看每个红包的数字都是真金白银,要求服务的鲁棒性非常高,背后包含着很多后台服务技术细节。
今天就来说说高并发服务编程中的redis分布式锁。
这里罗列出3种redis实现的分布式锁,并分别对比说明各自特点。
setnx用法参考redis官方文档
SETNX key value
将key
设置值为value
,如果key
不存在,这种情况下等同SET命令。 当key
存在时,什么也不做。SETNX
是”SET if Not eXists”的简写。
返回值:
SETNX lock.foo <current Unix time + lock timeout + 1>
如果客户端获得锁,SETNX
返回1
,加锁成功。
如果SETNX
返回0
,那么该键已经被其他的客户端锁定。
接上一步,SETNX
返回0
加锁失败,此时,调用GET lock.foo
获取时间戳检查该锁是否已经过期:
如果没有过期,则休眠一会重试。
如果已经过期,则可以获取该锁。具体的:调用GETSET lock.foo <current Unix timestamp + lock timeout + 1>
基于当前时间设置新的过期时间。
注意: 这里设置的时候因为在SETNX
与GETSET
之间有个窗口期,在这期间锁可能已被其他客户端抢去,所以这里需要判断GETSET
的返回值,他的返回值是SET之前旧的时间戳:
解锁相对简单,只需GET lock.foo
时间戳,判断是否过期,过期就调用删除DEL lock.foo
set用法参考官方文档
SET key value [EX seconds|PX milliseconds] [NX|XX]
将键key
设定为指定的“字符串”值。如果 key
已经保存了一个值,那么这个操作会直接覆盖原来的值,并且忽略原始类型。当set
命令执行成功之后,之前设置的过期时间都将失效。
从2.6.12版本开始,redis为SET
命令增加了一系列选项:
EX
seconds – Set the specified expire time, in seconds.PX
milliseconds – Set the specified expire time, in milliseconds.NX
– Only set the key if it does not already exist.XX
– Only set the key if it already exist.EX
seconds – 设置键key的过期时间,单位时秒PX
milliseconds – 设置键key的过期时间,单位是毫秒NX
– 只有键key不存在的时候才会设置key的值XX
– 只有键key存在的时候才会设置key的值版本>= 6.0
KEEPTTL
-- 保持 key 之前的有效时间TTL一条命令即可加锁: SET resource_name my_random_value NX PX 30000
The command will set the key only if it does not already exist (NX option), with an expire of 30000 milliseconds (PX option). The key is set to a value “myrandomvalue”. This value must be unique across all clients and all lock requests.
这个命令只有当key
对应的键不存在resource_name时(NX选项的作用)才生效,同时设置30000毫秒的超时,成功设置其值为my_random_value,这是个在所有redis客户端加锁请求中全局唯一的随机值。
解锁时需要确保my_random_value和加锁的时候一致。下面的Lua脚本可以完成
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end 复制代码
这段Lua脚本在执行的时候要把前面的my_random_value
作为ARGV[1]
的值传进去,把resource_name
作为KEYS[1]
的值传进去。释放锁其实包含三步操作:’GET’、判断和’DEL’,用Lua脚本来实现能保证这三步的原子性。
前面两种分布式锁的实现都是针对单redis master实例,既不是有互为备份的slave节点也不是多master集群,如果是redis集群,每个redis master节点都是独立存储,这种场景用前面两种加锁策略有锁的安全性问题。
比如下面这种场景:
于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。
针对这种多redis服务实例的场景,redis作者antirez设计了Redlock (Distributed locks with Redis)算法,就是我们接下来介绍的。
集群加锁的总体思想是尝试锁住所有节点,当有一半以上节点被锁住就代表加锁成功。集群部署你的数据可能保存在任何一个redis服务节点上,一旦加锁必须确保集群内任意节点被锁住,否则也就失去了加锁的意义。
具体的:
my_random_value
,也包含过期时间(比如PX 30000
,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。
上面描述的算法已经有现成的实现,各种语言版本。
CRedLock * dlm = new CRedLock(); dlm->AddServerUrl("127.0.0.1", 5005); dlm->AddServerUrl("127.0.0.1", 5006); dlm->AddServerUrl("127.0.0.1", 5007); 复制代码
CLock my_lock; bool flag = dlm->Lock("my_resource_name", 1000, my_lock); 复制代码
CLock my_lock; bool flag = dlm->ContinueLock("my_resource_name", 1000, my_lock); 复制代码
my_resource_name
是加锁标识;1000
是锁的有效期,单位毫秒。
Lock
结构如下class CLock { public: int m_validityTime; => 9897.3020019531 // 当前锁可以存活的时间, 毫秒 sds m_resource; => my_resource_name // 要锁住的资源名称 sds m_val; => 53771bfa1e775 // 锁住资源的进程随机名字 }; 复制代码
dlm->Unlock(my_lock); 复制代码
综上所述,三种实现方式。
更多精彩原创关注公众号「柠檬的编程学堂」大厂程序员,十年多编程学习经验,用通俗易懂的方式与你分享技术学习和程序员的那些事。