本文代码对应的github地址:https://github.com/nieandsun/redis-study
本文整理自图灵学院诸葛老师公开课!!!
文章目录
1 超卖现象简单介绍
有如下代码:
@RestController
public class RedisLockController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/deduct-stock1")
public String DeductStock() {
synchronized (this) {
//查看数据库中是否有库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//如果有库存则购买一个商品
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
//如果没有库存,则扣减失败
System.out.println("扣减失败,库存不足");
}
}
return "ok!!!";
}
}
相信大家都可以看出来,如若不是分布式部署的话,上面的代码其实没有问题的。
但是如果在分布式部署的情况下,就不保证是否有问题了,下图应该是一个最简单的分布式部署场景了:
以此图为例,由于tomcat1和tomcat2在两个JVM进程里,而无论是synchronized 关键字还是JUC包里的Lock锁,都无法保证不同JVM内共用一个锁对象,因此上面的代码在高并发场景下是非常容易出现超卖问题的。
这时候就不得不使用分布式锁。
2 使用redis实现分布式锁的最基本原理
最基本的原理为:
redis有一个SETNX命令,该命令的功能如下:
SETNX key value,将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
这样的话,tomcat1 、tomcat2。。。甚至tomcatN就可以以谁最先执行了SETNX命令来作为有没有抢到锁的依据了 — 这其实就是能够使用redis做分布式锁的最基本原理。
由此我们应该把1中的代码改写成下面的样子:
@RestController
public class RedisLockController2 {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/deduct-stock2")
public String DeductStock() {
String LOCK_KEY = "deduct-stock-lock";
String LOCK_VALUE = "deduct-stock-value";
//尝试加锁
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_KEY, LOCK_VALUE);
//加锁失败
if (!flag) {
System.out.println("秒杀失败,请重试!!!");
}
//加锁成功
if (flag) {
//查看数据库中是否有库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//如果有库存则购买一个商品
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
//如果没有库存,则扣减失败
System.out.println("扣减失败,库存不足");
}
}
//释放锁
Boolean delete = stringRedisTemplate.delete(LOCK_KEY);
System.out.println("删除" + LOCK_KEY + ":" + delete); //打印日志
return "ok!!!";
}
}
相信很多人都可以看出来上面的代码其实有很多的问题,但是我觉得
这 应该算是使用redis实现分布式锁的最核心的原理了。
3 使用redis实现分布式锁的问题
3.1 使用redis实现分部式锁必须要设置KEY的失效时间
对redis实现分布式锁必须要设置KEY的失效时间的分析如下:
3.2 使用redis实现分部式锁必须自己线程加的锁自己释放
+ 必须对失效时间进行延长处理
3.2.1 自己线程加的锁必须自己释放
+ 必须对失效时间进行延长处理
原因分析
以3.1的代码为例,如果自己线程加的锁不是自己释放,将会发生下面的问题:
最坏的情况分布式锁将完全失效
,即某线程刚加上锁,就被前面的线程给释放掉了,因此这样显然是不可行的。
因此还必须要解决两个问题:
- 自己线程加的锁只能有自己释放
- 必须对失效时间进行延长处理
首先我们应该想明白的是,这个失效时间到底设置多大合适??? —》 如果按照3.1的代码而言,其实设置多大都不合适!!!
因为生产环境下,分布式锁包围的代码,我们是很难确切的知晓其具体执行时间的,这就面临这样一个两难的问题:
- 设置太长了,会影响并发效率,给用户造成不好的使用体验,
- 设置太短了,有可能会致使锁失效,从而发生超卖!!!
3.2.2 解决问题的思路
该问题比较好的一个解决方式如下:
(1)前提是自己线程加的锁只能自己释放
(2)先按照开发环境对这段代码的压测结果,设置一个合理的时间
(3)当某线程抢到锁进入这块代码后,开启一个新的线程,每隔一段时间专门来检查该线程是否还持有锁,如若持有,将该线程持有锁的时间进行适当的延长 —> 其实就是把KEY的时间进行适当延长。
大致原理如下:
3.2.3 具体解决方式 — Redisson的分布式锁解决方案
Redission官网:https://redisson.org/
Redission给出了3.2.2中的具体实现,这里简单介绍一下springboot集成Redission的方式(单机版):
(1)添加jar包
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.0</version>
</dependency>
(2)单机模式下自动装配Redisson客户端
具体的可以clone下来本篇文章对应的源码进行查看
@Autowired
private RedisProperties redisProperties;
@Bean
public Redisson redisson(){
Config config = new Config();
String redisUrl = String.format("redis://%s:%s",redisProperties.getHost()+"",redisProperties.getPort()+"");
config.useSingleServer().setAddress(redisUrl).setPassword(redisProperties.getPassword());
config.useSingleServer().setDatabase(0);
return (Redisson) Redisson.create(config);
}
(3)使用Redission后的扣减库存代码
@RestController
public class RedisLockController3 {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private Redisson redisson;
@GetMapping("/deduct-stock3")
public String DeductStock() {
String LOCK_KEY = "deduct-stock-lock";
RLock redissonLock = redisson.getLock(LOCK_KEY);
//加锁成功
try {
redissonLock.lock();
//查看数据库中是否有库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//如果有库存则购买一个商品
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
//如果没有库存,则扣减失败
System.out.println("扣减失败,库存不足");
}
} finally {
redissonLock.unlock();
System.out.println("成功释放锁"); //打印日志
}
return "ok!!!";
}
}
3.3 其他问题
问题1如下:
解决方案:
像ZK一样,当redis的多数节点都同步到数据后才表示抢锁成功,Redission的实现为RedLock —》 有兴趣的自己研究吧!!!
问题2: 效率问题
锁的目的是让线程同步执行 —》 即一个个的执行,它与多线程高并发其实就是背道而驰的。
解决方案:
可以考虑将数据存放于不同的节点或槽
,然后读取数据时按槽进行读取 —》 类似于ConcurrentHashMap的设计理念!!!
end!!!