【Redis专题】大厂生产级Redis高并发分布式锁实战

news2025/1/17 22:59:24

目录

  • 前言
  • 课程内容
    • 一、一个案例引发的思考
    • 二、Redis分布式锁的演进
      • 2.1 单纯使用Redis的setnx实现分布式锁
      • 2.2 setnx + 过期时间
      • 3.3 Redisson实现分布式锁:setnx + 过期时间 + 锁续命
    • 三、Redisson客户端实现的分布式锁及源码分析
  • 学习总结

前言

Redis中间件,非常推荐大家学的一个东西。甚至这么说,Redis也许是我们Java程序员,能接触到的分布式、微服务中间件中一个较为高级,但又比较接地气的中间件了。为什么接地气?因为哪怕是在小项目中,Redis都是一个比较常用、可靠的中间件!
但是我发现,新手用Redis缓存很容易钻入一个牛角尖,那就是Redis会不会崩啊?万一哪一天断电了,宕机了呢?最后得到一个结论:Redis不可靠啊!!! 但现实是,博主当前所在的小公司小项目Redis生产环境运行2年没蹦过。而且我们那点小体量,就算是崩了也无所谓,重启就行了(事实上,大公司大项目都会使用Redis集群解决这个问题)。但这话说的不严谨,其实关于Redis不可靠问题,是我在前一篇文章说的正是【Redis主从架构】、【Redis集群】要解决的问题,人家已经给出方案了。一句话:切勿讳疾忌医啊同学们

课程内容

一、一个案例引发的思考

先假设,我们当前线上有一个项目,使用nginx分别轮循到2个tomcat上。它的模型如下:
在这里插入图片描述
如上图,为了减缓节点压力,我们把项目部署成了2个tomcat,分别是8080端口和8081端口。并且采用的是轮询策略,客户端每次过来一条请求,将按序依次分流到这2个tomcat上。
然后,这个tomcat项目提供了如下这个接口:

    @RequestMapping("/deduct_stock")
    public String deductStock1() {
        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)
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }

        return "end";
    }

案例分析:这个案例很简单,就是提供一个扣减库存的接口,模拟外部电商系统购买物品之后,扣减库存。但是,大家一定要注意到以下几个点:

  1. 无论我们部署多少个tomcat,库存肯定是被共享的。假设,我只有100个优惠产品,那肯定只能被卖出去100件
  2. 我i们把库存量放到了redis中去了,每卖出去一件,stock - 1,并且写回缓存

但事实上,上面这个代码是有问题的。不知道大家能不能理解到?这个对于有经验的朋友来说可能洒洒水而已,很简单。为了照顾萌新,我这边画个图吧。
在这里插入图片描述
上图是单个请求(线程)扣减库存的时序图(可能画的不标准,别在意,意思到了就好)。单个线程之下,如果请求都是串行的,也就是上一条执行完了,下一条继续进来请求扣减库存,那当然没问题。但是同学们啊,我们这里是多线程、多个tomcat的分布式环境,所以,你很可能会遇到下面这种情况:
在这里插入图片描述
我想,我上图已经画的很清楚了。当这里有另一个客户端请求进来的时候,并且请求顺序跟上面一样,情况显然就开始不对了,出现了【超卖】问题(两个客户端都扣减了一次库存,但是写回都是:99)。为了提点一下小白,上述的并发编程思想,我尽量再点一下:

  1. 一个http请求,代表着一个线程,所以,同一时刻不同的http请求,肯定是两条不同的线程
  2. 由于线程之间的隔离,每个线程都会保存一个变量副本,即:上图中客户端1跟客户端2,都会各自保存一个stock副本,各自进行加减操作,然后再把结果写回redis

好了,既然【超卖】问题已经出现了,那上面的问题怎么解决呢?下面,我们就好好研究一下,这个解决方式的演进。

二、Redis分布式锁的演进

