引言
在多台机器上操作同一个数据时,需要分布式锁。(比如11.11的场景)
两大类分布式锁:
- 类CAS自旋式——只能靠自己不断询问的方式,不断尝试加锁 (IO太高),例如mysql,redis
- event事件式—— 靠event事件通知我后续锁的变化,轮询向外的过程,例如zookeeper,etcd
redis是用类CAS自旋式锁。
redis的worker是单线程串行的,处理clients的请求时,是串行的!比如c1来了,抢到了锁。c2来的时候,没抢到,被拒了。接下来可能会发生:
- 如果c1运行一段,挂掉了,但没释放锁。死锁了。
- 此时c2就一直自旋询问(网络IO)worker,好了没好了没好了没。worker给他一个timeout时间,等会再来问。
实现
Redis 锁主要利用 Redis 的 setnx 命令。伪代码:
1 | if (setnx(key, 1) == 1) // 获取锁。KEY 是锁的唯一标识,一般按业务来决定命名 |
上述锁存在一些问题:
死锁
如果 SETNX 成功,在设置锁超时时间后,服务器挂掉、重启或网络问题等,导致 EXPIRE 命令没有执行,锁没有设置超时时间变成死锁。 很多开源代码来解决这个问题,比如使用 lua 脚本。示例
1 | if (redis.call('setnx', KEYS[1], ARGV[1]) < 1) |
锁已解除
如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。 解决方案:加锁时,将锁value设为UUID(唯一标识码)。释放锁之前,先再次通过 get 命令获取加锁 key 的 value 值,然后判断 value 跟加锁时设置的 value 是否一致,这就看出 UUID 的重要性了,如果一致,就执行 delete() 方法释放锁,否则不执行。
1 | // 加锁 |
超时解锁导致并发
如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。
A、B 两个线程发生并发显然是不被允许的,一般有两种方式解决该问题:
- 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。
- 为获取锁的线程增加守护线程,让守护线程在一段时间后,重新去设置这个锁的LockTime。
抢不到锁
如果锁被C1占了,同时C2也想加锁,怎么办?两种解决办法:
- 客户端轮询。非常消耗服务器资源。
- 用Redis发布订阅功能。当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。