首先就我们自身的秒杀系统,并发稍微上去一点,就偶尔会发生超卖的问题,所以针对相关资料和视频,整理出一个优化方案。 此次分几步,层层递进,一步一步完善我们秒杀下的分布式锁
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);
}
- 流程时间把控
目前已经支持相对简单的,支持小并发的秒杀扣减库存的场景了。
下面我们要考虑流程把控的问题,前提我们的锁失效时间设置为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);
}
}
这样我们就可以解决高并发场景下带来的一些超卖问题。记一下笔记方便后面回顾
后续还有一些问题等我操作过后再提出~
来源于图灵公开课——诸葛老师