【redis知识点整理】 --- redis实现分布式锁需要解决的问题

本文代码对应的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!!!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值