Redis实现分布式锁
Redis实现分布式锁
概要
分布式锁是一种在分布式系统中,用于确保多个节点在并发访问共享资源时,保证资源操作的互斥性和一致性的一种机制。它在多台机器上协调资源访问,防止不同节点同时对同一资源进行操作,从而避免数据不一致或资源竞争的问题。
一、分布式锁的常见实现方式
分布式锁是用来解决分布式应用中并发冲突的一种常用手段,实现方式有:基于MySQL、Zookeeper、redis等。
1. 基于数据库实现
使用数据库中的锁表,通过事务和唯一性约束来实现分布式锁。
优点:简单易用,容易实现。
缺点:性能较差,数据库压力较大,不适合高并发场景。
2. 基于缓存(如 Redis)实现
利用 Redis 的原子操作(如 SET NX)来实现分布式锁,常见的实现如 Redis 的 SET key value NX PX timeout 命令。
优点:性能高,支持高并发,Redis 原生支持过期时间。
缺点:需处理锁失效、超时等问题。
3. 基于 Zookeeper 实现
使用 Zookeeper 的临时有序节点机制来实现分布式锁。每个客户端创建一个临时节点,通过最小节点获得锁,删除节点释放锁。
优点:Zookeeper 保证强一致性,适用于高可靠性的场景。
缺点:实现复杂,性能不如 Redis。
二、分布式锁的关键特性
1. 互斥性
同一时间只能有一个客户端获得锁,确保操作的原子性。
2. 可重入性
同一客户端可多次获取锁,防止死锁(可选)。
3. 超时机制
锁设置自动失效时间,防止死锁或资源长时间占用。
4. 故障恢复
客户端或节点宕机后,锁应该能自动释放,避免资源被永久占用。
三、基于缓存Redis实现的分布式锁
1. 基础使用
查阅Redis的使用手册,可以看到:Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
基本语法:SETNX KEY_NAME VALUE
2. 容易遇到的坑
比如下面这段代码:
1 <?php 2 3 $ok = $redis->setNX($key, $value); 4 5 if ($ok) { 6 $cache->update(); 7 $redis->del($key); 8 } 9
我们来分析这样写可能会产生的问题(主要是针对保证设置锁和过期时间的原子性)
1)因为 SetNX 不具备设置过期时间的功能,所以我们需要借助 Expire 来设置,同时我们需要把两者用 Multi/Exec 包裹起来以确保请求的原子性,以免 SetNX 成功了 Expire 却失败了
1 <?php 2 $redis->multi(); 3 $redis->setNX($key, $value); 4 $redis->expire($key, $ttl); 5 $redis->exec();
2)当多个请求到达时,虽然只有一个请求的 SetNX 可以成功,但是任何一个请求的 Expire 却都可以成功,如此就意味着即便获取不到锁,也可以刷新过期时间,如果请求比较密集的话,那么过期时间会一直被刷新,导致锁一直有效。于是乎我们需要在保证原子性的同时,有条件的执行 Expire,接着就需要写一个Lua 脚本来控制。
3)但是这样一个简单的功能还需要写个Lua脚本,实在有些麻烦。其实 Redis从 2.6.12版本开始 ,SET 涵盖了 SETEX 的功能,并且 SET 本身已经包含了设置过期时间的功能,也就是说,我们前面需要的功能只用 SET 就可以实现。
1 <?php 2 3 $ok = $redis->set($key, $value, array('nx', 'ex' => $ttl)); 4 5 if ($ok) { 6 $cache->update(); 7 $redis->del($key); 8 }
四、应用场景
主要有这三个场景:
- 定时任务调度:在集群中,防止多个实例同时执行定时任务。
- 资源竞争:控制对共享资源的并发访问,如库存扣减。
- 事务协调:在分布式系统中处理跨节点的事务。
这里针对资源竞争的情况下,来看看redis分布式锁的使用。
1. 单用户并发(对同一操作发起多次请求)
单用户并发的场景有:支付、抽奖、领取奖励、刷新页面初始化用户数据等
全局唯一锁(只有用户自己能拿到这把锁)
1 public function singleTest($openid) 2 { 3 $redis = new RedisServer(REDIS_HOST, REDIS_PORT); //获取redis实例化对象 4 $key = 'single_test'.$openid; //这里openid是指用户唯一标识 5 //设置锁 6 $ok = $redis->set($key,1, array('nx', 'ex' => 10)); 7 if ($ok) { 8 //更新缓存 9 //$cache->update(); 10 if ($redis->get($key)) { 11 $redis->del($key); 12 } 13 } 14 }
2. 多用户并发(多用户同时对有限的公共资源进行操作)
多用户并发的场景有:秒杀、抢购等(短时间内多用户争夺数量有限的物品)
全局锁(也叫互斥锁、排他锁):任何一个时刻只有1人能够持有这把锁,其他人等待锁的释放,重新抢锁。
实现:Redis setnx ,Memcached add ,MySQL 行锁、表锁。
下面是用Redis来实现的一段代码:
1 public function multiTest() 2 { 3 $redis = new RedisServer(REDIS_HOST, REDIS_PORT); //获取redis实例化对象 4 $key = 'multi_test'; 5 $random = Common::generateRandom(); //引入一个随机值:唯一的字符串 7 $ok = $redis->set($key, $random, array('nx', 'ex' => 10)); 8 if ($ok) { 9 //更新缓存 10 //... 11 12 //先判断随机数,是同一个则删除锁 13 if ($redis->get($key) == $random) { 14 $redis->del($key); 15 } 16 } 17 }
这里引入随机值的原因:
如果一个请求更新缓存的时间比较长,甚至比锁的有效期还要长,导致在缓存更新过程中,锁就失效了,此时另一个请求会获取锁。
但前一个请求在缓存更新完毕的时候, 如果不加以判断直接删除锁,就会出现误删除其它请求创建的锁的情况。所以我们在创建锁的时候需要引入一个随机值。
五、Redisson
Redisson 广泛应用于 Java 应用 中,是一个开源的Redis客户端库,它可以轻松实现分布式系统中的各种功能,如分布式锁、分布式集合、分布式信号量等。Redisson 提供了一个简化的 Redis 客户端 API,并且是线程安全的,封装了复杂的 Redis 操作,使得在分布式环境中使用 Redis 更加便捷。
1. 引入依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.21.0</version> <!-- 可以根据实际需求选择版本 --> </dependency>
2. 使用举例
在电商系统中,用户提交订单时可能会发生超卖问题。通过 Redisson 的分布式锁,系统可以确保同一件商品在同一时间只能被一个用户锁定,从而避免多个用户同时下单导致超卖。
直接使用 Redis 的 SETNX
可以实现一个简单的分布式锁,但需要开发者手动处理很多细节,比如:
- 设置锁的过期时间(避免死锁)。
- 检查并删除锁时需要保证原子性。
- 处理锁的自动续期。
Redisson 封装了这些复杂的逻辑,提供了更方便、安全的分布式锁实现。
示例如下:
1 RLock lock = redisson.getLock("order-lock:" + orderId); 2 try { 3 if (lock.tryLock(0, 10, TimeUnit.SECONDS)) { 4 // 处理订单逻辑 5 } 6 } finally { 7 lock.unlock(); 8 }
六、针对STW问题的处理
在 Redis 分布式锁下遇到 STW(Stop-the-World)时,任务可能会因为垃圾回收、网络延迟等原因暂停执行,从而导致任务超时或锁过期。为了确保任务能够顺利完成,并且处理好锁的安全问题,需要从以下几个方面进行设计:
1. 锁过期时间与自动续期机制
STW 可能导致任务执行时间过长,从而超出锁的过期时间。为了应对这种情况,可以采取以下措施:
1)合理设置锁的过期时间:确保锁的过期时间长于任务的预计执行时间。避免任务因为 STW 或其他延迟原因导致锁超时失效。
2)自动续期机制:使用支持自动续期的 Redis 客户端(如 Redisson)。当锁接近过期时,Redisson 会自动延长锁的过期时间,避免任务被中断。
举个例子:
RLock lock = redisson.getLock("myLock"); boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); // 锁最多有效30秒 if (locked) { try { // 执行任务,Redisson 会自动续期锁 executeLongRunningTask(); } finally { // 不需要额外的检查,Redisson 会自动检查是否由当前线程持有 lock.unlock(); // 只有当前线程持有锁时才能释放 } } else { // 锁获取失败,执行备用方案 }
2. 重试机制
在获取锁失败时,进行重试,直到成功获取锁。通过设置合适的重试间隔和最大重试次数,避免锁的竞争和过度请求。
通过重试机制,系统可以在一定的重试间隔内,反复尝试获取锁,从而减少由于 STW 引发的锁竞争问题。如下图:
举个例子:
1 RLock lock = redisson.getLock("myLock"); 2 3 RetryPolicy retryPolicy = new RetryPolicy() 4 .withMaxAttempts(5) // 最大重试次数 5 .withInitialDelay(2, TimeUnit.SECONDS) // 初始延迟时间为2秒 6 .withDelay(3, TimeUnit.SECONDS) // 每次重试之间的间隔为3秒 7 .withJitter(1,TimeUnit.SECONDS); // 允许的抖动范围0-1秒 8 9 // 创建带有自定义重试策略的锁 10 lock = redisson.getLock("myLock", retryPolicy); 11 12 try { 13 boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); 14 if (locked) { 15 // 执行任务 16 executeTask(); 17 } else { 18 System.out.println("获取锁失败,执行备用方案"); 19 } 20 } catch (InterruptedException e) { 21 Thread.currentThread().interrupt(); 22 }
3. 任务幂等性
确保任务具有幂等性,即使任务执行被中断或重试多次,也能保证最终结果一致。
参考链接:https://blog.huoding.com/2015/09/14/463
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)