Redis实现可靠的分布式锁

1.Redis分布式锁的实现基础

不管是单机的锁还是分布式锁,要达成锁的功能一定要有原子性、互斥性的能力,Redis提供了好几个命令可以选择,其中使用最常用的就是SETNX命令:如果key不存在,就设置,否则就什么也不做。在分布式环境下,多个线程都来对某个key执行SETNX命令,Redis能够保证只有1个线程能够执行成功,这样就可以基于这个特点来做一个分布式锁。

2.死锁问题

如果单纯用Redis的SETNX命令来实现分布式锁,还有很多问题。例如某个线程加锁成功,如果这个线程发生了永久性的阻塞或者进程结束,那么Redis里面的这个Key就永远无法释放,这样就发生了死锁。
解决死锁也很简单:为key加一个过期时间即可,而且设置值和设置过期时间要保持原子性,Redis提供了一个命令SET lock EX 30 NX 就可以。

3.误释放问题

A线程加锁成功设置过期时间10s,此时A线程发生了阻塞;时间来到第11s,B线程过来加锁发现这把锁被过期自动解除了,B线程加锁成功,此时A线程苏醒过来执行完任务去unlock,此时解掉的是B加的锁,这就发生了误释放的问题。
解决方案:在加锁的时候可以在锁上添加一个线程的唯一标识,例如UUID等。释放的时候要判断一下自己线程的ID和锁里面的ID是否相等,不相等就不能释放锁。这个线程ID可以保存在ThreadLocal中。
这个步骤GET和DEL操作是两步,要保证原子性,可以将get和del命令封装成Lua脚本来执行。

4.锁续期问题

刚才的场景里,如果A线程执行某个任务很久,Redis自动释放了锁,这好像也不是很合理。可以设置一个方案来对这把锁进行续期,而且不会永久续下去。
Redisson这个客户端框架就提供了类似的功能:看门狗。他的核心思想就是通过后台线程对加锁任务的不断检查,如果发现工作线程还在进行中那么就会对这把锁进行续期,一直到最大续期容忍时间。他的优势就是无需对每一个工作线程分配一个守护线程来做,而是创建一个续期任务对象,交给一个定时任务+递归的模式去处理。

5.加锁本地化策略

在高并发场景下,加锁成功的只有1个,其它都会加锁失败。这些加锁失败的线程在网络上的消耗和对Redis的集中访问势必会产生一定的负面影响。本地化策略的思想就是,线程在去Redis加锁之前,先尝试在本地JVM里面竞争一遍(可以通过ReentrantLock或者synchronized),竞争成功了的线程再去Redis加锁。

6.红锁 RedLock

单节点的Redis拿来做分布式锁其实已经很优秀了,但是在极端情况下例如发生了主备切换,就可能导致锁的丢失。RedLock的思想就是:部署多个master实例,这些实例不在一个网段和机房,完全物理隔离,利用奇数个实例对其进行同时加锁,超过一半成功就算加锁成功。

  1. 客户端获取本地时间戳T1,分别向5台Redis加锁。
  2. 如果>=3台都加锁成功,然后获取本地时间T2,且T2-T1 < 锁的过期时间,表示加锁成功。
  3. 如果加锁失败,则释放所有节点的锁。

但是红锁也不是无懈可击,他存在NPC(网络延迟、进程暂停、时钟漂移)问题,例如进程暂停、时钟漂移等问题。但是这些问题并不是Redis专属的,而是分布式环境下本身就会存在的问题。例如:
在步骤3后,客户端发生了长时间的GC,锁被自动释放。另一个客户端就可以加锁成功,但这个问题在分布式环境下本身就是一个问题。但是如果GC等原因发生在步骤3之前,那么T2-T1就可以判断出来是否加锁成功,就不会发生冲突。
而且客户端获取时间戳这个操作在分布式环境下也要保证同步才能避免出问题,如果发生了时钟跳跃也是有问题的。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在 Spring Boot 中,可以使用 Redis 的 `setIfAbsent` 方法来实现分布式锁。下面是一个简单的示例代码: ```java @Autowired private RedisTemplate<String, String> redisTemplate; public boolean acquireLock(String lockKey, String requestId, int expirationTime) { Boolean isSet = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expirationTime, TimeUnit.MILLISECONDS); return isSet != null && isSet; } public void releaseLock(String lockKey, String requestId) { String currentValue = redisTemplate.opsForValue().get(lockKey); if (currentValue != null && currentValue.equals(requestId)) { redisTemplate.delete(lockKey); } } ``` 在上面的示例中,`acquireLock` 方法尝试获取分布式锁。它使用 Redis 的 `setIfAbsent` 方法来在指定的键不存在时设置键值对,并设置了过期时间。如果成功设置了键值对,说明获取到了锁,返回 true;否则返回 false。 `releaseLock` 方法用于释放分布式锁。它首先获取当前锁的值,检查是否与传入的请求 ID 相同,如果相同则删除该键,释放锁。 在使用分布式锁时,通常会将请求 ID 设置为唯一标识符,以便在释放锁时进行校验。 需要注意的是,使用 Redis 实现分布式锁并不是完全可靠的,可能存在一些竞态条件。在实际使用中,您可能需要考虑更加复杂的方案,例如使用 Redlock 算法或基于 Redisson 等第三方库实现分布式锁。 希望对您有所帮助!如果您有任何进一步的问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Minor王智

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值