关于,分布式锁其实是一个非常常见的问题,但是常见,不一定见得在许多方面都能详细了解。
如何用redis实现分布式锁?
分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。
注意点:
- 加锁和解锁需要是同一个主体,否则业务会发生错乱;
- 要有锁超时机制,避免锁被永久占用;
- 加锁和解锁要是原子性操作;
加锁
Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁的加锁操作,而且它是原子性的。
- 如果key存在,则显示插入成功,表示加锁成功;
- 如果key不存在,则显示插入失败,表示加锁失败,已被其他主体占用;
那加锁的值应该用什么标识呢?由于上面的注意点已经有所表示,加锁和解锁需要同一个主体,所以加锁的值要保存标识主体的唯一性。这里,可用一个UUID即可,解锁时,也需要用这个值去比较。
例如:
SET lock_key unique_value NX PX 10000
- lock_key 就是 key 键;
- unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
- NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
- PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
解锁
解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
优缺点
优点:
- 实现简单,现有的redis即可实现;
- 性能高效,redis的高性能不必说;
- 如果是集群部署,没有单点故障; 缺点:
- 锁超时时间不好确定,如果业务执行时长超过锁超时时间,则可以由于提前释放导致业务错乱;
- 那如何解决呢? 基于续约的方式,也就是正常设置锁超时时间后,另起一个线程,定时去判断锁的情况,定时时间要小于锁超时时间,如果主线程还在执行业务,同时锁快失效了,就延长锁的超时时间。(redission)
- Redis的主从复制是异步复制的,如果redis主节点还未将锁同步到其他从节点时,主节点挂了,就会导致锁失效了。
如何实现redis集群下的锁的可靠性?
redis官方提供了一个叫做redlock的算法。它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败
这样一来,即使有某个 Redis 节点发生故障,因为锁的数据在其他节点上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。
加锁成功要同时满足两个条件(简述:如果有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功):
- 条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁;
- 条件二:客户端获取锁的总耗时(t1)没有超过锁的有效时间。
加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁的最初有效时间」减去「客户端为获取锁的总耗时(t1)」。
加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。