基于redis实现分布式锁(SETNX和Redisson)(以扣减库存来当作案例)

一:实现原理:

利用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 :只在键已经存在时,才对键进行设置操作。

二:SETNX
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁,比如setnx name xiaowang。

当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:设置成功,返回 1 。设置失败,返回 0 。

三:接下来我们以一个案例来看看,如何使用redis实现分布式锁

@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实现分布式锁
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";
}

}
上一篇:Qlib外部股票数据获取


下一篇:(5)MySQL进阶篇SQL优化(优化数据库对象)