基于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的分布式锁
分布式场景下并发安全问题的引发 前面通过加锁解决了单机状态下一人一单的问题,但是当出现了分布式,前面的加锁形式不再适用 ,每个jvm有一个自己的锁监视器,只能被内部线程获取,其他jvm无法使用,那么多台j…...
如何将 Apifox 的自动化测试与 Jenkins 集成?
CI/CD (持续集成/持续交付) 在 API 测试 中的主要目的是为了自动化 API 的验证流程,确保 API 发布到生产环境前的可用性。通过持续集成,我们可以在 API 定义变更时自动执行功能测试,以及时发现潜在问题。 Apifox 支持…...
【FFmpeg】av_write_frame函数
目录 1.av_write_frame1.1 写入pkt(write_packets_common)1.1.1 检查pkt的信息(check_packet)1.1.2 准备输入的pkt(prepare_input_packet)1.1.3 检查码流(check_bitstream)1.1.4 写入…...
【算法专题】双指针算法
1. 移动零 题目分析 对于这类数组分块的问题,我们应该首先想到用双指针的思路来进行处理,因为数组可以通过下标进行访问,所以说我们不用真的定义指针,用下标即可。比如本题就要求将数组划分为零区域和非零区域,我们不…...
Lock与ReentrantLock
在 Java 中,Lock 接口和 ReentrantLock 类提供了比使用 synchronized 方法和代码块更广泛的锁定机制。 简单示例: 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题(二十四)—— 说说你空窗期做了什么?
回答示例一 在空窗期这段时间,我主要进行了两方面的活动。 一方面,我持续提升自己的专业技能。我系统地学习了最新的软件测试理论和方法,深入研究了自动化测试工具和框架,例如 Selenium、Appium 等,并通过在线课程和实…...
基础权限储存
一、要求: 1、建立用户组shengcan,其id为2000工 2、建立用户组 caiwu,其id为2001 3、建立用户组 jishu,其id 为 2002 4、建立目录/sc,此目录是 shengchan 部门的存储目录,只能被 shengchan 组的成员操作,其他用户没有…...
Could not find a package configuration file provided by “roscpp“ 的参考解决方法
文章目录 写在前面一、问题描述二、解决方法参考链接 写在前面 自己的测试环境: Ubuntu20.04 ROS-Noetic 一、问题描述 编译程序时出现如下报错: -- 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:http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/…...
Virtualbox和ubuntu之间的关系
1、什么是ubuntu Ubuntu 是一个类似于 Windows 的操作系统,但它是基于 Linux 内核开发的开源操作系统 2、什么是Virtualbox VirtualBox 是一款虚拟机软件,使我们可以物理机上创建和运行虚拟机 也就是说,VirtualBox 提供了一个可以安装和运行其他操作系…...
【在Linux世界中追寻伟大的One Piece】HTTPS协议原理
目录 1 -> HTTPS是什么? 2 -> 相关概念 2.1 -> 什么是"加密" 2.2 -> 为什么要加密 2.3 -> 常见的加密方式 2.4 -> 数据摘要 && 数据指纹 2.5 -> 数字签名 3 -> HTTPS的工作过程 3.1 -> 只使用对称加密 3.2 …...
【WebRTC实现点对点视频通话】
介绍 WebRTC (Web Real-Time Communications) 是一个实时通讯技术,也是实时音视频技术的标准和框架。简单来说WebRTC是一个集大成的实时音视频技术集,包含了各种客户端api、音视频编/解码lib、流媒体传输协议、回声消除、安全传输等。对于开发者来说可以…...
【Unity】RPG2D龙城纷争(八)寻路系统
更新日期:2024年7月4日。 项目源码:第五章发布(正式开始游戏逻辑的章节) 索引 简介一、寻路系统二、寻路规则(角色移动)三、寻路规则(角色攻击)四、角色移动寻路1.自定义寻路规则2.寻…...
C++ 函数高级——函数重载——基本语法
作用:函数名可以相同,提高复用性 函数重载满足条件: 1.同一个作用域下 2.函数名称相同 3.函数参数类型不同 或者 个数不同 或者 顺序不同 注意:函数的返回值不可以作为函数重载的条件 示例: 运行结果:...
Go语言实现的端口扫描工具示例
Go语言实现的端口扫描工具示例 创建一个端口扫描工具涉及到网络编程和并发处理,下面是一个简单的Go语言实现的端口扫描工具示例。这个工具会扫描指定IP地址的指定范围内的端口。 请注意,使用端口扫描工具可能会违反某些网络的使用条款,甚至…...
SpringSecurity初始化过程
SpringSecurity初始化过程 SpringSecurity一定是被Spring加载的: web.xml中通过ContextLoaderListener监听器实现初始化 <!-- 初始化web容器--><!--设置配置文件的路径--><context-param><param-name>contextConfigLocation</param-…...
Python爬取股票信息-并进行数据可视化分析,绘股票成交量柱状图
为了使用Python爬取股票信息并进行数据可视化分析,我们可以使用几个流行的库:requests 用于网络请求,pandas 用于数据处理,以及 matplotlib 或 seaborn 用于数据可视化。 步骤 1: 安装必要的库 首先,确保安装了以下P…...
秋招突击——7/4——复习{}——新作{最长公共子序列、编辑距离、买股票最佳时机、跳跃游戏}
文章目录 引言复习新作1143-最长公共子序列个人实现 参考实现编辑距离个人实现参考实现 贪心——买股票的最佳时机个人实现参考实现 贪心——55-跳跃游戏个人实现参考做法 总结 引言 昨天主要是面试,然后剩下的时间都是用来对面试中不会的东西进行查漏补缺ÿ…...
udp发送数据如果超过1个mtu时,抓包所遇到的问题记录说明
最近在测试Syslog udp发送相关功能,测试环境是centos udp头部的数据长度是2个字节,最大传输长度理论上是65535,除去头部这些字节,可以大概的说是64k。 写了一个超过64k的数据(随便用了一个7w字节的buffer)发送demo,打…...
python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...
使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...
CentOS下的分布式内存计算Spark环境部署
一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架,相比 MapReduce 具有以下核心优势: 内存计算:数据可常驻内存,迭代计算性能提升 10-100 倍(文档段落:3-79…...
涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...
Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马(服务器方面的)的原理,连接,以及各种木马及连接工具的分享 文件木马:https://w…...
基于SpringBoot在线拍卖系统的设计和实现
摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统,主要的模块包括管理员;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...
MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