一个很正常的思路,对于这种资源共享问题,多线程竞争问题,我想很多同学会想:那就加个锁呗。于是,有朋友提出了:synchronized锁住代码块,嘿嘿,如下所示:

    @RequestMapping("/deduct_stock")
    public String deductStock1() {
        synchronized (this) {
            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)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        }
        return "end";
    }

咱先别讨论【锁粒度】问题,是不是真的有朋友想着用上面这种方式解决的呢?
这么说,上面这种方式在一个tomcat下,单进程的时候,是有效的,但是大伙忘了我们当前的环境,2个tomcat,分布式环境啊!你在tomcat1加synchronized锁,我tomcat2是感知不到的!所以得换个方式。
有经验的朋友可能已经想到了,利用redis io跟命令处理是单线程的特性,所以可以使用setnx key value实现分布式锁,没错,这就是我们现在要讲的东西。下面我们将开始演进,利用Redis实现分布式锁需要解决的问题。这边用的Redis客户端(工具)是StringRedisTemplate,具体使用方式这边就不介绍了。

2.1 单纯使用Redis的setnx实现分布式锁

改正后的代码如下:

    @RequestMapping("/deduct_stock")
    public String deductStock1() {
        String lockKey = "lock:product_101";
		
		// 使用redis的setnx命令
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");
        if (!result) {
            return "error_code";
        }

		try {
            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)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
             stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }

这边很简单,就是使用了stringRedisTemplate.opsForValue().setIfAbsent,即:Redissetnx命令。然后,如果Redis返回的result不是true,那就返回一个错误码,提示客户端【上锁失败】就好了。那同学们,这样就行了吗?我们画个图吧,嘿嘿嘿
在这里插入图片描述

就像上图这样,显然,从目前来看,是没问题,确实已经实现了,多个tomcat情况下,都能控制共享资源了。但是,万一,真的出现了,客户端1在拿到锁之后,还没走到释放锁的时候宕机了,那完了,资源没办法被释放!怎么办?难道我手动删除不成?这就是,单纯利用setnx会遇到的第一个问题:死锁。
哈,我想到了这里,敏锐的同学发现了,既然有这种问题,那我个过期时间不就行了吗?对啊,给过期时间啊,行得通!

2.2 setnx + 过期时间

对于这个方案,其实有2个版本,我先说第一个错误版本:

    @RequestMapping("/deduct_stock")
    public String deductStock1() {
        String lockKey = "lock:product_101";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");
        stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
        if (!result) {
            return "error_code";
        }

		try {
            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)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
             stringRedisTemplate.delete(lockKey);
        }

        return "end";
    }

跟上述代码一样,我们在setIfAbsent之后,加一个过期时间函数expire。这个方案其实是不行的。很显然,目前这两步操作不是【原子性】的,Java代码嘛,肯定是一条一条按顺序执行的,就跟上面的例子一样,当我们出现极端情况,诶,还真就执行完setIfAbsent之后,expire之前宕机了呢?一样完犊子,会出现死锁,所以,正常我们是利用setIfAbsent另一个重载方法,它会帮我们【原子】地操作这两步,如下:

    @RequestMapping("/deduct_stock")
    public String deductStock1() {
        String lockKey = "lock:product_101";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge", 30, TimeUnit.SECONDS);
        if (!result) {
            return "error_code";
        }

try {
            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)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
             stringRedisTemplate.delete(lockKey);
        }

        return "end";
    }

