当前位置: 首页 > news >正文

基于Redis的分布式锁

分布式场景下并发安全问题的引发

前面通过加锁解决了单机状态下一人一单的问题,但是当出现了分布式,前面的加锁形式不再适用 ,每个jvm有一个自己的锁监视器,只能被内部线程获取,其他jvm无法使用,那么多台jvm的锁监视器不共用一个锁监视器,就容易出现分布式场景下并发安全问题。

问题分析 

所以我们要使用可以解决分布式场景下的位于jvm外的锁,多个jvm共同使用该锁,而不是使用每个jvm的内部锁。

分布式锁有如下特点:

 这里我们就选用redis来实现我们的分布式锁!

Redis锁的demo

redis锁要实现如上两个基本操作:获取锁和删除锁,在获取锁的同时为了防止宕机出现死锁,要手动添加过期时间,那么为了防止只加锁没有加过期时间的情况出现,我们要保证加锁和加过期时间的原子性,也就是他俩必须同时进行!

那么上述加锁的命令可以换成如下:

分布式锁初步实现

实现锁接口

public class SimpleRedisLock implements ILock {//用户的useridprivate String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}//锁的key值private static final String KEY_PREFIX = "lock:";//生成锁的value值private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);//防止Boolean和boolean拆箱出问题,如果success为null,则返回falsereturn Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}
}

使用锁

VoucherOrderServiceImpl.java中的seckillVoucher方法中编写如下代码:

//获取用户userid
Long id = UserHolder.getUser().getId();SimpleRedisLock redisLock = new SimpleRedisLock("order:" + id, stringRedisTemplate);
//加锁,1200s是锁的过期时间boolean tryLock = redisLock.tryLock(1200);//判断锁是否获取成功if (!tryLock){return Result.fail("不允许重复下单");}try {//锁加到这里,事务提交后才释放锁//获取事务的动态代理对象,需要在启动类加注解暴漏出对象IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();//拿到动态代理对象
//            return createVoucherOrder(voucherId, voucher);return proxy.createVoucherOrder(voucherId, voucher);//使用动态代理类的对象,事务可以生效} finally {//无论如何都要释放锁,防止死锁redisLock.unlock();}

分布式场景下调试看效果

两个application,一个是8081端口,一个是8082端口。

apifox模拟同一个用户发送请求,authorization的参数值是同一个用户的,存储在redis中。

 如下:在8082的断点处获取锁失败,在8081的断点处获取锁成功,即只有一次成功获取锁

数据库中优惠券库存stock-1而不是-2,优惠券订单产生1个,数据库没问题!

redis中查看锁的key值,1010正是userid,问题解决,达到我们想要的效果!

redis分布式锁误删问题

问题分析

当线程1获取锁成功时,如果该业务执行时间长以至于超过了设置的锁过期时间,那么在业务还未完成时,锁便自动释放,此时线程1无锁,线程2获取到了锁执行业务,当线程1业务执行完后,按照业务逻辑仍会释放锁,但此时释放的是线程2的锁,这就出现了锁误删的问题。

解决锁误删

对于每个线程,我们获取其线程标识(每个JVM内部都维护了线程的id,这个id是自增的,那么多个jvm可能出现线程id一致的情况,为了避免该情况出现我们用UUID生成一个随机字符串作为前缀,以降低线程id重复的概率)作为锁的value

在释放锁时,我们先从redis获取对应的value值,跟当前线程的value做对比,一致则可以删除,否则就不能删除。

修改trylock和unlock方法 

public class SimpleRedisLock implements ILock {//用户的useridprivate String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}//锁的key值private static final String KEY_PREFIX = "lock:";//线程的前缀,因为分布式下,多个jvm,每个jvm中维护的线程的id都是递增的,那么可能出现多个jvm的线程id一致,所以这里用uuid生成字符串作为前缀private static final String THREAD_PREFIX = UUID.randomUUID().toString(true)+"-";@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标示  线程前缀+线程idString threadId = THREAD_PREFIX + Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);//防止Boolean和boolean拆箱出问题,如果success为null,则返回falsereturn Boolean.TRUE.equals(success);}@Overridepublic void unlock() {// 获取线程标示String threadId =THREAD_PREFIX + Thread.currentThread().getId();// 获取锁中的标示String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断锁标示是否一致,防止锁误删if(threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}}
}

分布式锁的原子性

问题分析

JVM做Full GC时会阻塞所有代码,时间过长会出现锁超时自动释放,那么其他线程会趁虚而入获得锁。 

