基于Redis实现的分布式锁
基于Redis实现的分布式锁
- 什么是分布式锁
- 分布式锁主流的实现方案
- Redis分布式锁
- Redis分布式锁的Java代码体现
- 优化一:使用UUID防止误删除
- 优化二:LUA保证删除原子性
什么是分布式锁
- 单体单机部署中可以为一个操作加上锁,这样其他操作就会等待锁释放才能操作
- 但是随业务的不断发展,单机应用常会被分布式集群系统所取代
在分布式集群中存在多台机器,如果给某台机器上加普通的锁,此锁只针对当前机器有效(因为jvm不能跨系统进行锁的控制),因此一种对所有机器都有效的锁应运而生,此即为分布式锁。
即随业务不断发展,需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁机制要解决的问题!
分布式锁主流的实现方案
分布式锁主流实现方案:
-
- 基于数据库实现分布式锁
-
- 基于缓存(Redis等)
-
- 基于Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
-
- 性能:redis最高
-
- 可靠性:zookeeper最高
这里,我们就基于redis实现分布式锁进行讲解。
Redis分布式锁
Redis中的setex命令就是针对分布式锁操作的一个命令。
回顾setex命令:(setnx中的“nx”表示“not exist:不存在”)
- setnx key value:只有在key 不存在时,才能设置 key 的值。如下图:

使用setnx命令相当于加了一把锁,只有当锁释放的时候此操作才可以继续进行。
思考此锁如何释放?
①首先我们想到的就是del命令删除数据,删除后锁释放,可以再次setnx。 如下图:

但此方案有缺陷。如果锁一直不释放,其他操作就只能等待。所以这样设计不合理!
②于是我们想到expire设置过期时间自动释放锁。如下图:

setnx上锁之后,设置过期时间(通过ttl命令可以查看key剩余多久过期)。过期之后,锁释放。即可再次进行setnx操作。
但上述方式依旧存在问题。
我们提倡的是原子操作,以上setnx操作和使用expire设置过期时间分了两步进行。如果setnx操作执行之后,还没有设置过期时间服务器就断电挂掉了,就不能设置过期时间。针对上锁之后出现异常的情况,引入第三种情况。
③上锁的同时设置上过期时间即可保证原子性操作
(ex表示expire:过期)

Redis分布式锁的Java代码体现
接下来我们通过编写Java代码用一个简单的例子进行演示:
①首先,创建一个SpringBoot空项目,将Redis整合进此项目
②存入redis一条数据,可以把此步骤看作一些具体业务