好了,目前是原子性的了。并且呢,我们给这个锁,+了一个30秒的过期时间。可以了吗?啊,不完全对。大家能想明白吗?
很显然啊,你这个过期时间是固定30秒的,万一我业务30秒内完成不了呢?嘿,你是不是想说什么业务30秒完成不了,哈,真可能出现,比如IO阻塞了什么的。
那有朋友会继续建议:那我设置60秒?120秒?240秒?丢,我设置超长时间,总行了吧???咳咳咳,啊这个,有点道理的
在这里插入图片描述
可如果,我拿出:我不管,你锁多久我业务执行时间永远比你多1秒,阁下该如何应对呢?
在这里插入图片描述
哈哈,你是不是想说我无理取闹。好吧,我不抬杠了,我提出一个比较合理的问题哦。
如果你设置的过期时间比较长假设5分钟,万一这时候真的宕机了呢?等过期时间到期吗?天啊,如果此时有数百万个请求进来,你是不是想让人等你5分钟自动解锁啊?这个时候跟宕机有啥区别!
在这里插入图片描述
上述说问题的就是由于过期时间比较长,造成的整体【拒绝服务】问题,这是问题一。
其实时间设置过短,我想大家也能想得到会出现什么问题,可能业务没执行完就释放锁了,最后锁形同虚设,其他请求一样进来了,到时候又出现了跟最开始说的情景。这是问题二。
另外还想补充一点,时间设置过短其实还会出现一个很有意思的现象,这里我们画个图给大家看看:
在这里插入图片描述
由于客户端A在执行业务期间锁就过期了,此时,客户端B进来加锁肯定是能成功的。但是客户端A在没有出现错误的情况,肯定会继续执行下去的,并且最终会释放锁。那最终这个释放锁释放的是谁的锁呢?客户端B的呀!此时,又有一个新的客户端C过来加锁,那不是成功了吗?显然这样做是有问题的,【错误释放别人的锁】,并且自己的业务还不一定执行完了!
其实针对这个问题,还是有解决方案的。那就是每次上锁的时候,+一个uuid,最后释放锁的时候判断一下uuid是不是跟当前的uuid一样就好了。如下:

    @RequestMapping("/deduct_stock")
    public String deductStock1() {
        String lockKey = "lock:product_101";
        String clientId = UUID.randomUUID().toString();
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
        if (!result) {
            return "error_code";
        }

        try {
            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)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "end";
    }

关键代码如上:最后finnaly块判断释放的时候,里面的value值是不是当初我们设置的那个。但其实这仅仅只是解决了我们其中一个问题而已。还有个关键问题,那就是锁时间到底该怎么来确定?
于是有人提出了一个方案:锁续命。顾名思义,就是临到期前,重新设置过期时间。

3.3 Redisson实现分布式锁:setnx + 过期时间 + 锁续命

这里就要开始介绍,基于Redisson实现的一个分布式锁方案了。首先,要使用Reddison,需要先引入jar包,pom.xml如下:

<dependency>
  <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>

 <dependency>
   <groupId>redis.clients</groupId>
   <artifactId>jedis</artifactId>
   <version>2.9.0</version>
 </dependency>

 <dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.6.5</version>
 </dependency>

代码示例:

@RequestMapping("/deduct_stock")
public String deductStock1() {

     String lockKey = "lock:product_001";
     
     //获取锁对象
     RLock redissonLock = redisson.getLock(lockKey);
     
     //加分布式锁
     redissonLock.lock();  //  .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
     try {
         int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); 
         if (stock > 0) {
             int realStock = stock - 1;
             stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
             System.out.println("扣减成功,剩余库存:" + realStock);
         } else {
             System.out.println("扣减失败,库存不足");
         }
     } finally {
         
         //解锁
         redissonLock.unlock();
     }
     return "end";
 }

上面的代码很简单,我们无需关心之前提到的那几个问题了,Redisson在封装的api里面已经帮我们做好了一切。我们只需要简单的调用lockunlock而已。

三、Redisson客户端实现的分布式锁及源码分析

待完成。。。

学习总结

  1. 学习了Redis分布式锁实现的演进过程
  2. 学习了基于Redis实现分布式锁需要解决的一些问题,及解决思路

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/997879.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文件上传之图片码混淆绕过(upload的16,17关)

