锁要实现的三个目标:
- 相互排斥。在任何给定时刻,只有一个客户端可以持有锁。
- 无死锁。最终,即使锁定资源的客户端崩溃或被分区,也始终可以获取锁定。
- 容错。只要大多数Redis节点启动,客户端就能够获取和释放锁。
1. 获取锁:
SET resource_name my_random_value NX PX 30000
该命令仅在密钥尚不存在时才设置key(NX选项),到期时间为30000毫秒(PX选项)。键设置为值“myrandomvalue”。此值必须在所有客户端和所有锁定请求中都是唯一的。
- myrandomvalue:必须当前唯一
- 30000:30000毫秒是你yu
2. 释放锁
使用随机值是为了以安全的方式释放锁,实现上使用了Redis的脚本:只有当key存在且其值恰好是我期望的随机值时才删除key。这是通过以下Lua脚本完成的:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这一点很重要,这可以避免删除由另一个客户端创建的锁。例如:
- 线程A获取锁,由于某些原因A线程挂起了。时间大于锁的过期时间。
- 锁过期后,线程B获取锁。
- 线程A恢复以后,处理完相关事件,向redis发起 del命令。锁被释放。
- 线程C获取锁。这个时候一个系统中同时两个线程(B和C)持有锁。
ok,这样我们完成redis分布式锁的实现。你以为这样就安全了吗,接下来我们设想这样一个情况:
首先,假设我们的Redis是主从的:
- 如果在线程A在master上拿到了锁。
- 在master将数据同步到slave上之前,master宕机。
- 线程B就从slave上又一次拿到了锁。
这时就会同时有两把锁存在的情况。对于这种情况(非常特殊),Redis官方提供了一个叫RedLock的解决方案。
RedLock
我们假设我们有N个Redis主机。这些节点完全独立,因此我们不使用复制或任何其他隐式协调系统。我们假设N = 5,这是一个合理的值,因此我们需要在不同的计算机或虚拟机上运行5个Redis主服务器,以确保它们以大多数独立的方式失败。
为了获取锁,客户端执行以下操作:
- 获取当前时间的毫秒值。
- 它尝试使用相同的键名和随机值按顺序获取所有N个实例中的锁。在步骤2期间,当在每个实例中设置锁定时,客户端使用与总锁定自动释放时间相比较小的超时以获取它(客户端超时时间,并不是key的过期时间)。例如,如果自动释放时间是10秒,则超时可以在~5-50毫秒范围内。这可以防止客户端长时间保持阻塞状态,试图与已关闭的Redis节点通信:如果一个实例不可用,我们应该尝试尽快与下一个实例交谈。
- 客户端通过从当前时间中减去在步骤1中获得的时间戳来计算获取锁定所经过的时间。当且仅当客户端能够在大多数实例(至少3)中获取锁,并且获取锁所经过的总时间小于锁定有效时间时,才认为锁定被获取。
- 如果获得了锁,则其有效时间被认为是初始有效时间减去经过的时间,如步骤3中计算的那样。
- 如果客户端由于某种原因(无法锁定N / 2 + 1实例或有效时间为负)未能获取锁定,它将尝试解锁所有实例(即使它认为不是能够锁定)。
我个人觉得对于小公司这还是相当昂贵的。
其他的细节请参考redis官方文档。
网友评论