Redis实践篇(分布式锁)

94 阅读4分钟

关于,分布式锁其实是一个非常常见的问题,但是常见,不一定见得在许多方面都能详细了解。

如何用redis实现分布式锁?

分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。

image.png

注意点:

  1. 加锁和解锁需要是同一个主体,否则业务会发生错乱;
  2. 要有锁超时机制,避免锁被永久占用;
  3. 加锁和解锁要是原子性操作;

加锁

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[1then  
    return redis.call("del",KEYS[1])  
else  
    return 0  
end

优缺点

优点:

  1. 实现简单,现有的redis即可实现;
  2. 性能高效,redis的高性能不必说;
  3. 如果是集群部署,没有单点故障; 缺点:
  4. 锁超时时间不好确定,如果业务执行时长超过锁超时时间,则可以由于提前释放导致业务错乱;
  • 那如何解决呢? 基于续约的方式,也就是正常设置锁超时时间后,另起一个线程,定时去判断锁的情况,定时时间要小于锁超时时间,如果主线程还在执行业务,同时锁快失效了,就延长锁的超时时间。(redission)
  1. Redis的主从复制是异步复制的,如果redis主节点还未将锁同步到其他从节点时,主节点挂了,就会导致锁失效了。

如何实现redis集群下的锁的可靠性?

redis官方提供了一个叫做redlock的算法。它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败

这样一来,即使有某个 Redis 节点发生故障,因为锁的数据在其他节点上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。

加锁成功要同时满足两个条件(简述:如果有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功):

  • 条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁;
  • 条件二:客户端获取锁的总耗时(t1)没有超过锁的有效时间。

加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁的最初有效时间」减去「客户端为获取锁的总耗时(t1)」。

加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。