利用redis中的set命令来实现分布式锁。
从Redis 2.6.12版本开始,set可以使用下列参数:
SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
EX second :设置键的过期时间为second秒。 SET key value EX second效果等同于SETEX key second value 。
PX millisecond :设置键的过期时间为millisecond毫秒。 SET key value PX millisecond效果等同于PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX效果等同于SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁,比如setnx name xiaowang。
当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
返回值:设置成功,返回 1 。设置失败,返回 0 。
@RestController
public class IndexController {
// private static final Logger LOGGER= LoggerFactory.getLogger(IndexController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if(stock>0){
int realStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");//jedis.set(key,value)
}else{
System.out.println("扣减失败,库存不足");
}
return "end";
}
}
这是一个经典的库存扣减的案例,每实现一次则库存减一:
在这里我们使用的是springboot整合redis来实现的,注入了springboot操作redistribution的模板。当然不清楚也没关系,我在每个关键的代码后面都用jedis解释过了。
比如这个去设置key,value的操作,jedis.set(key,value);
stringRedisTemplate.opsForValue().set("stock",realStock+"");//jedis.set(key,value)
1:接下来我们具体看看这段代码,首先能不能看出这段代码有没有什么问题?
可以看出是由并发问题,如果有很多个线程来执行这段代码,有很多请求,如果有三个用户同时访问这段代码的话,如果有50件商品,如果同时发生的话,有可能最后返回的是49,按理来说应该47,出现了并发问题,那么怎么解决呢?
2:解决方案:添加synchronized锁能否解决问题呢?
如果在单体架构中的话,添加synchronized是没有问题的。但如果在集群架构中的话,这个war包放在多个tomcat上面的话,jdk为我们提供了很多锁,比如synchronized锁或者lock锁都是在jvm进程上的,只在一个tomcat上面有用,如果通过nginx发放到多个tomcat上面的话,实际上都是锁不住的。
3:使用redis的SETNX实现分布式锁
当有很多请求发过来时,会一个一个请求来执行,因为redis是单线程架构。
问题1:stringRedisTemplate.delete(lockKey);//释放点这个锁,如果在这段代码出现异常了的话, 这个锁没有释放掉,别的线程进来的话拿不到锁,会陷入一种死锁状态。
解决办法:给这段代码加异常进行处理,让他最后都能释放掉这把锁,不要陷入死锁的情况中。
问题2:如果还没有释放掉锁时,系统突然宕机了,finally后面的代码执行不了了,怎么办呢?
解决办法:可以给这个key设置10秒钟的过期时间,当时间到了的时候,redis会自动删除这个key。
问题3:在超高并发场景下,我们设置key'的过期时间是10秒,如果第一个线程过来会执行15秒的话,在高并发的情况下,很有可能会出现第一个线程释放掉了第二个线程的锁,而导致锁失效。
解决办法:我们给每一个线程生成一个不重复的字符串,在释放的锁的时候去验证一下是不是我们这个线程自己的锁
问题4:如果我们代码执行了10秒钟的话,这个key已经过期了,代码还没有执行完,这时应该怎么解决的?
解决办法:我们可以在这个key过期后去演演唱这个key的生命时间去给他续命,我们可以用redisson去实现。
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey="lockKey";
String redisId= UUID.randomUUID().toString();
try { //问题一
Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"lockkey",10, TimeUnit.SECONDS);//问题二
if(!result){
return "err";
}
int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if(stock>0){
int realStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");//jedis.set(key,value)
}else{
System.out.println("扣减失败,库存不足");
}
}finally {
if(stringRedisTemplate.opsForValue().get(lockKey).equals(redisId)){ //问题三
stringRedisTemplate.delete(lockKey);//释放点这个锁
}
}
return "end";
}
Redisson就是redis的一个开源框架,这个框架就已经将上述的问题都封装好了,直接使用就行了
@RestController
public class IndexController {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey="lockKey";
RLock redisId=redisson.getLock(lockKey);//拿到锁对象
try { //问题一
redisId.lock();//j加锁
int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if(stock>0){
int realStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");//jedis.set(key,value)
}else{
System.out.println("扣减失败,库存不足");
}
}finally {
redisId.unlock();
}
return "end";
}
}
Redisson底层基本上大致就是我们上述的那些问题并且进行了封装,让代码变得更简洁。