比如说:

 有个生发洗发水100元,有个10元优惠券,每人限制领劵1张
隔壁老王,使用时间暂停来发现问题,并发领劵
A线程原先查询出来没有领劵,要再插入领劵记录前暂停
然后B线程原先查询出来也没有领劵,则插入领劵记录,然后A线程也插入领劵记录
老王就有了两个优惠券
问题来源核心:对资源的修改没有加锁,导致多个线程可以同时操作,从而导致数据不正确
解决问题:分布式锁 或者 细粒度分布式锁

解决问题思路:

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

等都可以

java优惠券过期实现 java优惠券防止并发多领_redis

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

解决方案之 踩坑一

基于redis实现分布式锁
◦加锁 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()
   }
 }
 存在的坑是?????

--------------------------华丽分割线---------------踩坑中-------------------------

多个命令之间不是原子性操作,如setnx和expire之间,如果setnx成功,但是expire失败,且宕机了,则这个资源就是死锁
使用原子命令:设置和配置过期时间  setnx / setex

如: set key 1 ex 30 nx
 java里面 redisTemplate.opsForValue().setIfAbsent("seckill_1",1,30,TimeUnit.MILLISECONDS)

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

java优惠券过期实现 java优惠券防止并发多领_java_02

可以在 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的值
◾核心还是判断和删除命令 不是原子性操作导致
核心是保证多个指令原子性,加锁使用setnx setex 可以保证原子性,那解锁使用 判断和删除怎么保证原子性
◦多个命令的原子性:采用 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);
  采用lua脚本的删除锁方式--------------------------------------代码如下---------------------------------/**
 * 原生分布式锁 开始
 * 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分钟时间

-------------------------------上述实现lua+redis靠谱,但是太过繁琐------------------正解如下--------------------------------
使用Redisson,pom.xml
 <dependency>
       <groupId>org.redisson</groupId>
       <artifactId>redisson</artifactId>
       <version>3.10.1</version>
 </dependency>创建redisson客户端
@Configuration
 @Data
 public class AppConfig {    @Value("${spring.redis.host}")
     private String redisHost;    @Value("${spring.redis.port}")
     private String redisPort;     @Value("${spring.redis.password}")
     private String redisPwd;     /**
      * 配置分布式锁的redisson
      * @return
      */
     @Bean
     public RedissonClient redissonClient(){
         Config config = new Config();        //单机方式
         config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);        //集群
         //config.useClusterServers().addNodeAddress("redis://192.31.21.1:6379","redis://192.31.21.2:6379")        RedissonClient redissonClient = Redisson.create(config);
         return redissonClient;
     }}
  ---------------------使用如下----------------
       String lockKey = "lock:coupon:"+couponId;
         RLock rLock = redissonClient.getLock(lockKey);        //多个线程进入,会阻塞等待释放锁,默认30秒,然后有watch dog自动续期
         rLock.lock();        //加锁10秒钟过期,没有watch dog功能,无法自动续期
         //rLock.lock(10,TimeUnit.SECONDS);        log.info("领劵接口加锁成功:{}",Thread.currentThread().getId());
         
         try {
          //这是业务逻辑咯
             }
 
         }finally {
                rLock.unlock();
         }