articleList

07-【高级篇】分布式锁之 Redis6+Lua 脚本实现原生分布式锁

2025/03/16 posted in  Redis
Tags: 

第 1 集 分布式核心技术-关于高并发下分布式锁你知道多少?

简介:分布式锁核心知识介绍和注意事项

  • 背景

    • 就是保证同一时间只有一个客户端可以对共享资源进行操作

    • 案例:优惠券领劵限制张数、商品库存超卖

    • 核心

      • 为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度
      • 利用互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
  • 避免共享资源并发操作导致数据问题

    • 加锁

      • 本地锁:synchronize、lock 等,锁在当前进程内,集群部署下依旧存在问题
      • 分布式锁:redis、zookeeper 等实现,虽然还是锁,但是多个进程共用的锁标记,可以用 Redis、Zookeeper、Mysql 等都可以

  • 设计分布式锁应该考虑的东西

    • 排他性

      • 在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
    • 容错性

      • 分布式锁一定能得到释放,比如客户端奔溃或者网络中断
    • 满足可重入、高性能、高可用

    • 注意分布式锁的开销、锁粒度

第 2 集 基于 Redis 实现分布式锁的几种坑你是否踩过《上》

简介:基于 Redis 实现分布式锁的几种坑

  • 实现分布式锁 可以用 Redis、Zookeeper、Mysql 数据库这几种 , 性能最好的是 Redis 且是最容易理解

    • 分布式锁离不开 key - value 设置

      key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种优惠券活动加锁,key 命名为 “coupon:id” 。value就可以使用固定值,比如设置成1
      
  • 基于 redis 实现分布式锁,文档:http://www.redis.cn/commands.html#string

    • 加锁 SETNX key value

      setnx 的含义就是 SET if Not Exists,有两个参数 setnx(key, value),该方法是原子性操作
      ​
      如果 key 不存在,则设置当前 key 成功,返回 1;
      ​
      如果当前 key 已经存在,则设置当前 key 失败,返回 0
      
    • 解锁 del (key)

      得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用 del(key)
      
    • 配置锁超时 expire (key,30s)

      客户端奔溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放
      
    • 综合伪代码

      methodA(){
        String key = "coupon_66"
      ​
        if(setnx(key,1) == 1){
            expire(key,30,TimeUnit.MILLISECONDS)
            try {
                //做对应的业务逻辑
                //查询用户是否已经领券
                //如果没有则扣减库存
                //新增领劵记录
            } finally {
                del(key)
            }
        }else{
      ​
          //睡眠100毫秒,然后自旋调用本方法
          methodA()
        }
      }
      
    • 存在哪些问题,大家自行思考下

第 3 集 基于 Redis 实现分布式锁的几种坑你是否踩过《下》

简介:手把手教你彻底掌握分布式锁+原生代码编写

  • 存在什么问题?

    • 多个命令之间不是原子性操作,如setnxexpire之间,如果setnx成功,但是expire失败,且宕机了,则这个资源就是死锁

      使用原子命令:设置和配置过期时间  setnx / setex
      如: set key 1 ex 30 nx
      java里面 redisTemplate.opsForValue().setIfAbsent("seckill_1","success",30,TimeUnit.MILLISECONDS)
      

    • 业务超时,存在其他线程勿删,key 30 秒过期,假如线程 A 执行很慢超过 30 秒,则 key 就被释放了,其他线程 B 就得到了锁,这个时候线程 A 执行完成,而 B 还没执行完成,结果就是线程 A 删除了线程 B 加的锁

      可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁, 那 value 应该是存当前线程的标识或者uuid
      ​
      String key = "coupon_66"
      String value = Thread.currentThread().getId()
      ​
      if(setnx(key,value) == 1){
          expire(key,30,TimeUnit.MILLISECONDS)
          try {
              //做对应的业务逻辑
          } finally {
            //删除锁,判断是否是当前线程加的
            if(get(key).equals(value)){
                //还存在时间间隔
                del(key)
              }
          }
      }else{
      
        //睡眠100毫秒,然后自旋调用本方法
      ​
      }
      
    • 进一步细化误删

      • 当线程 A 获取到正常值时,返回带代码中判断期间锁过期了,线程 B 刚好重新设置了新值,线程 A 那边有判断 value 是自己的标识,然后调用 del 方法,结果就是删除了新设置的线程 B 的值
      • 核心还是判断和删除命令 不是原子性操作导致
    • 总结

      • 加锁+配置过期时间:保证原子性操作
      • 解锁: 防止误删除、也要保证原子性操作
    • 那如何解决呢?下集讲解

第 4 集 手把手教你彻底掌握分布式锁 lua 脚本+redis 原生代码编写

简介:手把手教你彻底掌握分布式锁+原生代码编写

  • 前面说了 redis 做分布式锁存在的问题

    • 核心是保证多个指令原子性,加锁使用 setnx setex 可以保证原子性,那解锁使用 判断和删除怎么保证原子性

    • 文档:http://www.redis.cn/commands/set.html

    • 多个命令的原子性:采用 lua 脚本+redis, 由于【判断和删除】是 lua 脚本执行,所以要么全成功,要么全失败

      //获取lock的值和传递的值一样,调用删除操作返回1,否则返回0
      ​
      String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
      ​
      //Arrays.asList(lockKey)是key列表,uuid是参数
      Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);
      
    • 全部代码

      /**
      * 原生分布式锁 开始
      * 1、原子加锁 设置过期时间,防止宕机死锁
      * 2、原子解锁:需要判断是不是自己的锁
      */
      @RestController
      @RequestMapping("/api/v1/coupon")
      public class CouponController {
      
          @Autowired
          private StringRedisTemplate redisTemplate;
      ​
          @GetMapping("add")
          public JsonData saveCoupon(@RequestParam(value = "coupon_id",required = true) int couponId){
              //防止其他线程误删
              String uuid = UUID.randomUUID().toString();
      ​
              String lockKey = "lock:coupon:"+couponId;
      ​
              lock(couponId,uuid,lockKey);
      ​
              return JsonData.buildSuccess();
          }
      
      ​
          private void lock(int couponId,String uuid,String lockKey){
              //lua脚本
              String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
      ​
              Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,Duration.ofSeconds(30));
              System.out.println(uuid+"加锁状态:"+nativeLock);
              if(nativeLock){
                  //加锁成功
                  try{
                      //TODO 做相关业务逻辑
                      TimeUnit.SECONDS.sleep(10L);
                  } catch (InterruptedException e) {
      ​
                  } finally {
                      //解锁
                      Long result = redisTemplate.execute( new DefaultRedisScript<>(script,Long.class),Arrays.asList(lockKey),uuid);
                      System.out.println("解锁状态:"+result);
                  }
      ​
              }else {
                  //自旋操作
                  try {
                      System.out.println("加锁失败,睡眠5秒 进行自旋");
                      TimeUnit.MILLISECONDS.sleep(5000);
                  } catch (InterruptedException e) { }
                  //睡眠一会再尝试获取锁
                  lock(couponId,uuid,lockKey);
              }
          }
      ​
      }
      
    • 遗留一个问题,锁的过期时间,如何实现锁的自动续期 或者 避免业务执行时间过长,锁过期了?

      • 原生方式的话,一般把锁的过期时间设置久一点,比如 10 分钟时间
    • 原生代码+redis 实现分布式锁使用比较复杂,且有些锁续期问题更难处理