秒杀场景下的分布式锁笔记——2020.08.26

236 阅读4分钟

首先就我们自身的秒杀系统,并发稍微上去一点,就偶尔会发生超卖的问题,所以针对相关资料和视频,整理出一个优化方案。 此次分几步,层层递进,一步一步完善我们秒杀下的分布式锁

1.reids指令 —— SETNX

  • reids命令 将Key的值设为value,当且仅当key不存在 若给定的key已经存在,则SETNX不做任何动作。 SETNX是 set if not exists (如果不存在, 则set)的简写。 那么spring的项目中jedis的代码为:
 redisTemplate.opsForValue().setIfAbsent("key","value");

我们现在知道了设置分布式锁的基本代码,可以加一点我们的业务代码逻辑了

//**锁的key:自然是商品id了
String lockKey = "goods_01";

//**如果set成功 返回true 反之返回false
Bollean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"Guccisue");

//**如果没有设值成功 可以认为没有竞争到锁 则返回错误信息
if(!result){
	return "error_code";
}

//业务逻辑-扣减库存
....

//在方法的最后删除该商品的锁,也就是把锁释放
redisTemplate.delete(lockKey);

如此看来,简单的完成了 使用SETNX来实现分布式锁的功能
因为是第一步,还是有很多地方需要优化的,我们一步一步看

  • 先处理一个小问题,假如在释放锁之前(执行业务逻辑代码的时候)发生了异常,会导致什么情况
    (1)当前线程会挂掉。如果你对于业务代码上做了try,catch,可能会好一点
    (2)导致当前商品id的锁无法清除
    (3)后续进程再进来的时候,发现该商品id的锁依然存在,全部return
    所以我们可以在业务代码这里增加try finally,使之无论如何都会删除当前的锁

再进一步,如果程序跑在业务代码的时候宕机了,情况又不一样了
那我们可以考虑给key(锁)增加一个失效时间,代码如下

Bollean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"Guccisue");

redisTemplate.expire(lockKey,10,TimeUnit.SECONDS);

这样又会有一个问题出现:我们现在模拟的是高并发下的秒杀场景,所以有可能在加锁的时候程序挂掉
那么增加失效时间的操作就不会做,则又会出现锁一直存在的情况了

那么我们为了保证原子性,可以将加锁和设置失效时间用同一条语句处理,如下:

Bollean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"Guccisue",10,TimeUnit.SECONDS);

所以我们最终的代码就是:

//**锁的key:自然是商品id了
 String lockKey = "goods_01";
 
 //**如果set成功 返回true 反之返回false
Bollean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"Guccisue",10,TimeUnit.SECONDS);
 
 //**如果没有设值成功 可以认为没有竞争到锁 则返回错误信息
 if(!result){
 	return "error_code";
 }
   try{
     //业务逻辑-扣减库存
     ....
   }finally {
     //在方法的最后删除该商品的锁,也就是把锁释放
     redisTemplate.delete(lockKey);
   }
  1. 流程时间把控

目前已经支持相对简单的,支持小并发的秒杀扣减库存的场景了。
下面我们要考虑流程把控的问题,前提我们的锁失效时间设置为10秒(实际不会如此之长),出问题的流程如下:

1.高并发下模拟流程(结合以上代码查看):

线程1线程2总时长
获取锁(为该商品设值)等待0
执行了10秒
(锁到达失效时间,锁失效)
开始并竞争到锁10
继续执行5秒继续执行5秒15
线程执行到最后,删除锁正在执行,同时该商品锁被删除
其他线程可以继续竞争
15

至此大家应该也模拟出了这个情况,简单来说就是线程2 的锁被线程1 删除了,
严重的说就是——这个锁永久失效

有的人会说可以把锁失效时间进行增加,但是如果这个时间增加到足够高了
系统的用户体验就会很差,总不能所有人都等你个1分多钟吧,所以基于这个问题,

做法:我们可以给锁中的value值赋值为该线程的id

所谓解铃还须系铃人,谁上的锁,就要由谁释放掉。最简单的,当前线程我们给他随机一个UUID,
那么代码实例如下:

 String clientId = UUID.randomUUID().toString();
 String lockKey = "goods_01";
 
//将该锁的值赋值为线程id
Bollean result = redisTemplate.opsForValue().setIfAbsent(lockKey,client,10,TimeUnit.SECONDS);
  if(!result){
      return "error_code";
   }
   try{
     //业务逻辑-扣减库存
     ....
   }finally {
     //如果线程ID相同的情况下,才允许删除该锁
     if(clientId.equals(redisTemplate.opsForValue().get(lockKey))){
     	redisTemplate.delete(lockKey);
     }
   }

这样我们就可以解决高并发场景下带来的一些超卖问题。记一下笔记方便后面回顾
后续还有一些问题等我操作过后再提出~

来源于图灵公开课——诸葛老师