我们知道reids 可以通过 SET lock_key unique_value NX PX 10000
来做分布式事务锁,但其中还有些细节值得思考。
解锁
解锁就是删key,为了防止解错了锁,删掉了不是自己持有的,就需要对 key 的值 unique_value 进行判断,得是『我』自己才行。
解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
1 | // 释放锁时,先比较 unique_value 是否相等,避免锁的误释放 |
go demo
关于 redis lua 释放锁的操作,可以看下这个 go 版本的 demo:
1 | package main |
过期时间
过期时间的设置也有点学问,太长了还好,仅仅影响性能。太短了,锁自己就释放了,问题就大了。
如果一定要纠结这个过期时间,有种方案:起一个守护进程来对这个 key 续约,快到期的时候,如果没用完,就续约。程序用完之后杀死这个守护进程(停止续约)
主从延迟
如果主从延迟了,其他客户端读到的从库没更新最新数据,其实没问题,因为主节点没挂,set 操作都要走主节点的。
如果主节点挂了呢?
主节点挂了,但是挂之前 A 已经拿到锁,这时候新主被选举出来,把锁给了 B,这时候 A/B 都持有这个锁。
怎么办?
redis 提供了一种分布式锁算法『红锁』 Redlock
它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署 5 个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
这样一来,即使有某个 Redis 节点发生故障,因为锁的数据在其他节点上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。
Redlock 算法加锁三个过程:
第一步是,客户端获取当前时间(t1)。
第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作:
加锁操作使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。
如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给「加锁操作」设置一个超时时间(不是对「锁」设置超时时间,而是对「加锁操作」设置超时时间),加锁操作的超时时间需要远远地小于锁的过期时间,一般也就是设置为几十毫秒。
第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。
可以看到,加锁成功要同时满足两个条件(简述:如果有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功):
条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁;
条件二:客户端从大多数节点获取锁的总耗时(t2-t1)小于锁设置的过期时间。
加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2-t1)」。如果计算的结果已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。
加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。