分布式锁(Redis分布式锁的问题、锁超时、Redlock、Redission)

单体锁与分布式锁

单体锁(JVM级别):单体应用中,如果对共享数据不进行加锁操作,多线程操作共享数据时会出现数据一致性问题,解决办法通常是加锁,如单体锁(Synchronized、RentranLock)来保证单个实例并发安全。

然而,当单体应用部署到多个实例(Tomcat)上时,每个 Tomocat 实例是一个JVM进程,单体锁是JVM层面锁,只能控制单个实例上的并发访问安全,多实例下存在数据一致性问题。

分布式锁是指所有服务中的所有线程都去获取同一把锁,但只有一个线程可以成功的获得锁,其他没有获得锁的线程必须全部等待,直到持有锁的线程释放锁。

分布式锁和单体应用中 Synchronized 等作用相同,都是表示希望避免并发,如果有并发的话就排队等候。

redis的setnx命令,在设置锁的同时设置超时时间,并在finally块删除锁。

Redis分布式锁

redis命令说明:
1. setnx命令:set if not exists,当且仅当 key 不存在时,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。
- 返回1时,说明该进程获得锁,将 key 的值设为 value
- 返回0时,说明其他进程已经获得了锁,进程不能进入临界区。
命令格式:setnx key value

2. get命令:获取key的值,如果存在则返回;如果不存在,则返回nil
命令格式:get key

3. getset命令:该方法是原子的,对key设置newValue值,并且返回key原来的旧值。
命令格式:getset key newValue

4. del命令:删除Redis中指定的key
命令格式:del key

方案一:基于 setnx 命令的分布式锁

  1. 加锁:使用 setnx 命令进行加锁,当指令返回结果为1时,说明成功获得到锁

  2. 解锁:当得到锁的线程执行完任务之后,使用 del 命令释放锁,以便其他线程可以继续执行setnx 命令来获得锁

    • 存在问题:假设线程获取了锁之后,在执行任务的过程中出现异常、系统宕机,导致锁没有执行 del 命令释放锁,那么竞争该锁的线程都会执行不了,最终出现死锁情况
    • 解决方案:给锁设置一个超时时间
  3. 设置锁超时时间:给 setnx 的 key 设置一个超时时间,以保证即使没有被显式释放,这把锁也会在一定时间后自动释放,可以使用 expire 命令设置锁超时时间

    • 存在问题:setnx 和 expire 命令不是原子性的操作,假设某个线程执行 setnx 命令,成功获得了锁,但是还没来得及执行 expire 命令服务器就挂掉了,这种情况下,这把锁就没有设置上过期时间,别的线程无法获得此锁,最后变成了死锁
    • 解决方案:Redis 的 set 命令支持在获取锁的同时设置 key 过期时间这种原子操作
  4. 使用 set 命令加锁并设置锁过期时间 :命令格式:set nx ex

方案二:基于 RedLock 的分布式锁(解决锁丢失问题)
通常为了实现高可用,Redis会进行集群部署 (例如主从架构、哨兵架构),但这这样又会出现锁丢失问题:

假如线程A在Redis的master节点上拿到了锁,并且加锁的key还没同步到slave节点,此时恰好master节点发生故障,其中一个slave节点就会升级为master节点。由于锁没有同步成功,线程B就可以获取同个key的锁,这就会出现线程A还没执行完,线程B又来执行,导致并发安全问题

Redis作者提出了一种Redlock分布式锁算法来解决此问题,Redlock是一种基于多节点Redis实现分布式锁的算法。

Redlock原理:部署多个Redis master节点,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。当超过半数的master节点加锁成功才算成功获取到了锁。

Redlock简化实现过程:

  • 按顺序向5个master节点请求加锁
  • 根据设置的超时时间来判断,是不是要跳过该master节点
  • 如果大于等于3个节点加锁成功,并且消耗时间小于锁的有效期,即认定节点加锁成功
  • 如果获取锁失败则解锁。 (没有在至少N/2+1个master实例取到锁,或者获取锁时间已经超过了有效时间,客户端要在所有的master节点上解锁.)
  • 为了保证更高效的获取锁,还可以设置重试策略,在一定时间后重新尝试获取锁,同时要设置重试次数

Redis分布式锁存在的问题

