LUA脚本改造redis分布式锁
在redis集群模式下,我们会启动多个tomcat实例,每个tomcat实例都有一个JVM,且不共享。而synchronize锁的作用范围仅仅是当前JVM,所以我们需要一个作用于集群下的锁,也就是分布式锁。(就是不能用JVM自带的锁了,需要一个第三方应用实现锁)
在redis集群中我们是用setnx实现互斥锁来实现分布式锁。
setnx key value NX EX 时间 是往redis中创建一个key-value键值对,如果redis中没有该key,则创建成功,返回true;如果redis已经有了该key,则创建失败,返回null。NX是互斥,EX是超时时间,后跟具体时间+单位(s),EX用于过期时间,过了过期时间自动删除该key-value键值对。
我们用setnx命令实现分布式锁时,key和value都是String类型,key一般是特定前缀+该锁的名字,
value为线程id。
为什么value要存线程id呢?
因为存在这样情况:线程一获取锁,去执行逻辑,过程中遇到网络动荡,线程一卡住了,然后一段时间后,线程一获取的锁的过期时间到了,线程一的锁自动释放。然后线程二来获取锁,线程二获取成功,去执行逻辑,而在这个过程中线程一的网络动荡恢复了,线程一继续执行,线程一先于线程二执行完,线程一去释放锁,但此时线程一创建的锁已经因超时而自动释放了,所以此时线程一会去错误的释放线程二的锁,所以我们要把线程id存入加以判断,防止误删其他线程的id。
代码实现获取锁和释放锁:
package com.hmdp.utils;import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;import java.util.Collections;
import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}private static final String KEY_PREFIX = "lock:";//randomUUID生成的数字会带一个横线,toString(true)方法就是去掉横线//因为不同JVM中,可能存在线程号相同的情况,所以需要用UUID来区分不同的JVMprivate 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);//不能直接返回success,因为会有自动拆箱的风险,如果success是null,就会返回true,返回错误数据return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁中的标示String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断标示是否一致if(threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}}
}
我们需要新建一个类实现ILock接口,然后去重写其中的tryLock获取锁、unlock释放锁方法。
往SimpleRedisLock的构造方法传入name变量,方便我们在实例化SimpleRedisLock类中设置该锁的名字。我们后面用setnx命令创建锁时的key值就是特定前缀KEY_PREFIX加上这个name;value值是randomUUID生成的唯一一串数字加上该线程id,因为不同JVM中,可能存在线程号相同的情况,所以需要用UUID来区分不同的JVM。
问题:
当本线程1在进入这个if判断后(释放锁之前),突然阻塞(比如full GC,该JVM上全服务堵塞)阻塞时间过长,锁超时释放,这时另一个jvm的线程2获取到锁,然后线程1继续执行释放锁
这样就又会出现同一个用户会有俩个线程在同时运行,所以需要保证判断和释放锁这俩步为一个原子性,同成功或同失败
这样很容易想到事务,redis也有事务,但redis的事务可以保证原子性,但不能保证一致性,而且redis的事务,是最后事务中的步骤同时完成。并不是一步一步的执行,所以只能用乐观锁但不建议用乐观锁,推荐用lua脚本
一个lua脚本中可以编写多条redis命令,确保多条命令的原子性。即实现判断和释放锁一起执行的原子性。
我们需要把这个脚本写在resources目录下
释放锁的lua脚本:
local key=KEYS[1] -- 锁的key
local threadId=ARGV[1] --线程唯一标识
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', key) == threadId) then-- 释放锁 del keyreturn redis.call('del', key)
end
return 0;
if() then end 相当于Java命令中的 if( ) { } 。
KEYS[1]:为需要传入的key值,当需要传入多个key值时,声明KEYS[2]、KEYS[3]...等就行。
ARGV[1]:为需要传入的其他非key的变量,声明方法与key一样。例如:
local key= KEYS[1]
local key2= KEYS[2]
local threadId=ARGV[1]
local releaseTime=ARGV[2]
redis.call(' ', ....)是执行的redis命令,命令中单引号' '中写要执行的redis操作,后面的参数为执行该redis命令所需的参数,例如,get命令,需要知道key值。
然后改写我们的分布式锁:
我们需要先加载我们的lua脚本:
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); //加载的脚本的位置,如果脚本文件在resources目录下,则只需写脚本名称即可。UNLOCK_SCRIPT.setResultType(Long.class); //返回值的类型}
@Overridepublic void unlock() {// 调用lua脚本 ,原来的多行代码变成了现在的单行代码就保证了原子性stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name), //生成单元素的集合,即脚本中的需要的KETS[1]参数ID_PREFIX + Thread.currentThread().getId()); //即脚本中需要的ARVG[1]参数}
完整代码:
package com.hmdp.utils;import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;import java.util.Collections;
import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}private static final String KEY_PREFIX = "lock:";//randomUUID生成的数字会带一个横线,toString(true)方法就是去掉横线//因为不同JVM中,可能存在线程号相同的情况,所以需要用UUID来区分不同的JVMprivate static final String ID_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) {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);//不能直接返回success,因为会有自动拆箱的风险,如果success是null,就会返回true,返回错误数据return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {// 调用lua脚本 ,原来的多行代码变成了现在的单行代码就保证了原子性stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name), //生成单元素的集合,即脚本中的需要的KETS[1]参数ID_PREFIX + Thread.currentThread().getId()); //即脚本中需要的ARVG[1]参数}}
相关文章:
LUA脚本改造redis分布式锁
在redis集群模式下,我们会启动多个tomcat实例,每个tomcat实例都有一个JVM,且不共享。而synchronize锁的作用范围仅仅是当前JVM,所以我们需要一个作用于集群下的锁,也就是分布式锁。(就是不能用JVM自带的锁了…...
web端使用HTML5开发《贪吃蛇》小游戏教程【附源码】
自制游戏列表 1植物大战僵尸自制HTML5游戏《植物大战僵尸》2开心消消乐自制HTML5游戏《开心消消乐》3贪吃蛇自制HTML5游戏《贪吃蛇》4捕鱼达人自制HTML5游戏《捕鱼达人》 一、游戏简介 贪吃蛇是一款经典的电子游戏,最早在1976年由Gremlin公司推出,名为…...
Selenium使用教程-Selenium环境搭建与基础操作
Selenium环境搭建与基础操作 1. 引言:Selenium简介 Selenium,作为自动化测试领域的明星工具,以其强大的跨浏览器测试能力而闻名。它支持多种编程语言(如Java、Python、C#等),允许开发者编写脚本来模拟真…...
1950年-2021年中国历年民航航线里程统计报告
数据为1950年到2021年我国每年的民航航线总里程数据。 2021年,我国定期航班航线总里程为689.78万公里,相比2019年下降了258.44万公里。 数据统计单位为:公里. 数据说明: 2011年起民航航线里程改为定期航班航线里程 我国定期航班…...
前端了解到框架-网络复习
前端 HTML 超文本标记语言 画页面 各种各样的标签组成页面进行展示 桌面创建文本修改后缀即可 <!DOCTYPE html>: 声明文档类型和HTML版本。<html>: 根标签,所有其他标签都包含在内。<head>: 包含了文档的元数据,如字符编码、网页标…...
防火墙——网络环境支持
目录 网络环境支持 防火墙的组网 web连接上防火墙 web管理口 让防火墙接到网络环境中 编辑 管理员用户管理 缺省管理员 接口 配置一个普通接口 创建安全区域 路由模式 透明模式 混合模式 防火墙的安全策略 防火墙转发流程 与传统包过滤的区别 创建安全策略 …...
阅读笔记:明朝那些事儿之拐弯中的帝国
万历皇帝时期内阁首辅: 张居正,申时行,王锡爵,许国,王家屏,赵志皋(给皇帝写辞职信没有回音,自己不告而回家),沈一贯,于慎行,叶向高…...
React基础知识 精简全面 推荐
这篇博文主要对一些刚入门react框架的同学,以及对react基本知识进行巩固的,最后就是精简一下基本知识,以方便自己查看,感谢参考,有问题评论区交流,谢谢。 目录 1.JSX 2.Props 和 State 3.组件生命周期…...
OV SSL证书申请指南
OV SSL证书除了验证域名所有权外还需要验证组织信息,这类证书适用于对公司官网、品牌、安全性等有较高程度要求的企业级用户。具体申请流程如下: 一 、注册账号 注册账号填写230919注册码即可获得大额优惠券和全程一对一技术支持https://www.joyssl.co…...
变色树脂的变色原理?变色树脂在水处理中的应用?
变色树脂是一种具有特殊功能的高分子材料,能够在特定条件下改变其颜色,从而指示环境变化(如pH值、温度、特定离子浓度等)或反应进程。这类树脂通常含有能够响应特定刺激的化学结构,通过化学反应、离子交换、分子构象变…...
16 敏捷开发实践(1)
敏捷方法:是一种从1990年代开始逐渐引起广泛关注的一些新型软件开发方法,是一种应对快速变化的需求的一种软件开发能力。 敏捷开发:是一种以人为核心、迭代、循序渐进的开发方法。 敏捷实践:精益软件开发(LSD&#x…...
如何使用虚拟机如何安装 Kali Linux ?
1.下载虚拟机:https://www.virtualbox.org/wiki/Downloads 选择你的系统版本 2.下载kali linux系统镜像:https://www.kali.org/get-kali/#kali-virtual-machines 全部下载完成后,我们会得到以下文件! 1.压缩Kali Linux压缩包 2.安…...
Yarn UI 时间问题,相差8小时
位置 $HADOOP_HOME/share/hadoop/yarn/hadoop-yarn-common-2.6.1.jar 查看 jar tf hadoop-yarn-common-2.6.1.jar |grep yarn.dt.plugins.js webapps/static/yarn.dt.plugins.js 解压 jar -xvf hadoop-yarn-common-2.6.1.jar webapps/static/yarn.dt.plugins.js inflated: we…...
【JavaWeb项目】——外卖订餐系统之登入、登入后显示餐品信息、用户注册、注销部分
🎼个人主页:【Y小夜】 😎作者简介:一位双非学校的大二学生,编程爱好者, 专注于基础和实战分享,欢迎私信咨询! 🎆入门专栏:🎇【MySQL࿰…...
怎么保护电脑文件夹?文件夹保护方法大盘点
文件夹是管理电脑数据的重要工具,可以有效避免数据混乱。而为了避免文件夹数据泄露,我们需要严格保护文件夹。下面我们就来盘点一下文件夹的保护方法。 文件夹隐藏 隐藏文件夹是一种简单有效的保护方式,通过隐藏文件夹来避免其他人发现&…...
Temporal(时效)模式01
Andy Carlson, Sharon Estepp, Martin Fowler 著,透明 译 抽象 在面向对象设计中,我们不断使用“对象”(object)这个词。对象不仅仅用来表现真实世界中存在的物件,它们也被用来表现那些曾经存在但已经消失了的物件&…...
C语言 -- 动态内存管理
C语言 -- 动态内存管理 1. 为什么要有动态内存分配2. malloc 和 free2.1 malloc2.2 free 3. calloc 和 realloc3.1 calloc3.2 realloc 4. 常见的动态内存的错误4.1 对NULL指针的解引用操作4.2 对动态开辟空间的越界访问4.3 对非动态开辟内存使用free释放4.4 使用free释放一块动…...
docker 篇
简单描述下,有时候真的要熟练,否者上了生产真的不知所措。 背景:有个项目上线了,依赖的项目没有上线,因此需要紧急发布,发现:打包环境有问题,第一、架构不一致,第二、环…...
汽车、能源、烟草、电力行业洞见:TDengine 用户大会亮点荟萃
近年来,随着物联网、车联网、工业互联网等前沿技术的迅猛发展,全球数据量呈指数级增长。作为大数据的一个重要组成部分,时序数据因其在实时监控、预测分析和智能决策中的独特优势,正逐步成为数字化转型的关键要素。尤其在 AI 时代…...
从零开始编写一个Chrome插件:详细教程
个人名片 🎓作者简介:java领域优质创作者 🌐个人主页:码农阿豪 📞工作室:新空间代码工作室(提供各种软件服务) 💌个人邮箱:[2435024119@qq.com] 📱个人微信:15279484656 🌐个人导航网站:www.forff.top 💡座右铭:总有人要赢。为什么不能是我呢? 专栏导…...
关于nvm与node.js
1 安装nvm 安装过程中手动修改 nvm的安装路径, 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解,但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后,通常在该文件中会出现以下配置&…...
前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...
Java多线程实现之Callable接口深度解析
Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...
今日科技热点速览
🔥 今日科技热点速览 🎮 任天堂Switch 2 正式发售 任天堂新一代游戏主机 Switch 2 今日正式上线发售,主打更强图形性能与沉浸式体验,支持多模态交互,受到全球玩家热捧 。 🤖 人工智能持续突破 DeepSeek-R1&…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)
参考官方文档:https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java(供 Kotlin 使用) 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...
AGain DB和倍数增益的关系
我在设置一款索尼CMOS芯片时,Again增益0db变化为6DB,画面的变化只有2倍DN的增益,比如10变为20。 这与dB和线性增益的关系以及传感器处理流程有关。以下是具体原因分析: 1. dB与线性增益的换算关系 6dB对应的理论线性增益应为&…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
