十三、利用分布式锁解决超卖问题
库存超卖问题
对于商城系统。超卖了一部分可以补获,12306对超卖问题更敏感。
JMeter的使用
超卖演示&使用JMeter对购票功能进行压测
使用synchronized是否能解决库存超卖?
超卖问题出现原因:
假设余票为1,此时多个线程同时查询到这条余票记录,并进行扣减,那么则会导致超卖发生。
public synchronized void doConfirm(ConfirmOrderDoReq req)
加锁会导致吞吐量/TPS变低,即:售卖效率不高。
一个节点可以加锁,1万个节点呢?还是无法解决超卖。(加节点可以解决性能问题)
存在问题
- 会导致售卖效率不高(可以容忍)
- 在多节点的情况下,还是会出现超卖(不能容忍)。
- 多个人抢多个车次也不应该受影响。
使用Redis分布式锁能否解决库存超卖?
解决超卖:所有节点都能得到的锁
可以通过数据库加锁,性能不高。
所以用redis加锁。
第一个线程进来setIfAbsent设置为true,此时其他线程不可入。
1 String key = req.getDate() + "-" + req.getTrainCode(); 2 Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(key, key, 3600, TimeUnit.SECONDS); 3 if (setIfAbsent) { 4 LOG.info("恭喜,抢到锁啦!"); 5 } else { 6 LOG.info("很遗憾,没有抢到锁"); 7 throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL); 8 }
key就用日期加车次号,value随意。没抢到有提示。
新的问题:
某一处出现异常会怎么样?必须过了超时时间才能进新的请求。
可以在方法结尾删除锁 redisTemplate.delete(lockKey);
只能解决正常流程问题,异常还有待处理。
将整个set锁以下的部分加异常处理。
1 Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 5, TimeUnit.SECONDS); 2 if (setIfAbsent) { 3 LOG.info("恭喜,抢到锁啦!"); 4 } else { 5 LOG.info("很遗憾,没有抢到锁"); 6 throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL); 7 }
如果这部分也加了锁,没抢到锁也会抛异常,会把别人的锁给删掉。
- 加锁的动作不要放在try里
- 加锁时,将当前线程ID放到锁对应的value中,删除时,先去获取value,比对value值和当前线程ID一致时才能删除。
使用Redisson看门狗解决超时问题
之前的方法就是setnx命令。
最关键的问题是有超时时间,如果项目很大,卡住线程,锁释放掉,还会引起超卖问题。
设置守护线程(看门狗),不断重置超时时间。
使用守护线程的好处是会随主线程的结束而结束,所以不会出现一直重置成60s,永不过期的问题。
Redis红锁是什么
看门狗锁还会遇到redis宕机问题。
第1个线程拿到锁,redis宕机。第二个线程进来也会拿到锁。
如果有A B C D E台redis(不是集群),第1个线程进来拿到A B C,拿到半数以上的节点,就认为拿到了锁。要求必须是奇数台redis。
RedissonRedLock。