那么会出现如下情况:线程1获取锁执行业务逻辑后要释放锁,在判断完释放锁的条件为true后,即 threadId.equals(id)==true,正要释放锁时出现 Full GC,所有代码被阻塞,直到锁超时自动释放 (注意此时锁不是正常释放而是锁超时释放的),就在这时GC完毕代码恢复,线程2趁虚而入获得锁,而线程1也恢复了要执行释放锁的代码,因为GC前已经判断过释放条件为ture,那么此时线程1仍然认为锁是自己的,会错误地释放线程2的锁,又出现了误删问题。这里我们就要保证锁的原子性,即 判断锁的标识 和 释放锁 两个动作必须同时发生!

问题解决(Lua脚本)

Lua是一种编程语言,Redis提供了Lua脚本功能,即可以在一个脚本中写多条redis指令,确保了多条命令执行的原子性

代码实现

public class SimpleRedisLock implements ILock {//用户的useridprivate String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}//锁的key值private static final String KEY_PREFIX = "lock:";//线程的前缀,因为分布式下,多个jvm,每个jvm中维护的线程的id都是递增的,那么可能出现多个jvm的线程id一致,所以这里用uuid生成字符串作为前缀private static final String THREAD_PREFIX = UUID.randomUUID().toString(true)+"-";private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标示  线程前缀+线程idString threadId = THREAD_PREFIX + Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);//防止Boolean和boolean拆箱出问题,如果success为null,则返回falsereturn Boolean.TRUE.equals(success);}@Overridepublic void unlock() {// 调用lua脚本  stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),THREAD_PREFIX + Thread.currentThread().getId());}
}

在resource资源文件夹下创建Lua脚本内容如下:

if (redis.call('get',KEYS[1])==ARGV[1]) thenreturn redis.call('del',KEYS[1])
end
return 0

相关文章:

基于Redis的分布式锁

分布式场景下并发安全问题的引发 前面通过加锁解决了单机状态下一人一单的问题&#xff0c;但是当出现了分布式&#xff0c;前面的加锁形式不再适用 &#xff0c;每个jvm有一个自己的锁监视器&#xff0c;只能被内部线程获取&#xff0c;其他jvm无法使用&#xff0c;那么多台j…...

如何将 Apifox 的自动化测试与 Jenkins 集成?

CI/CD &#xff08;持续集成/持续交付&#xff09; 在 API 测试 中的主要目的是为了自动化 API 的验证流程&#xff0c;确保 API 发布到生产环境前的可用性。通过持续集成&#xff0c;我们可以在 API 定义变更时自动执行功能测试&#xff0c;以及时发现潜在问题。 Apifox 支持…...

【FFmpeg】av_write_frame函数

目录 1.av_write_frame1.1 写入pkt&#xff08;write_packets_common&#xff09;1.1.1 检查pkt的信息&#xff08;check_packet&#xff09;1.1.2 准备输入的pkt&#xff08;prepare_input_packet&#xff09;1.1.3 检查码流&#xff08;check_bitstream&#xff09;1.1.4 写入…...

【算法专题】双指针算法

1. 移动零 题目分析 对于这类数组分块的问题&#xff0c;我们应该首先想到用双指针的思路来进行处理&#xff0c;因为数组可以通过下标进行访问&#xff0c;所以说我们不用真的定义指针&#xff0c;用下标即可。比如本题就要求将数组划分为零区域和非零区域&#xff0c;我们不…...

Lock与ReentrantLock

在 Java 中&#xff0c;Lock 接口和 ReentrantLock 类提供了比使用 synchronized 方法和代码块更广泛的锁定机制。 简单示例&#xff1a; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {pr…...

ARM/Linux嵌入式面经(十三):紫光同芯嵌入式

static关键字 static关键字一文搞懂这个知识点,真的是喜欢考!!! stm32启动时如何配置栈的地址 在STM32启动时配置栈的地址是一个关键步骤,这通常是在启动文件(如startup_stm32fxxx.s,其中xxx代表具体的STM32型号)中完成的。 面试者回答: STM32启动时配置栈的地址主…...

名企面试必问30题(二十四)—— 说说你空窗期做了什么?

回答示例一 在空窗期这段时间&#xff0c;我主要进行了两方面的活动。 一方面&#xff0c;我持续提升自己的专业技能。我系统地学习了最新的软件测试理论和方法&#xff0c;深入研究了自动化测试工具和框架&#xff0c;例如 Selenium、Appium 等&#xff0c;并通过在线课程和实…...

