第七章 高级篇分布式锁之Redis6+Lua脚本实现原生分布式锁

Posted 老吴IT代码笔记*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第七章 高级篇分布式锁之Redis6+Lua脚本实现原生分布式锁相关的知识,希望对你有一定的参考价值。

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

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

  • 背景

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

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

    • 核心

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

       

  • 避免共享资源并发操作导致数据问题

    • 加锁

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

image-20210410164720650

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

    • 排他性

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

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

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

 

 

 

 

 

 

 

 

 

 

 

第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)
     

    image-20210410165806304

    • 业务超时,存在其他线程勿删,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实现分布式锁使用比较复杂,且有些锁续期问题更难处理

 

以上是关于第七章 高级篇分布式锁之Redis6+Lua脚本实现原生分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

多级缓存架构 | 黑马Redis高级篇

Lua从青铜到王者基础篇第七篇:Lua数组和迭代器

漫谈分布式锁之ZooKeeper实现

c++ 与 lua socket脚本高级调用

REDIS6_分布式存储极致性能目录

REDIS6_分布式存储极致性能目录