目录 1.upload16关 1.上传gif loadup17关&#xff08;文件内容检查&#xff0c;图片二次渲染&#xff09; 1.上传gif&#xff08;同上面步骤相同&#xff09; 2.条件竞争 1.upload16关 1.上传gif imagecreatefromxxxx函数把图片内容打散&#xff0c;&#xff0c;但是不会…

Selenium - Tracy 小笔记2

selenium本身是一个自动化测试工具。 它可以让python代码调用浏览器。并获取到浏览器中加们可以利用selenium提供的各项功能。帮助我们完成数据的抓取。它容易被网站识别到&#xff0c;所以有些网站爬不到。 它没有逻辑&#xff0c;只有相应的函数&#xff0c;直接搜索即可 …

dubbo 服务注册使用了内网IP,而服务调用需要使用公网IP进行调用

一、问题描述&#xff1a; 使用dubbo时&#xff0c;提供者注册时显示服务地址ip为[内网IP:20880]&#xff0c;导致其他消费者在外部连接的情况下时&#xff0c;调用dubbo服务失败 二、解决办法 方法一、修改hosts文件 &#xff08;1&#xff09;. 先查询一下服务器的hostna…

【动态规划刷题 13】最长递增子序列 摆动序列

300. 最长递增子序列 链接: 300. 最长递增子序列 1.状态表示* dp[i] 表⽰&#xff1a;以 i 位置元素为结尾的「所有⼦序列」中&#xff0c;最⻓递增⼦序列的⻓度。 2.状态转移方程 对于 dp[i] &#xff0c;我们可以根据「⼦序列的构成⽅式」&#xff0c;进⾏分类讨论&#…

RabbitMQ管控台使用

安装成功RabbitMQ后&#xff0c;进入到管理控制台界面 拷贝配置文件到指定目录当中然后重启RabbitMQ。

FIR滤波器简述及FPGA仿真验证

数字滤波器的设计&#xff0c;本项目做的数字滤波器准确来说是FIR滤波器。 FIR滤波器&#xff08;有限冲激响应滤波器&#xff09;&#xff0c;与另一种基本类型的数字滤波器——IIR滤波器&#xff08;无限冲击响应滤波器&#xff09;相对应&#xff0c;其实就是将所输入的信号…

算法通关村第十九关——动态规划是怎么回事(青铜)

算法通关村第十九关——动态规划是怎么回事&#xff08;青铜&#xff09; 前言1 什么是动态规划2 动态规划的解题步骤3 简单入门3.1 组合总和3.2 最小路径和3.3 三角形最小路径和 4 理解动态规划 前言 动态规划是一种解决复杂问题的算法思想&#xff0c;它将一个大问题分解为多…

Apache HTTPD 多后缀名解析漏洞复现

什么是多后缀名解析漏洞加粗样式: 多后缀名解析漏洞&#xff08;Multiple Extension Handling Vulnerability&#xff09;指的是一种安全漏洞&#xff0c;发生在某些操作系统或网络服务中的文件扩展名处理机制中。 这种漏洞的本质是当文件具有多个后缀名&#xff08;例如file.…

l8-d12 IP协议与ethernet协议

一、IP协议作用和意义 分组在互联网中的传送 分组传输路径 二、IP 数据报首部格式 1.IP 数据报的格式 版本——占 4 位&#xff0c;指 IP 协议的版本。目前的 IP 协议版本号为 4 (即 IPv4)。 首部长度——占 4 位&#xff0c;可表示的最大数值是 15 个单位(一个单位为 4 字…

【Spring】手动实现Spring底层机制-问题的引出

&#x1f384;欢迎来到边境矢梦的csdn博文&#x1f384; &#x1f384;本文主要梳理手动实现Spring底层机制-问题的引出 &#x1f384; &#x1f308;我是边境矢梦&#xff0c;一个正在为秋招和算法竞赛做准备的学生&#x1f308; &#x1f386;喜欢的朋友可以关注一下&#x1…