基础权限储存

一、要求&#xff1a; 1、建立用户组shengcan&#xff0c;其id为2000工 2、建立用户组 caiwu&#xff0c;其id为2001 3、建立用户组 jishu&#xff0c;其id 为 2002 4、建立目录/sc,此目录是 shengchan 部门的存储目录&#xff0c;只能被 shengchan 组的成员操作,其他用户没有…...

Could not find a package configuration file provided by “roscpp“ 的参考解决方法

文章目录 写在前面一、问题描述二、解决方法参考链接 写在前面 自己的测试环境&#xff1a; Ubuntu20.04 ROS-Noetic 一、问题描述 编译程序时出现如下报错&#xff1a; -- Could NOT find roscpp (missing: roscpp_DIR) -- Could not find the required component roscpp.…...

运维系列.Nginx配置中的高级指令和流程控制

运维专题 Nginx配置中的高级指令和流程控制 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/…...

Virtualbox和ubuntu之间的关系

1、什么是ubuntu Ubuntu 是一个类似于 Windows 的操作系统&#xff0c;但它是基于 Linux 内核开发的开源操作系统 2、什么是Virtualbox VirtualBox 是一款虚拟机软件&#xff0c;使我们可以物理机上创建和运行虚拟机 也就是说,VirtualBox 提供了一个可以安装和运行其他操作系…...

【在Linux世界中追寻伟大的One Piece】HTTPS协议原理

目录 1 -> HTTPS是什么&#xff1f; 2 -> 相关概念 2.1 -> 什么是"加密" 2.2 -> 为什么要加密 2.3 -> 常见的加密方式 2.4 -> 数据摘要 && 数据指纹 2.5 -> 数字签名 3 -> HTTPS的工作过程 3.1 -> 只使用对称加密 3.2 …...

【WebRTC实现点对点视频通话】

介绍 WebRTC (Web Real-Time Communications) 是一个实时通讯技术&#xff0c;也是实时音视频技术的标准和框架。简单来说WebRTC是一个集大成的实时音视频技术集&#xff0c;包含了各种客户端api、音视频编/解码lib、流媒体传输协议、回声消除、安全传输等。对于开发者来说可以…...

【Unity】RPG2D龙城纷争(八)寻路系统

更新日期&#xff1a;2024年7月4日。 项目源码&#xff1a;第五章发布&#xff08;正式开始游戏逻辑的章节&#xff09; 索引 简介一、寻路系统二、寻路规则&#xff08;角色移动&#xff09;三、寻路规则&#xff08;角色攻击&#xff09;四、角色移动寻路1.自定义寻路规则2.寻…...

C++ 函数高级——函数重载——基本语法

作用&#xff1a;函数名可以相同&#xff0c;提高复用性 函数重载满足条件&#xff1a; 1.同一个作用域下 2.函数名称相同 3.函数参数类型不同 或者 个数不同 或者 顺序不同 注意&#xff1a;函数的返回值不可以作为函数重载的条件 示例&#xff1a; 运行结果&#xff1a;...

Go语言实现的端口扫描工具示例

Go语言实现的端口扫描工具示例 创建一个端口扫描工具涉及到网络编程和并发处理&#xff0c;下面是一个简单的Go语言实现的端口扫描工具示例。这个工具会扫描指定IP地址的指定范围内的端口。 请注意&#xff0c;使用端口扫描工具可能会违反某些网络的使用条款&#xff0c;甚至…...

SpringSecurity初始化过程

SpringSecurity初始化过程 SpringSecurity一定是被Spring加载的&#xff1a; web.xml中通过ContextLoaderListener监听器实现初始化 <!-- 初始化web容器--><!--设置配置文件的路径--><context-param><param-name>contextConfigLocation</param-…...

Python爬取股票信息-并进行数据可视化分析,绘股票成交量柱状图

为了使用Python爬取股票信息并进行数据可视化分析&#xff0c;我们可以使用几个流行的库&#xff1a;requests 用于网络请求&#xff0c;pandas 用于数据处理&#xff0c;以及 matplotlib 或 seaborn 用于数据可视化。 步骤 1: 安装必要的库 首先&#xff0c;确保安装了以下P…...

秋招突击——7/4——复习{}——新作{最长公共子序列、编辑距离、买股票最佳时机、跳跃游戏}