误删锁:UUID防止误删锁

如果设置锁的超时时间为10s,程序没执行完但是锁已经被释放了,线程2重新获得锁,但是线程1执行完删除了线程2的锁,导致其他线程重新获得锁,可以使用UUID防止误删锁,同一把锁在上锁时设置不同的uuid,删除锁时判断是否是当前线程的锁,如果是,就删除,如果不是,就不删除。

锁操作的原子性:lua脚本/set nx ex

Redis的加锁(lock)和释放锁(unlock)不是原子操作,使用了,可以通过执行Lua脚本来保证获取锁和释放锁的原子性。

local lockKey = "mylock"
local clientId = "unique_client_id"

local currentLockHolder = redis.call("GET", lockKey)
if currentLockHolder == clientId then
    return redis.call("DEL", lockKey)
else
    return 0
end

锁超时续期:Redlock和Redisson

如果程序还没有执行完,但是锁超时了,可以使用Redlock和Redisson两个常用的分布式锁的实现库,实现锁续命

  • Redlock:Redlock是一个由Redis的创始人提出的算法,用于实现分布式锁。在Redlock中,锁的超时处理主要通过续约机制实现。
    1. 获取锁时,会设置一个锁的超时时间(例如30秒)。
    2. 在持有锁的客户端执行业务逻辑期间,定期(例如每10秒)续约锁的超时时间,延长锁的有效期
  • Redisson:Redisson是一个基于Redis的Java分布式应用开发框架,提供了丰富的分布式功能,包括分布式锁。在Redisson中,锁的超时处理主要通过看门狗(Watchdog)机制实现。
    1. 获取锁时,会设置一个锁的超时时间(例如30秒)。
    2. 同时,启动一个看门狗线程,定期检查锁的剩余过期时间。
    3. 如果看门狗线程检测到锁的剩余过期时间低于某个阈值(例如10秒),会自动延长锁的超时时间,防止锁在业务逻辑执行期间过期。

Redlock和Redisson在发生死锁时有不同的行为:

  • Redlock算法本身并没有内置的死锁检测和解除机制。如果发生死锁,即多个客户端同时持有不同的锁并且无法释放,Redlock无法自动解除死锁状态
  • Redisson中的分布式锁具有内置的死锁检测和解除机制。Redisson使用了RedLock的基本思想,但在其实现中添加了死锁监测和解除的功能。当Redisson检测到某个客户端的锁处于死锁状态时,它会尝试解除死锁并释放相应的锁。Redisson的死锁解除机制依赖于Redis的WATCH命令和Lua脚本,用于检测死锁并执行解锁操作。

RedLock加解锁代码

@Autowired
private Redission redission;

//获取某个key的锁(分布式锁)
RLock lock = redission.getLock(key);
lock.lock();
......
//释放锁
lock.unlock();

Redission加解锁代码

RLock redissonLock = redisson.getLock(lockKey);
try {
    //加锁
    redissonLock.lock();

}catch (Exception e){

}finally {
    redissonLock.unLock();
}

Redission

Redission:Java客户端,具有内存数据网格的特点。官网描述它能做的事如下:Redission官网

  • 扩展Java应用程序:Java上的分布式应用程序需要基于Redis的对象、集合、锁、同步器和服务。
  • 缓存:基于Redis的Java缓存实现,如JCache API、Hibernate二级缓存、Spring缓存和应用级缓存。
  • 数据源缓存基于Redis的Java缓存,用于数据库、web服务或使用通读、写进和写后策略的任何其他数据源。
  • 分布式Java任务调度和执行:Java上的任务处理可以与基于Redis的ExecutorService和ScheduledExecutorService的分布式实现并行运行。
  • 分布式数据处理:基于Java的MapReduce编程模型来处理存储在Redis中的大量数据。
  • 简单的Redis Java客户端:Redisson是最先进和最简单的Redis Java客户端。它有零学习曲线,因此你不需要知道任何Redis命令开始与它的工作。
  • Web会话集群使用基于Redis的Tomcat会话管理器和Spring会话实现的用户会话负载平衡。
  • Microservices:基于Redis的可靠Java微服务通信使用RPC、消息传递和缓存。
  • 消息传递:基于Redis的Java消息代理,用于发布/订阅和流消息传递。
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值