Redis实现分布式锁的核心:
redis命令:setnx key value
SETNX key value
可用版本: >= 1.0.0
时间复杂度: O(1)
只在键 key 不存在的情况下, 将键 key 的值设置为 value 。
若键 key 已经存在, 则 SETNX 命令不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
返回值
命令在设置成功时返回 1 , 设置失败时返回 0 。
代码示例
redis> EXISTS job # job 不存在
(integer) 0
redis> SETNX job "programmer" # job 设置成功
(integer) 1
redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败
(integer) 0
redis> GET job # 没有被覆盖
"programmer"
使用场景:
单机场景下redis预减库业务在高并发场景下会出现超卖问题:
代码逻辑:redis查询库存 → redis减库存
上述操作不是原子性的,当多个线程同时执行该方法时,会出现超卖问题:线程A查询库存但是还没减库存这一个时间差内,线程B来查询了库存,假设库存有50个,线程A查询了50,同时线程B查询了50,那么线程A减库存,库存剩下49,而线程B也减库存,库存也剩下49,这就造成了超卖问题,卖给了两个人,结果数据库就减了一份
@RequestMapping("/redis")
public class RedisDemoController {
@Autowired
private RedisTemplate<String,Integer> redisTemplate;
@RequestMapping("/lock")
public String deductStock(){
int stock = redisTemplate.opsForValue().get("stock");
if(stock < 0){
int remainStock = stock - 1;
redisTemplate.opsForValue().set("stock",remainStock);
System.out.println("扣减成功,剩余库存:"+remainStock);
}else{
System.out.println("扣减失败,库存不足");
}
return "index";
}
}
怎么解决?
用synchronized代码段把redis查库存到减库存这一段逻辑锁起来,让其执行起来是原子性的
@RequestMapping("/redis")
public class RedisDemoController {
@Autowired
private RedisTemplate<String,Integer> redisTemplate;
@RequestMapping("/lock")
public String deductStock(){
synchronized (this){//加上synchronized锁
int stock = redisTemplate.opsForValue().get("stock");
if(stock < 0){
int remainStock = stock - 1;
redisTemplate.opsForValue().set("stock",remainStock);
System.out.println("扣减成功,剩余库存:"+remainStock);
}else{
System.out.println("扣减失败,库存不足");
}
}
return "index";
}
}
上面的场景是单机场景下,那假如是分布式场景呢?分布式场景就出现问题了,synchronized是JVM级别的锁,假如是分布式场景下,synchronized就不起作用了,虽然在单机上不会出现线程问题,但是即便用了synchronized,相对于整个集群来说,也是线程不安全的,可以看下面这张图,即便Tomcat 1和2都部署了单机场景下线程安全的项目,假如有两个请求,一个是A,另一个是B,然后请求A被Nginx分发到Tomcat 1,请求B被Nginx分发到Tomcat 2 上,然后两个请求同时执行上面的这段代码,那么同样会出现超卖的问题,synchronized是JVM级别的,而锁不了整个集群
分布式下的秒杀项目(redis预减库存逻辑)
假设场景:秒杀项目分部在多个tomcat运行,如果恰好有两个请求同时落在redis预减库存逻辑上,那么就会出现超卖问题,比如商品有50个,线程A查询有50个,买了一个,然后库存就剩下49个,恰好线程B也查到有50个,也买了一个,结果库存还是49个,这就造成了超卖问题。
解决上述问题还是离不开加锁,给谁加锁?给redis加锁,这个时候就要用到setnx命令了
redis是单线程模型,并发请求落到redis上也只能是一个一个执行,假设线程A给redis上了锁setnx,key为商品id,那么当其他线程落到访问redis时,同样执行setnx操作,但是线程A已经给redis上了锁,setnx在key存在时的情况会执行失败,所以其他线程只能等待线程A释放锁
@RequestMapping("/lock3")
public String RedisDistributeLock(User user){
String lockKey = "product_001";
//分布式锁,setIfAbsent是对setnx的封装,setnx成功返回true,否则返回false
Boolean LockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, user.getId());
if(!LockSuccess){
return "error";
}
int stock = redisTemplate.opsForValue().get("stock");
if(stock < 0){
int remainStock = stock - 1;
redisTemplate.opsForValue().set("stock",remainStock);
System.out.println("扣减成功,剩余库存:"+remainStock);
}else{
System.out.println("扣减失败,库存不足");
}
//释放分布式锁:把lockKey删掉
redisTemplate.delete(lockKey);
return "index";
}
用了redis的setnx就万无一失了吗?
有加锁就必须有解锁(删除setnx种下的key),假如线程A加锁成功后,还没释放锁,结果抛异常了,就没办法释放锁了,那么线程A后面的所有线程都无法执行该逻辑
那怎么应对这个场景?
上述场景是由于发生异常造成没有执行释放锁,那么可以用try-finally机制,保证即使中间发生了异常,最后也能够执行释放锁这一步
@RequestMapping("/lock4")
public String RedisDistributeLock01(User user){
String lockKey = "product_001";
//分布式锁,setIfAbsent是对setnx的封装,setnx成功返回true,否则返回false
try{
Boolean LockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, user.getId());
if(!LockSuccess){
return "error";
}
int stock = redisTemplate.opsForValue().get("stock");
if(stock < 0){
int remainStock = stock - 1;
redisTemplate.opsForValue().set("stock",remainStock);
System.out.println("扣减成功,剩余库存:"+remainStock);
}else{
System.out.println("扣减失败,库存不足");
}
}finally {
//释放分布式锁:把lockKey删掉
redisTemplate.delete(lockKey);
}
return "index";
}
假如执行过程中是web程序挂了,而不是出现异常呢?在执行try代码块的时候程序挂了是无法执行finally代码块的,那该怎么办?
redis有个key过期机制,给lockKey设置一个过期值,若干秒之后lockKey会失效,只要redis不挂,那么都能正常释放锁
但即使设置了过期时间也仍然会存在风险:
加锁和设置过期时间为两步操作,并非原子操作,有可能加了锁之后,程序就崩溃了,没有执行到设置过期时间的这一步,那么同样会发生错误
@RequestMapping("/lock4")
public String RedisDistributeLock01(User user){
String lockKey = "product_001";
try{
//分布式锁,setIfAbsent是对setnx的封装,setnx成功返回true,否则返回false
Boolean LockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, user.getId());
//锁加完但是还没设置过期时间就宕机了,怎么办
redisTemplate.expire(lockKey,10, TimeUnit.SECONDS);//设置过期时间
if(!LockSuccess){
return "error";
}
int stock = redisTemplate.opsForValue().get("stock");
if(stock < 0){
int remainStock = stock - 1;
redisTemplate.opsForValue().set("stock",remainStock);
System.out.println("扣减成功,剩余库存:"+remainStock);
}else{
System.out.println("扣减失败,库存不足");
}
}finally {
//释放分布式锁:把lockKey删掉
redisTemplate.delete(lockKey);
}
return "index";
}
怎么解决?
使用原子性的api
@RequestMapping("/lock4")
public String RedisDistributeLock01(User user){
String lockKey = "product_001";
try{
//分布式锁,setIfAbsent是对setnx的封装,setnx成功返回true,否则返回false
//设置锁的同时设置过期时间,原子性的api
Boolean LockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, user.getId(), 10, TimeUnit.SECONDS);
if(!LockSuccess){
return "error";
}
int stock = redisTemplate.opsForValue().get("stock");
if(stock < 0){
int remainStock = stock - 1;
redisTemplate.opsForValue().set("stock",remainStock);
System.out.println("扣减成功,剩余库存:"+remainStock);
}else{
System.out.println("扣减失败,库存不足");
}
}finally {
//释放分布式锁:把lockKey删掉
redisTemplate.delete(lockKey);
}
return "index";
}
那用原子性的操作在加锁的同时设置过期时间,是不是就真的没有问题了?
这里面还涉及到删除了不属于自己的锁的情况
场景:线程A执行业务在某些特殊情况下需要可能需要15s,但是业务代码默认设置锁过期时间为10s,线程A执行到第10s的时候,锁过期了,这时候线程B来了,线程B可能要执行8s,当线程B执行到第5s的时候,线程A里面的业务执行到15s,删除了锁,但此时线程B还没有执行完,锁就没有了,以至于后面可能还有线程C发现没有上锁,加锁后又被线程B把自己加的锁删了,出现了线程删除不是自己加的锁的情况,这把锁就会永久失效
怎么解决线程误删锁的情况?
上面的问题的本质是线程删除了一个其他线程加的锁,因为线程没有判断该锁是不是自己加的,这里的是解决方案的切入点,当一个线程加锁时,setnx 的value拼接上能够唯一标识该线程的的数据,key为商品Id,value能够唯一标识,然后每个线程在释放锁的时候需要判断该锁是不是自己加的
以上是自己通过看视频,博客总结的一些知识点,关于redis实现分布式锁这个知识点还有很多,在实际场景中要比上面写的更加复杂,自己总结的也只是一些皮毛。