分布式锁防止个人超领优惠券

第十五章 原生分布式锁-Redisson分布式锁防止个人超领优惠券

一行代码10个bug-单用户超领优惠券问题介绍

简介:讲解单用户优惠券超领业务问题和效果演示

  • 什么单用户超领优惠券

    • 优惠券限制1人限制1张,有些人却领了2张
    • 优惠券限制1人限制2张,有些人却领了3或者4张
  • 案例举例和问题来源

前面解决了,优惠券超发的问题,但是这个个人领取的时候,有张数限制,

有个生发洗发水100元,有个10元优惠券,每人限制领劵1张

小滴课堂-老王,使用时间暂停思维来发现问题,并发领劵

A线程原先查询出来没有领劵,要再插入领劵记录前暂停
然后B线程原先查询出来也没有领劵,则插入领劵记录,然后A线程也插入领劵记录
老王就有了两个优惠券

问题来源核心:对资源的修改没有加锁,导致多个线程可以同时操作,从而导致数据不正确

解决问题:分布式锁 或者 细粒度分布式锁
第2集 分布式核心技术-关于高并发下分布式锁你知道多少?

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

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

在这里插入图片描述

  • 设计分布式锁应该考虑的东西
    • 排他性
      • 在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
    • 容错性
      • 分布式锁一定能得到释放,比如客户端奔溃或者网络中断
    • 满足可重入、高性能、高可用
    • 注意分布式锁的开销、锁粒度
基于Redis实现分布式锁的几种坑你是否踩过《上》

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

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

    • 分布式锁离不开 key - value 设置
    key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种商品的秒杀活动加锁,key 命名为 “seckill_商品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()
      }
    }
    
    • 存在哪些问题,大家自行思考下
第4集 基于Redis实现分布式锁的几种坑你是否踩过《下》

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

    • 存在什么问题?

      • 多个命令之间不是原子性操作,如setnxexpire之间,如果setnx成功,但是expire失败,且宕机了,则这个资源就是死锁
      使用原子命令:设置和配置过期时间  setnx / setex
      如: set key 1 ex 30 nx
      java里面 redisTemplate.opsForValue().setIfAbsent("seckill_1",1,30,TimeUnit.MILLISECONDS)
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LrnpPXLL-1665481082566)(img/image-20210209105010981.png)]

      • 业务超时,存在其他线程勿删,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的值
        • 核心还是判断和删除命令 不是原子性操作导致
      • 那如何解决呢?下集讲解

手把手教你彻底掌握分布式锁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、原子解锁:需要判断是不是自己的锁
    */
    String uuid = CommonUtil.generateUUID();
    String lockKey = "lock:coupon:"+couponId;
    Boolean nativeLock=redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,Duration.ofSeconds(30));
        if(nativeLock){
          //加锁成功
          log.info("加锁:{}",nativeLock);
          try {
               //执行业务  TODO
            }finally {
               String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
    
                    Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);
                    log.info("解锁:{}",result);
                }
    
            }else {
                //加锁失败,睡眠100毫秒,自旋重试
                try {
                    TimeUnit.MILLISECONDS.sleep(100L);
                } catch (InterruptedException e) { }
                return addCoupon( couponId, couponCategory);
            }
            //原生分布式锁 结束
    
    • 遗留一个问题,锁的过期时间,如何实现锁的自动续期 或者 避免业务执行时间过长,锁过期了?
      • 原生方式的话,一般把锁的过期时间设置久一点,比如10分钟时间
第6集 基于Redis官方推荐-分布式锁最佳实践介绍

简介:redis官方推荐-分布式锁最佳实践

  • 原生代码+redis实现分布式锁使用比较复杂,且有些锁续期问题更难处理

    • 官方推荐方式:https://redis.io/topics/distlock
    • 多种实现客户端框架

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zt88LrLY-1665481154540)(…/…/…/Library/Application Support/typora-user-images/image-20210209234838064.png)]

    • Redisson官方中文文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
  • 聚合工程锁定版本,common项目添加依赖(多个服务都会用到分布式锁)

<!--分布式锁-->
<dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.10.1</version>
</dependency>

  • 创建redisson客户端
    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private String redisPort;

    @Value("${spring.redis.password}")
    private String redisPwd;
    
		/**
     * 配置分布式锁
     * @return
     */
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();

        //单机模式
        //config.useSingleServer().setPassword("123456").setAddress("redis://8.129.113.233:3308");
        config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);

        //集群模式
        //config.useClusterServers()
        //.setScanInterval(2000)
        //.addNodeAddress("redis://10.0.29.30:6379", "redis://10.0.29.95:6379")
        // .addNodeAddress("redis://127.0.0.1:6379");

        RedissonClient redisson = Redisson.create(config);

        return redisson;
    }

  • 模拟controller接口测试
实战Redisson实现优惠券微服务领劵接口的分布式锁

简介:redisson实现优惠券微服务领劵接口的分布式锁

  • 优惠券微服务,分布式锁实现方式
Lock lock = redisson.getLock("lock:coupon:"+couponId);
//阻塞式等待,一个线程获取锁后,其他线程只能等待,和原生的方式循环调用不一样
lock.lock();
        try {
            CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>().eq("id", couponId)
                    .eq("category", couponCategory)
                    .eq("publish", CouponPublishEnum.PUBLISH));

            this.couponCheck(couponDO,loginUser.getId());

            CouponRecordDO couponRecordDO = new CouponRecordDO();
            BeanUtils.copyProperties(couponDO,couponRecordDO);
            couponRecordDO.setCreateTime(new Date());
            couponRecordDO.setUseState(CouponStateEnum.NEW.name());
            couponRecordDO.setUserId(loginUser.getId());
            couponRecordDO.setUserName(loginUser.getName());
            couponRecordDO.setCouponId(couponId);
            couponRecordDO.setId(null);
            //高并发下扣减劵库存,采用乐观锁,当前stock做版本号,一次只能领取1张
            int rows = couponMapper.reduceStock(couponId);

            if(rows == 1){
                //库存扣减成功才保存
                couponRecordMapper.insert(couponRecordDO);
            }else {
                log.warn("发放优惠券失败:{},用户:{}",couponDO,loginUser);
                throw new BizException(BizCodeEnum.COUPON_NO_STOCK);
            }

        }finally {
            lock.unlock();
        }
Redisson是怎样解决分布式锁的里面的坑

简介:redisson解决分布式锁里面的坑

  • 问题 : Redis锁的过期时间小于业务的执行时间该如何续期?

    • watch dog看门狗机制
    负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。或者业务执行时间过长导致锁过期,
    
    为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。
    
    Redisson中客户端一旦加锁成功,就会启动一个watch dog看门狗。watch dog是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间
    
    
    默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定
    
    • 指定加锁时间
    // 加锁以后10秒钟自动解锁
    // 无需调用unlock方法手动解锁
    lock.lock(10, TimeUnit.SECONDS);
    
    // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
    boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
    
    if (res) {
       try {
         ...
       } finally {
           lock.unlock();
       }
    }
    
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ITzhongzi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值