Redis高并发分布式锁

目录

场景描述

订单扣减场景举例

代码调整1

代码调整2

代码调整3

redisson锁续命核心代码


场景描述

订单扣减场景举例
//首先在redis中set stock 300
@RequestMapping("/deduct_stock")
public String deductStock() {
       int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
        if (stock > 0) {
             int realStock = stock - 1;
             stringRedisTemplate.opsForValue().set("stock", realStock + ""); //jedis.set(key,value)
             System.out.println("扣减成功,剩余库存:" + realStock);
          } else {
             System.out.println("扣减失败,库存不足");
          }
        return "end";
    }

       以上场景肯定会出现并发问题,当有多个用户同时进行库存扣减的时候,可能在获取stock数量的时候获取到相同的值,有可能会出现此时只有一件库存,但是三个用户下单,出现库存超卖问题。

代码调整1
    @RequestMapping("/deduct_stock")
    public String deductStock() {
        synchronized (this){
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
            return "end";
        }
    }

       通过增加synchronize锁可以解决并发问题,但是只对单机有效。如果多服务器之间也会出现并发超卖问题。

代码调整2
@RequestMapping("/deduct_stock")
    public String deductStock() {

        String lockKey = "lock:product_101";
        //通过redis的setnx命令来模拟一把分布式锁,并设置超时时间,该指令是原子操作
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "101", 30, TimeUnit.SECONDS); //jedis.setnx(k,v)

        //如果设置失败,result会返回false.如果成功走正常扣减订单逻辑。
        if (!result) {
            return "error_code";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
            return "end";
        //如果业务代码中出现异常,也要保证锁的释放,也就是setnx的删除操作,避免死锁
        } finally {
            stringRedisTemplate.delete(lockKey);
        }

    }

        以上代码通过setnx的方式在并发量不高的时候,可能没有什么问题,如果并发量较高,某个线程获取到锁有执行业务代码超过了设置的超时时间,就会有并发问题发生了。假设线程1执行完成该方法用时15秒,执行到10秒的时候,因为超时时间将锁释放了,此时线程2获取到锁并执行业务逻辑,执行过程中,线程1执行完业务,并通过finally又释放了一次锁,可此时线程2不一定执行完。这种情况就会出现严重的并发问题。

代码调整3

       通过上述代码会发现,造成该问题的主要因素是超时时间的问题,为了解决该问题使用redisson锁续命来完善代码。

引入redisson依赖

        
			org.redisson
			redisson
			3.6.5
		

在启动类中配置

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public Redisson redisson() {
        // 单机模式
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }

}

订单接口调整

  @Autowired
  private Redisson redisson;  

  @RequestMapping("/deduct_stock")
    public String deductStock() {
        String lockKey = "lock:product_101";
        //获取锁对象
        RLock redissonLock = redisson.getLock(lockKey);
        //加分布式锁
        redissonLock.lock();  //  .setIfAbsent(lockKey, "101", 30, TimeUnit.SECONDS);
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            //解锁
            redissonLock.unlock();
        }
        return "end";
    }

通过使用redisson会自动每隔10秒检查是否还持有锁,如果持有锁就延长锁的时间,默认延长30秒。

      Redis高并发分布式锁_第1张图片

redisson锁续命核心代码
    private void scheduleExpirationRenewal(final long threadId) {
        if (!expirationRenewalMap.containsKey(this.getEntryName())) {
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    RFuture future = RedissonLock.this.commandExecutor.evalWriteAsync(
                            RedissonLock.this.getName(), 
                            LongCodec.INSTANCE, 
                            RedisCommands.EVAL_BOOLEAN, 
                            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) " +
                                    "then redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                    "return 1; end; return 0;",
                            Collections.singletonList(RedissonLock.this.getName()), 
                            new Object[]{RedissonLock.this.internalLockLeaseTime,
                                    RedissonLock.this.getLockName(threadId)});
                    future.addListener(new FutureListener() {
                        public void operationComplete(Future future) throws Exception {
                            RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
                            if (!future.isSuccess()) {
                                RedissonLock.log.error("Can't update lock " +
                                        RedissonLock.this.getName() + 
                                        " expiration", future.cause());
                            } else {
                                if ((Boolean) future.getNow()) {
                                    RedissonLock.this.scheduleExpirationRenewal(threadId);
                                }

                            }
                        }
                    });
                }
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            if (expirationRenewalMap.putIfAbsent(this.getEntryName(), task) != null) {
                task.cancel();
            }

        }
    }

你可能感兴趣的:(分布式中间件,redis,分布式,数据库,java,后端,缓存)