为何需要分布式锁?在单机的服务中,对于并发下的锁争用,可以在进程内实现。然而分布式的情况下,各个服务分开部署,无法通过这种方式获得,因此需要分布式锁来进行同步。
# 实现
分布式锁可以怎样实现呢?实际上主要就是能够实现一个大家都能读写的东西,且保证一个写入了另一个能感知到并被拒绝,也就实现了所谓的锁。
# MySQL
依靠 MySQL 的唯一性去保证,例如插入一条记录,根据主键 / 唯一性的列,来保证只有一个能插入成功,成功者即获得锁。
同时需要设置好记录的时间,以便于实现超时释放的问题,避免死锁。
但高并发的情况下,MySQL 死锁检测比较影响效率,相对来说它是比较重的,较少用于加锁。
# Redis
最常听说用于加锁的组件之一。相对于 MySQL 插入记录来说,Redis 就比较轻量,只需要设置一个值即可,效率要高的多。
# ZooKeeper
通过创建临时节点用于加锁,通过顺序节点,多个应用获取将分配一个序号,根据自己是否是最小的序号可判断是否获得了锁。
# Redis 锁存在的问题
# 实现命令
加锁或解锁,都存在原子操作的问题,即如果这个步骤不是一次完成的,那多步之间就可能出现问题,导致锁的状态异常。因此要保证加锁或解锁的命令原子执行,不存在中间状态。
在 Redis 中,加锁可以使用扩产的 set 命令:
SET lock 1 EX 10 NX |
这相当于结合了 SETNX 和 EXPIRE 命令,设置锁的同时设置了过期时间。
删除时要注意,如果简单的删除锁可能在极端情况下删除了属于其他线程的锁。因此,加锁时可设置一个唯一的 id,在解锁时需要判断这个 id 是否属于自己,属于的才能够解锁。
例如 SET lock $uuid EX 10 NX
在解锁时使用相同的 id 去判断,但判断 / 删除,又是两条命令,因此这里需要使用 lua 脚本来实现原子操作:
if redis.call("GET",KEYS[1]) == ARGV[1] | |
then | |
return redis.call("DEL",KEYS[1]) | |
else | |
return 0 | |
end |
# 锁超时时间
为了避免一个线程获取锁后,由于程序崩溃或其他原因导致锁迟迟得不到释放产生死锁,所以设置了超时时间。但这又引发了另一个问题:时间设置多少呢?太短可能获取到锁后任务还未完成就失效了,太长,一方面浪费资源,另一方面也只是缓解了这个问题,并没有彻底消除。
因此这里需要一种机制,例如开启守护线程,来在任务未完成的时候,自动续期锁时间。
Java 中 Redission 实现了这个功能
# 分布式下的问题
上述的实现方式都基于一个 Redis 服务之中,如果是在分布式部署 + 哨兵模式下,由于主从的同步是异步的,因此可能会出现在主 Redis 加了锁,还未同步到从库就挂掉的情况,此时程序获得的锁就丢失了。
基于 Redis,可使用 Redlock。参见:Redlock 问题
# 总结
可见各种锁都类似:操作某个数据,判断是否成功作为获得锁的依据。
其中需要考虑:
- 锁超时:防止死锁,zookeeper 在连接断开的情况下会自动释放锁。
- 可重入:得到锁的线程再次获取时可获得,如递归调用下是否能正确执行。
- 自己的锁:能够判断一个锁是否是自己的,避免误删除。实际能够判断它,也就可以实现可重入了。
但从对于 Relock 的争论可以看出,在极端情况下,分布式锁不总是能保证其绝对的正确性。因此也需要在应用层 / 数据库层做出相应的保护,保证极端情况下的数据正确性,当然,最好可以调整架构,不使用分布式锁。