文章目录 引言复习新作1143-最长公共子序列个人实现 参考实现编辑距离个人实现参考实现 贪心——买股票的最佳时机个人实现参考实现 贪心——55-跳跃游戏个人实现参考做法 总结 引言 昨天主要是面试&#xff0c;然后剩下的时间都是用来对面试中不会的东西进行查漏补缺&#xff…...

udp发送数据如果超过1个mtu时,抓包所遇到的问题记录说明

最近在测试Syslog udp发送相关功能&#xff0c;测试环境是centos udp头部的数据长度是2个字节&#xff0c;最大传输长度理论上是65535&#xff0c;除去头部这些字节&#xff0c;可以大概的说是64k。 写了一个超过64k的数据(随便用了一个7w字节的buffer)发送demo&#xff0c;打…...

电子电气架构 --- 智能座舱万物互联

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…...

笔记本电脑内存不够

笔记本电脑内存不够是众多笔记本用户面临的常见问题&#xff0c;尤其是对于一些需要处理大型文件或者运行复杂软件的用户&#xff0c;这个问题可能会严重影响笔记本的使用体验。那么&#xff0c;我们应该如何解决笔记本电脑内存不够的问题呢&#xff1f;本文将从几个方面进行详…...

Vue项目使用mockjs模拟后端接口

文章目录 操作步骤1. 安装 mockjs 和 vite-plugin-mock2. 安装 axios3. 创建mock路径4. 配置 viteMockConfig5. 编写第一个mock接口6. 创建 createProdMockServer7. 配置 axios8. 编写请求接口9. 在页面中使用 操作步骤 1. 安装 mockjs 和 vite-plugin-mock vite-plugin-mock …...

Linux下使用arping检测IP地址是否冲突

arping简介 在Linux中&#xff0c;arping是一个用来发送ARP请求到一个相邻主机的工具&#xff0c;通常用于检测网络上的IP地址冲突。 使用arping检测IP地址是否冲突的方法 例1&#xff1a;使用如下命令检测10.206.216.95是否冲突 (使用-I参数指定网络接口) # arping -I eth…...

为什么 npm run serve 正常,npm run build 就报错:digital envelope routines::unsupported

这个错误通常与 Node.js 版本和使用的加密算法有关。让我解释一下原因和可能的解决方案&#xff1a; 错误原因 这个错误&#xff08;“error:0308010C:digital envelope routines::unsupported”&#xff09;通常发生在以下情况&#xff1a; 使用较新版本的 Node.js&#xf…...

模电基础 - 简介

目录 零 .简介 一. 学习方法 二. 教材推荐 三. 总结 零 .简介 “模电”即模拟电子技术&#xff0c;是电子信息工程、电气工程及其自动化等相关专业的一门关键基础课程。 首先&#xff0c;在半导体器件方面&#xff0c;二极管是一种具有单向导电性的器件&#xff0c;由 P 型…...

uniapp中实现瀑布流 短视频页面展示

直接上干货 第一部分为结构 <swiper class"list" :currentindex change"swiperchange" scrolltolower"onReachBottom"><swiper-item style"overflow: scroll;" v-for"(item,index) in 2" :key"index"&g…...

python-开关灯(赛氪OJ)

[题目描述] 假设有 N 盏灯&#xff08;N 为不大于 5000 的正整数&#xff09;&#xff0c;从 1 到到 N 按顺序依次编号&#xff0c;初始时全部处于开启状态&#xff1b;第一个人&#xff08; 1 号&#xff09;将灯全部关闭&#xff0c;第二个人&#xff08; 2 号&#xff09;将…...

基于改进高斯-拉普拉斯滤波器的一维时间序列平滑与降噪(MATLAB)

以图像处理为例&#xff0c;拉普拉斯算子是基于图像的二阶导数来找到边缘并搜索过零点&#xff0c;传统的拉普拉斯算子常产生双像素宽的边缘&#xff0c;对于较暗区域中的亮斑进行边缘检测时&#xff0c;拉普拉斯运算就会使其变得更亮。因此&#xff0c;与梯度算子一样&#xf…...

计算组的妙用!!页面权限控制

需求描述&#xff1a; 某些特殊的场景下&#xff0c;针对某页看板&#xff0c;需要进行数据权限卡控&#xff0c;但是又不能对全部的数据进行RLS处理&#xff0c;这种情况下可以利用计算组来解决这个需求。 实际场景 事实表包含产品维度和销售维度 两个维度属于同一公司下面的…...