Redis 分布式锁
问题描述
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的 Java API 并不能提供分布式锁的能力。为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
Redis分布式加锁策略:
分布式锁:set key value NX 会出现死锁问题 锁永不过期
set key value EX 3000 NX EX防止死锁 ,但是会出现误删(业务1没执行完,但key的过期时间到了,key已失效,相当于业务1没加锁了,业务2可以进行加锁了,但业务2执行期间,业务1执行完毕,删除锁删除的是业务2的锁)
set key value EX 3000 NX 加上uuid避免误删 但释放锁不是原子操作(业务1判断uuid成功删除锁之前,锁到期了,被redis释放,业务2可以加锁,业务1此时会释放了业务2的锁)删除操作缺乏原子性。
set key value EX 3000 NX 加上uuid 加上LRU LUA 脚本保证删除的原子性
分布式锁主流的实现方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis 等)
- 基于 Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
- 性能:redis 最高
- 可靠性:zookeeper 最高
实现方式 SETNX key value
- EX second :设置键的过期时间为 second 秒。
- SET key value EX second 效果等同于SETEX key second value 。
- NX :只在键不存在时,才对键进行设置操作。
- SET key value NX 效果等同于 SETNX key value 。
SETNX key value 实现分布式锁,加上过期时间可以避免死锁
- 多个客户端同时获取锁(setnx)
- 获取成功,执行业务逻辑{从 db 获取数据,放入缓存},执行完成释放锁(del)
- 其他客户端等待重试
优化之设置锁的过期时间
设置过期时间有两种方式:
1. 首先想到通过 expire 设置过期时间(缺乏原子性:如果在 setnx 和 expire 之间出现异常,锁也无法释放)
setnx k1 v1 expire k1 100
2. 在 set 时指定过期时间(推荐)
set k1 v1 EX 100 NX
场景:如果业务逻辑的执行时间是 7s。执行流程如下
- index1 业务逻辑没执行完,3 秒后锁被自动释放。
- index2 获取到锁,执行业务逻辑,3 秒后锁被自动释放。
- index3 获取到锁,执行业务逻辑
- index1 业务逻辑执行完成,开始调用 del 释放锁,这时释放的是 index3 的锁,
导致 index3 的业务只执行 1s 就被别人释放。最终等于没锁的情况。
优化之 UUID 防误删
场景:
- index1 执行删除时,查询到的 lock 值确实和 uuid 相等
- index1 执行删除前,lock 刚好过期时间已到,被 redis 自动释放
- index2 获取了 lock
- index1 执行删除,此时会把 index2 的 lock 删除
问题:删除操作缺乏原子性。
优化之 LUA 脚本保证删除的原子性
1 @GetMapping( "testLockLua") 2 public void testLockLua() { 3 String uuid = UUID. randomUUID ().toString(); 4 //定义一个锁:lua 脚本可以使用同一把锁,来实现删除! 5 String skuId = "25"; // 访问 skuId 为 25 号的商品 100008348542 6 String locKey = "lock:" + skuId; // 锁住的是每个商品的数据 7 Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit. SECONDS ); 8 if (lock) { 9 Object value = redisTemplate.opsForValue().get( "num"); 10 int num = Integer. parseInt (value + ""); 11 redisTemplate.opsForValue().set( "num", String. valueOf (++num)); 12 // 定义 lua 脚本 13 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; 14 // 使用 redis 执行 lua 执行 15 DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); 16 redisScript.setScriptText(script); 17 // 设置一下返回值类型 为 Long 18 // 因为删除判断的时候,返回的 0,给其封装为数据类型。如果不封装那么默认返回 String 类型, 19 // 那么返回字符串与 0 会有发生错误。 20 redisScript.setResultType(Long. class); 21 // 第一个script 脚本 ,第二个是 key,第三个就是 key 所对应的值。 22 redisTemplate.execute(redisScript, Arrays.asList (locKey), uuid); 23 } else { 24 try { 25 Thread. sleep (1000); 26 testLockLua(); 27 } catch (InterruptedException e) { 28 e.printStackTrace(); 29 } 30 } 31 }
LRU脚本:
总结
三步骤
- 加锁
- 使用 lua 释放锁
- 失败重试
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
- 加锁和解锁必须具有原子性。
Redlock
Redlock是redis官方提出的实现分布式锁管理器的算法。这个算法会比一般的普通方法更加安全可靠。
Redlock 简介
在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。实现高效的分布式锁有三个属性需要考虑:
1、安全属性:互斥,不管什么时候,只有一个客户端持有锁
2、效率属性A::不会死锁
3、效率属性B:容错,只要大多数redis节点能够正常工作,客户端端都能获取和释放锁。
Redlock 算法
在分布式版本的算法里我们假设我们有N个Redis master节点,这些节点都是完全独立的,我们不用任何复制或者其他隐含的分布式协调算法。我们已经描述了如何在单节点环境下安全地获取和释放锁。因此我们理所当然地应当用这个方法在每个单节点里来获取和释放锁。在我们的例子里面我们把N设成5,这个数字是一个相对比较合理的数值,因此我们需要在不同的计算机或者虚拟机上运行5个master节点来保证他们大多数情况下都不会同时宕机。一个客户端需要做如下操作来获取锁:
1、获取当前时间(单位是毫秒)。
2、轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
3、客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
4、如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
5、如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。
红锁即在代码中不依赖于主从,将这5台机器视为平等的,在代码中依次对这5台机器去加锁,只有成功的机器数
大于一半
就算加锁成功,其他机器也就没必要再去操作了,相反,如果大于一半的机器失败了,就算失败,其他机器也就没必要再去操作了。由于是遍历操作这5台机器,也就不用关心有没有机器挂掉了,因为挂掉了自然算加锁失败。红锁方案要求机器数为奇数。而且从原理上来看,每一个请求都会
从前往后的顺序依次
去操作这些机器,而不是乱序的,也就不会出现死锁的问题。为什么叫红锁?
因为这个方案是redis创始人发明的,本应该叫 Redis Lock,但是简写成 RedLock。
Redission redLock使用:
配置Redission客户端的Redis集群
1 @Bean 2 public RedissonClient redissonClient() { 3 Config config = new Config(); 4 config.useClusterServers() 5 .setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒 6 //可以用"rediss://"来启用SSL连接 7 .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001") 8 .addNodeAddress("redis://127.0.0.1:7002"); 9 return Redisson.create(config); 10 }基于Redis的Redisson红锁
RedissonRedLock
对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock
对象关联为一个红锁,每个RLock
对象实例可以来自于不同的Redisson实例。1 RLock lock1 = redissonClient1.getLock("lock1"); 2 RLock lock2 = redissonClient2.getLock("lock2"); 3 RLock lock3 = redissonClient3.getLock("lock3"); 4 RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3); 5 // 同时加锁:lock1 lock2 lock3 6 // 红锁在大部分节点上加锁成功就算成功。 7 lock.lock(); 8 ... 9 lock.unlock();Redisson 监控锁
大家都知道,如果负责储存某些分布式锁的某些Redis节点宕机以后,而且这些锁正好处于锁住的状态时,这些锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了
leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。1 RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3); 2 // 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开 3 lock.lock(10, TimeUnit.SECONDS); 4 // 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开 5 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); 6 ... 7 lock.unlock();
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南