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

posted @   欢乐豆123  阅读(145)  评论(0编辑  收藏  举报
编辑推荐:
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示