【实践篇】Redis最强Java客户端(四)之Ression分布式集合使用指南

文章目录 0. 前言1.Ression分布式集合1.1 分布式列表1.1.1 使用场景和实现原理&#xff1a;1.1.2 基本概念和使用方法&#xff1a; 1.2 分布式集合1.2.1 使用场景和实现原理&#xff1a;1.2.2 基本概念和使用方法&#xff1a; 1.3 分布式映射1.3.1 使用场景和实现原理&#xff…

CSS三种样式表、样式表优先级、CSS选择器

一、CSS介绍&#xff1a; 1.1、CSS介绍&#xff1a; CSS&#xff0c;全称是&#xff1a;Cascading Style Sheets&#xff0c;层叠样式表&#xff0c;用于修饰HTML页面的。 CSS编写规则如下所示&#xff1a; CSS编写的规则分为两部分&#xff0c;分别是&#xff1a;选择器、声…

SpringMVC学习简要

学习资料&#xff1a; SpringMVC-03-入门案例工作流程解析_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1ZF411G7eP/?p3&spm_id_frompageDriver&vd_source4ac53f52a57eb96a3c8406b971b038ae 常用MYSQL命令&#xff1a;http://t.csdn.cn/zshCP 学习要求 什么是…

微信小程序Day4笔记

1. 组件的创建与引用 创建组件 在项目的根目录中&#xff0c;鼠标右键&#xff0c;创建components -> test文件夹在新建的test文件夹上&#xff0c;鼠标右键&#xff0c;点击新建Component键入组件的名称之后回车&#xff0c;会自动生成组件对应的4个文件&#xff0c;后缀…

《服务器无状态设计:为什么如何实现无状态API?》

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

3.1 栈和队列的定义和特点

3.1.1 栈的定义和特点 主要内容&#xff1a; 3.1 栈和队列的定义和特点 3.1.1 栈的定义和特点 定义&#xff1a; 栈是一种特殊的线性表&#xff0c;只允许在一端进行插入或删除操作。这一端被称为栈顶&#xff0c;而另一端则称为栈底。不包含任何元素的栈被称为空栈。 特点…

分布式调度 Elastic-job

分布式调度 Elastic-job 1.概述 1.1什么是任务调度 我们可以思考一下下面业务场景的解决方案: 某电商平台需要每天上午10点&#xff0c;下午3点&#xff0c;晚上8点发放一批优惠券某银行系统需要在信用卡到期还款日的前三天进行短信提醒某财务系统需要在每天凌晨0:10分结算…

Java虚拟机(JVM)解析:内存区域、类加载、垃圾回收和选型考虑

目录 一、JVM内存区域划分 二、JVM类加载 三、JVM垃圾回收&#xff08;GC&#xff09; 一、JVM内存区域划分 栈堆方法区(元数据区)程序计数器 1.栈 举个例子&#xff1a; 那具体是怎么分的呢&#xff1f;如下图 本地方法栈&#xff1a;给JVM内部的方法准备的栈空间 虚拟…

在MySQL客户端使用Tab健进行命令补全

在MySQL客户端中&#xff0c;你可以使用Tab键进行命令补全&#xff0c;这将提高我们的效率&#xff0c;这与Linux命令行中的行为类似。例如&#xff0c;如果你输入SEL然后按Tab键&#xff0c;MySQL客户端会自动补全为SELECT。 然而&#xff0c;需要注意的是&#xff0c;这个功能…

数据结构 - 栈

目录 二、栈的实现 1.数组模拟实现栈 设计思想: 方法实现: Peek(): 偷窥栈顶元素 pop(): 栈顶出栈 push(): 2.链表模拟实现 3 . 栈的继承体系 总结 前言 大家好,这篇博客带大家了解一下栈是什么? 并且用两种方式为大家实现一下栈 一、栈是什么&#xff1f; 栈是一种数…