如何使用Redis实现分布式锁?

文章目录

  • 前言
    • 1、如何理解分布式锁?
    • 2、分布式锁的常用实现
    • 3、注意问题:
  • 一、基于Redis实现分布式锁
  • 二、Redis分布式锁注意事项
    • 1、如何合理设置超时时间
    • 2、Redis 如何解决集群情况下分布式锁的可靠性?
    • 3、采用哪种方案做自己的redis分布式锁使用?
  • 总结


前言

1、如何理解分布式锁?

  JDK的锁可以处理单机的加锁场景,对于多台机器的分布式服务需要一个公共的第三方服务负责分布式锁的维护
如何使用Redis实现分布式锁?_第1张图片

2、分布式锁的常用实现

实现分布式锁目前有三种流行方案,即基于数据库、Redis、ZooKeeper 的方案。

  • 基于关系型数据库实现分布式锁:依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等
  • 基于Redis 实现分布式锁:利用 setnx 和 expire 命令实现加锁
  • 基于ZK实现分布式锁:ZooKeeper 有四种节点类型,包括持久节点、持久顺序节点、临时节点和临时顺序节点,利用 ZooKeeper 支持临时顺序节点的特性,可以实现分布式锁

3、注意问题:

  • 死锁:当拿到锁的服务异常挂掉,如何防止死锁
  • 原子性:如何保证加锁和设置过期时间是原子操作?
  • 脑裂问题:集群或者主从同步时产生的数据不一致,导致新的客户端有可能拿到锁,但之前的客户端以为自己还有锁,那么就出现两个客户端拿到了同一个锁的问题
  • 可重入:同一个客户端可以多次获取同一把锁

一、基于Redis实现分布式锁

  在加锁的过程中,实际上就是在给 Key 键设置一个值;为避免死锁,要给 Key 键设置一个过期时间;为避免误删key,还需要在在删除key之前判断是否是拿到锁的客户端执行的操作

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 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。这个时候, unique_value 的作用就体现出来,实现方式可以通过 lua 脚本判断 unique_value 是否为加锁客户端。

使用 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分布式锁注意事项

1、如何合理设置超时时间

   设置锁的时候需要设置过期时间,防止出现死锁;但过期时间很难预估,具有不确定性,比如某天业务系统负载高,导致了业务处理时间拉的很长等;

  • 可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可。
  • Redisson的分布式锁也是基于此思路实现;对于一般的分布式场景,可以直接使用redisson提供的分布式锁

2、Redis 如何解决集群情况下分布式锁的可靠性?

   由于 Redis 集群(主从)数据同步到各个节点时是异步的,如果在 Redis主节点获取到锁后,在没有同步到从节点时,Redis 主节点宕机了,此时新选举的 Redis 主节点依然可以获取锁,所以多个客户端就可以同时获取到锁。

官方设计了一个分布式锁算法 Redlock 解决了这个问题,可通过Redisson引入

  • 假设目前有 N 个独立的 Redis 实例, 客户端先按顺序依次向 N 个 Redis 实例执行加锁操作。这里的加锁操作和在单实例上执行的加锁操作一样,需要注意的是,Redlock 算法设置了加锁的超时时间,为了避免因为某个 Redis 实例发生故障而一直等待的情况
  • 当客户端完成了和所有 Redis 实例的加锁操作之后,如果有超过半数的 Redis 实例成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功

3、采用哪种方案做自己的redis分布式锁使用?

  • 脱离了具体业务谈论Redis分布锁的实现方案难免有所欠缺
  • 对于一般业务,只需保证数据的最终一致性,使用普通的方案即可,比如使用set NX PX,再加上后台线程给锁续期的方案;既可以解决死锁问题,也可以处理锁提前释放问题;Redisson组件有这样现成的解决方案,开箱即用,非常简单
  • 对于要求数据强一致性的业务,比如银行金钱相关;需要考虑锁的高可用、互斥性,典型的比如主从redis架构发生主从切换时,由于数据未及时同步导致的多个客户端拿到相同的锁;可以考虑Redlock的相关方案,Redisson组件也提供了Redlock相关的实现
  • 具体的落地实施上面:推荐采用Redisson的redis锁实现方案,不需要自己重复造轮子,考虑的情况更多、异常情况也考虑的相对完善(通用组件的代码一般更鲁棒),如果你还在纠结,不妨试试Redisson

总结

在基于单个 Redis 实例实现分布式锁时,对于加锁操作,需要考虑四个条件:

  • 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,因此使用 SET 命令带上 NX 选项来实现加锁;
  • 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,因此在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
  • 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,因此使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端。
  • 由于过期时间的不确定性,需要考虑给锁定期续约的情况

基于主从或者集群的redis架构,由于数据的异步性,当主从节点切换时,可能出现多个客户端拿到同一把锁的情况。根据实际需求,如果可以容忍,采用类似于单机的实现方案即可;反之可以采用Redlock的实现方案







相关文章:

Redisson解决Redis分布式锁提前释放问题
基于Redis的分布式锁到底安全吗?

你可能感兴趣的:(MQ)