③Controller新增接口中写入如下代码
@GetMapping("testLock")public void testLock(){//1,获取锁,setnxBoolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111"); //此处相当于setnx的同时设置过期时间为3s//2,获取锁成功,则从Redis中查询num的值if(lock){Object value = redisTemplate.opsForValue().get("num");//判断num为空则直接returnif(StringUtils.isEmpty(value)){return;}//有值就转成成intint num = Integer.parseInt(value+"");//把redis的num加1redisTemplate.opsForValue().set("num", ++num);//释放锁,delredisTemplate.delete("lock");}else{//3获取锁失败,则每隔0.1秒再获取try {Thread.sleep(100);testLock();} catch (InterruptedException e) {e.printStackTrace();}}}
优化一:使用UUID防止误删除
以上的代码还是存在问题的,可能会释放掉其他服务器的锁(即锁释放错的问题)。
异常场景:
两个操作分别记为a、b,设置锁在10秒内过期。
如果a先上锁,在a执行业务操作过程中,其服务器突然卡顿超过10秒。此时分布式锁就会过期而自动释放(此时a的业务操作还未结束)。b拿到这把锁,b拿到之后会先上锁并执行业务操作,b在业务操作过程中,a的服务器卡顿结束,就需要继续完成a的业务操作,并手动释放锁(但a的锁已经过期自动释放了,此时手动释放锁就会释放掉b的锁),显然这是存在问题的。
解决上述问题的一个很好的方法是使用uuid防止误删除。
- 上锁的时候 set key uuid nx ex 10,上锁时设置value为一个唯一的随机值
- 利用uuid的唯一性,表示不同的操作
- 释放锁的时候补充
判断当前uuid和要释放锁的uuid是否一致,一致则释放,否则不释放
代码优化如下:
@GetMapping("testLock")public void testLock(){//1,生成uuidString uuid = UUID.randomUUID().toString();//2,获取锁,setnx (设置value为uuid)Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,10,TimeUnit.SECONDS); //3,获取锁成功,则从Redis中查询num的值if(lock){Object value = redisTemplate.opsForValue().get("num");//判断num为空则直接returnif(StringUtils.isEmpty(value)){return;}//有值就转成成intint num = Integer.parseInt(value+"");//把redis的num加1redisTemplate.opsForValue().set("num", ++num);//释放锁,del (释放之前判断当前的uuid是否一致,一致则释放)String lock1 = (String) redisTemplate.opsForValue().get("lock");if (lock1.equals(uuid)) {redisTemplate.delete("lock");}}else{//3,获取锁失败,则每隔0.1秒再获取try {Thread.sleep(100);testLock();} catch (InterruptedException e) {e.printStackTrace();}}}
优化二:LUA保证删除原子性
上一个环节,我们通过uuid解决了误删除问题。但优化后的代码依然存在问题:缺乏原子性。
异常场景:
两个操作分别记为a、b,设置锁在10秒内过期。
如果a先上锁,a执行完成业务操作需要释放锁,假设判断发现uuid一致,此时即将进行释放锁。但服务器此时突然卡顿超过10秒。此时分布式锁就会过期而自动释放(
此时a的锁还未释放)。b拿到这把锁,b拿到之后会先上锁并执行业务操作,b在业务操作过程中,a的服务器卡顿结束,就需要继续释放锁(但a的锁已经过期自动释放了,此时手动释放锁就会释放掉b的锁),显然这是存在原子性问题的。
解决上述问题的一个很好的方法是使用lua脚本(特点:支持原子性操作)。
将复杂的或多步骤的Redis操作,写为一个脚本,一次性提交给Redis执行,减少反复连接Redis,提高性能。
LUA脚本类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些类似Redis事务性的操作。
注意:LUA脚本只有Redis 2.6以上版本可用。
@GetMapping("testLockLua")public void testLockLua() {//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中String uuid = UUID.randomUUID().toString();//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!String skuId = "25"; // 访问skuId 为25号的商品 100008348542String locKey = "lock:" + skuId; // 锁住的是每个商品的数据// 3 获取锁Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 10, TimeUnit.SECONDS);// 第一种: lock 与过期时间中间不写任何的代码。// 如果trueif (lock) {// 执行的业务逻辑开始// 获取缓存中的num 数据Object value = redisTemplate.opsForValue().get("num");// 如果是空直接返回if (StringUtils.isEmpty(value)) {return;}// 不是空 int num = Integer.parseInt(value + "");// 使num 每次+1 放入缓存redisTemplate.opsForValue().set("num", String.valueOf(++num));/*使用lua脚本来释放锁*/// 定义lua 脚本String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";// 使用redis执行lua执行DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(script);// 设置一下返回值类型 为Long// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,// 那么返回字符串与0 会有发生错误。redisScript.setResultType(Long.class);// 第一个要是script 脚本 ,第二个需要判断的key,第三个是value值。redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);} else {// 其他线程等待try {// 睡眠Thread.sleep(1000);// 睡醒了之后,调用方法。testLockLua();} catch (InterruptedException e) {e.printStackTrace();}}}
总结:
为确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能释放掉别人加的锁。
- 加锁和解锁必须具有原子性。
相关文章:
基于Redis实现的分布式锁
基于Redis实现的分布式锁什么是分布式锁分布式锁主流的实现方案Redis分布式锁Redis分布式锁的Java代码体现优化一:使用UUID防止误删除优化二:LUA保证删除原子性什么是分布式锁 单体单机部署中可以为一个操作加上锁,这样其他操作就会等待锁释…...
2023年,还找算法岗工作吗?
点击下方卡片,关注“CVer”公众号AI/CV重磅干货,第一时间送达2023年春招(补招)已经大规模启动了!距离2023年暑期实习不到2个月!距离2024届校招提前批不到4个月!距离2024届秋招正式批不到6个月&a…...
正点原子ARM裸机开发篇
裸机就是手动的操作硬件来实现驱动设备,后面会有驱动框架不需要这么麻烦 第八章 汇编 LED 灯实验 核心过程 通过汇编语言来控制硬件(驱动程序) 代码流程 1、使能 GPIO1 时钟 GPIO1 的时钟由 CCM_CCGR1 的 bit27 和 bit26 这两个位控制&…...
20222023华为OD机试 - 压缩报文还原(JS)
压缩报文还原 题目 为了提升数据传输的效率,会对传输的报文进行压缩处理。 输入一个压缩后的报文,请返回它解压后的原始报文。 压缩规则:n[str],表示方括号内部的 str 正好重复 n 次。 注意 n 为正整数(0 < n <= 100),str只包含小写英文字母,不考虑异常情况。 …...
SheetJS的部分操作
成文时间:2023年2月18日 使用版本:"xlsx": "^0.18.5" 碎碎念: 有错请指正。 这个库自说自话升级到0.19。旧版的文档我记得当时是直接写在github的README上。 我不太会使用github,现在我不知道去哪里可以找到…...
pytest总结
这里写目录标题一、pytest的命名规则二、界面化配置符合命名规则的方法前面会有运行标记三、pytest的用例结构三部分组成四、pytest的用例断言断言写法:五、pytest测试框架结构六、pytest参数化用例1、pytest参数化实现方式2、单参数:每一条测试数据都会…...
CNI 网络分析(九)Calico IPIP
文章目录环境流量分析Pod 间Node 到 PodPod 到 serviceNode 到 serviceNetworkPolicy理清和观测网络流量环境 可以看到,在宿主机上有到每个 pod IP 的路由指向 veth 设备 到对端节点网段的路由 指向 tunl0 下一跳 ens10 的 ip 有到本节点网段 第一个 ip 即 tunl0 的…...
分布式任务调度(XXL-JOB)
什么是分布式任务调度? 任务调度顾名思义,就是对任务的调度,它是指系统为了完成特定业务,基于给定时间点,给定时间间隔或者给定执行次数自动执行任务。通常任务调度的程序是集成在应用中的,比如:…...
Django框架之模型视图--Session
Session 1 启用Session Django项目默认启用Session。 可以在settings.py文件中查看,如图所示 如需禁用session,将上图中的session中间件注释掉即可。 2 存储方式 在settings.py文件中,可以设置session数据的存储方式,可以保存…...
二极管的“几种”应用
不知大家平时有没有留意,二极管的应用范围是非常广的,下面我们来看看我想到几种应用,也可以加深对电路设计的认识: A,特性应用: 由于二极管的种类非常之多,这里这个大类简单罗列下:…...
github上传本地文件详细过程
repository 也就是俗称的仓库 声明:后续操作基于win10系统 前提:有一个github账号、电脑安装了git(官方安装地址) 目的: 把图中pdf文件上传到github上的个人仓库中 效果: 温馨提示: git中复制: ctrl insert…...
常用聚类算法分析
1. 什么是聚类 1.1. 聚类的定义 聚类(Clustering)是按照某个特定标准(如距离)把一个数据集分割成不同的类或簇,使得同一个簇内的数据对象的相似性尽可能大,同时不在同一个簇中的数据对象的差异性也尽可能地大。也即聚类后同一类的数据尽可能聚集到一起…...
OSG三维渲染引擎编程学习之五十八:“第五章:OSG场景渲染” 之 “5.16 简单光源”
目录 第五章 OSG场景渲染 5.16 简单光源 5.16.1 场景中使用光源 5.16.2 简单光源示例 第五章 OSG场景渲染 OSG存在场景树和渲染树,“场景数”的构建在第三章“OSG场景组...
80211无线网络架构
无线网络架构物理组件BSS(Basic Service Set)基本服务集BSSID(BSS Identification)ssid(Service Set Identification)ESS(Extended Service Set)扩展服务集物理组件 无线网络包含四…...
基于springboot+vue的便利店库存管理系统
基于springbootvue的便利店库存管理系统 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取项目下载方式🍅 一、项目背景…...
3|物联网控制|计算机控制-刘川来胡乃平版|第1章:绪论|青岛科技大学课堂笔记|U1 ppt
目录绪论(2学时)常用仪表设备(3学时)计算机总线技术(4学时)过程通道与人机接口(6学时)数据处理与控制策略(6学时)网络与通讯技术(3学时࿰…...
js打印本地pdf(使用HttpPrinter打印插件)
js打印本地pdf(使用HttpPrinter打印插件)第一步:启动HttpPrinter打印插件第二步:用浏览器打开示例文件\调用示例\websocket协议示例\html\打印pdf.html输入pdf地址 点击 “下载并打印pdf文件”按钮,就可以静默打印了。…...
华为OD机试 - 双十一(Python) | 机试题算法思路 【2023】
最近更新的博客 【新解法】华为OD机试 - 关联子串 | 备考思路,刷题要点,答疑,od Base 提供【新解法】华为OD机试 - 停车场最大距离 | 备考思路,刷题要点,答疑,od Base 提供【新解法】华为OD机试 - 任务调度 | 备考思路,刷题要点,答疑,od Base 提供【新解法】华为OD机试…...
2020年UML 秋季期末测试题
1.UML的全称是(B )。A.Unified Making LanguageB.Unified Modeling LanguageC.Unified Meodem languageD.Unify Modeling Language2.UML主要应用于( C)。A.基于螺旋模型的结构化开发方法B.基于数据的数据流开发方法C.基于对象的面…...
SpringCloud - Ribbon负载均衡
目录 负载均衡流程 负载均衡策略 Ribbon加载策略 负载均衡流程 Ribbon将http://userservice/user/1请求拦截下来,帮忙找到真实地址http://localhost:8081LoadBalancerInterceptor类对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表&…...
什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...
【C++特殊工具与技术】优化内存分配(一):C++中的内存分配
目录 一、C 内存的基本概念 1.1 内存的物理与逻辑结构 1.2 C 程序的内存区域划分 二、栈内存分配 2.1 栈内存的特点 2.2 栈内存分配示例 三、堆内存分配 3.1 new和delete操作符 4.2 内存泄漏与悬空指针问题 4.3 new和delete的重载 四、智能指针…...
Spring Boot + MyBatis 集成支付宝支付流程
Spring Boot MyBatis 集成支付宝支付流程 核心流程 商户系统生成订单调用支付宝创建预支付订单用户跳转支付宝完成支付支付宝异步通知支付结果商户处理支付结果更新订单状态支付宝同步跳转回商户页面 代码实现示例(电脑网站支付) 1. 添加依赖 <!…...
大数据治理的常见方式
大数据治理的常见方式 大数据治理是确保数据质量、安全性和可用性的系统性方法,以下是几种常见的治理方式: 1. 数据质量管理 核心方法: 数据校验:建立数据校验规则(格式、范围、一致性等)数据清洗&…...
Java后端检查空条件查询
通过抛出运行异常:throw new RuntimeException("请输入查询条件!");BranchWarehouseServiceImpl.java // 查询试剂交易(入库/出库)记录Overridepublic List<BranchWarehouseTransactions> queryForReagent(Branch…...
