Redis 实现分布式锁时需要考虑的问题
引言
分布式系统中的多个节点经常需要对共享资源进行并发访问,若没有有效的协调机制,可能会导致数据竞争、资源冲突等问题。分布式锁应运而生,它是一种保证在分布式环境中多个节点可以安全地访问共享资源的机制。而在Redis中,使用它的原子操作和高性能的特点,已经成为实现分布式锁的一种常见方案。
然而,使用Redis实现分布式锁时并不是一个简单的过程,开发者需要考虑到多种问题,如锁的竞争、锁的释放、超时管理、网络分区等。本文将详细探讨这些问题,并提供解决方案和代码实例,帮助开发者正确且安全地使用Redis实现分布式锁。
第一部分:什么是分布式锁?
1.1 分布式锁的定义
分布式锁是一种协调机制,用于确保在分布式系统中多个进程或线程可以安全地访问共享资源。通过分布式锁,可以确保在同一时间只有一个节点可以对某个资源进行操作,从而避免数据竞争或资源冲突。
1.2 分布式锁的特性
- 互斥性:同一时刻只能有一个客户端持有锁。
- 锁超时:客户端持有锁的时间不能无限长,必须设置锁的自动释放机制,以防止死锁。
- 可重入性:在某些场景下,允许同一个客户端多次获取锁,而不会导致锁定失败。
- 容错性:即使某些节点发生故障,锁机制仍然能保证系统的正常运行。
1.3 分布式锁的应用场景
- 电商系统中的库存扣减:当多个用户同时购买同一件商品时,需要通过分布式锁确保库存的正确扣减。
- 订单系统中的唯一订单号生成:确保在高并发场景下,不会生成重复的订单号。
- 定时任务调度:确保同一时刻,只有一个节点在执行定时任务。
第二部分:Redis 实现分布式锁的基本原理
2.1 Redis 的原子性操作
Redis 支持多种原子性操作,这使得它非常适合用来实现分布式锁。SETNX
(set if not exists)是其中一种常见的原子操作。它确保只有在键不存在的情况下,才会成功设置键。
// 使用 SETNX 实现分布式锁
boolean acquireLock(Jedis jedis, String lockKey, String clientId, int expireTime) {String result = jedis.set(lockKey, clientId, SetParams.setParams().nx().px(expireTime));return "OK".equals(result);
}
在上面的代码中,SETNX
实现了如下逻辑:
- 如果锁键不存在,则设置锁,并返回“OK”,表示获取锁成功。
- 如果锁键已存在,则返回空值,表示获取锁失败。
2.2 锁的自动释放机制
为了避免客户端因某些原因没有主动释放锁(如宕机或网络故障)导致的死锁问题,通常在获取锁时设置锁的超时时间。这可以通过Redis的PX
参数实现,它表示锁的自动过期时间。
jedis.set("lockKey", "client1", SetParams.setParams().nx().px(5000)); // 锁自动在5000毫秒后过期
2.3 Redis 分布式锁的基本流程
- 客户端使用
SETNX
命令尝试获取锁。 - 如果获取锁成功,客户端可以进行资源操作。
- 客户端操作完成后,通过
DEL
命令释放锁。 - 如果客户端在操作期间宕机,锁会在指定的超时时间后自动释放,防止死锁。
第三部分:Redis 实现分布式锁的常见问题
3.1 锁的释放问题
问题:客户端执行完业务逻辑后需要释放锁,但直接调用DEL
命令可能会出现误删其他客户端的锁的情况。具体来说,客户端A获取锁后,如果由于某些原因执行时间过长,锁自动过期释放,而客户端B获取了该锁。如果客户端A继续执行,并调用DEL
释放锁,那么就可能误删了客户端B的锁。
解决方案:为了避免误删其他客户端的锁,应该在获取锁时保存客户端ID,释放锁时首先检查当前锁的持有者是否为自己。如果是,则删除锁,否则不做操作。
代码示例:释放锁时验证持有者
boolean releaseLock(Jedis jedis, String lockKey, String clientId) {String lockValue = jedis.get(lockKey);if (clientId.equals(lockValue)) {jedis.del(lockKey); // 只有当前客户端持有锁,才释放锁return true;}return false;
}
为了确保操作的原子性,最好使用Redis的Lua脚本来完成此逻辑:
-- Lua 脚本:确保释放锁的原子性
if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])
elsereturn 0
end
使用Jedis调用Lua脚本的示例:
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(clientId));
3.2 锁超时问题
问题:设置锁的超时时间可以防止死锁问题,但如果客户端的业务逻辑执行时间超过了锁的过期时间,则会导致锁在业务逻辑尚未执行完毕时被Redis自动释放,其他客户端可能会在锁释放后获得该锁,从而导致多个客户端同时操作共享资源,进而引发并发问题。
解决方案1:合理设置超时时间
需要根据业务场景估计业务逻辑的最大执行时间,并合理设置锁的超时时间。如果无法准确预测执行时间,可以通过定时刷新锁的方式延长锁的持有时间。
解决方案2:续约机制(Lock Renewal)
在业务逻辑执行过程中,定期检查锁的剩余时间,并在锁即将到期时,自动延长锁的有效期。这可以通过一个后台线程来定期刷新锁的过期时间。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);void acquireLockWithRenewal(Jedis jedis, String lockKey, String clientId, int expireTime) {// 获取锁boolean acquired = acquireLock(jedis, lockKey, clientId, expireTime);if (acquired) {// 定期续约,确保锁不会自动过期scheduler.scheduleAtFixedRate(() -> {if (clientId.equals(jedis.get(lockKey))) {jedis.pexpire(lockKey, expireTime);}}, expireTime / 2, expireTime / 2, TimeUnit.MILLISECONDS);}
}
3.3 Redis 宕机问题
问题:如果Redis节点宕机或不可用,所有锁信息都会丢失,导致系统中可能出现多个客户端同时操作共享资源的情况,无法保证分布式锁的互斥性。
解决方案:主从复制与哨兵模式
为了解决Redis宕机导致的锁丢失问题,可以使用Redis的高可用架构,如主从复制(Replication)或哨兵模式(Sentinel)。通过搭建高可用Redis集群,确保即使某个节点宕机,系统也能够自动切换到备份节点,继续提供分布式锁服务。
3.4 网络分区问题
问题:在分布式环境中,网络分区(网络隔离)可能会导致部分客户端与Redis无法正常通信。在这种情况下,某些客户端可能误认为自己已经成功获取锁,而实际上其他客户端也可能同时获取了相同的锁,从而破坏锁的互斥性。
解决方案:基于Redlock算法的分布式锁
为了在网络分区下仍然保证分布式锁的可靠性,可以使用Redis官方提出的Redlock算法。Redlock通过在多个Redis实例上同时获取锁,并根据过半实例的成功情况来决定锁的有效性,从而在网络分区或部分节点宕机时,依然能够保证分布式锁的可靠性。
Redlock算法的基本步骤:
- 客户端向N个独立的Redis节点请求获取锁(推荐N=5)。
- 客户端为每个Redis节点设置相同的锁超时时间,并确保获取锁的时间窗口较短(小于锁的超时时间)。
- 如果客户端在大多数
(即超过N/2+1)Redis节点上成功获取锁,则认为获取锁成功。
4. 如果获取锁失败,客户端需要向所有已成功加锁的节点发送释放锁请求。
Redlock算法的实现示意图
+-----------+ +-----------+ +-----------+
| Redis1 | | Redis2 | | Redis3 |
+-----------+ +-----------+ +-----------+| | |v v v
获取锁成功 获取锁成功 获取锁失败
Redlock算法的Java实现可以使用官方提供的Redisson库。
第四部分:Redis 分布式锁的性能优化
4.1 减少锁的持有时间
在设计分布式锁时,应该尽量减少锁的持有时间。锁的持有时间越短,系统的并发度越高。因此,业务逻辑的执行应该尽量简化,将不需要加锁的操作移出锁定区。
4.2 限制锁的粒度
通过控制锁的粒度,可以减少锁的争用。锁的粒度越小,被锁定的资源越少,竞争的客户端越少。例如,在处理商品库存时,可以为每个商品设置独立的分布式锁,而不是为整个库存设置一个全局锁。
4.3 批量操作与分布式锁结合
在某些业务场景下,可以通过批量操作来减少锁的获取频率。例如,在电商系统中,用户下单时可以先将订单信息写入队列或缓存,再通过批量任务处理队列中的订单,减少锁的竞争。
第五部分:Redis 分布式锁的完整示例
以下是一个完整的Redis分布式锁的示例,结合了锁的获取、释放和续约机制。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;public class RedisDistributedLock {private Jedis jedis;private String lockKey;private String clientId;private int expireTime;private ScheduledExecutorService scheduler;public RedisDistributedLock(Jedis jedis, String lockKey, int expireTime) {this.jedis = jedis;this.lockKey = lockKey;this.clientId = UUID.randomUUID().toString();this.expireTime = expireTime;this.scheduler = Executors.newScheduledThreadPool(1);}// 获取锁public boolean acquireLock() {String result = jedis.set(lockKey, clientId, SetParams.setParams().nx().px(expireTime));if ("OK".equals(result)) {// 开启定时任务,自动续约锁scheduler.scheduleAtFixedRate(() -> renewLock(), expireTime / 2, expireTime / 2, TimeUnit.MILLISECONDS);return true;}return false;}// 续约锁private void renewLock() {if (clientId.equals(jedis.get(lockKey))) {jedis.pexpire(lockKey, expireTime);}}// 释放锁public boolean releaseLock() {String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";Object result = jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(clientId));return "1".equals(result.toString());}public static void main(String[] args) throws InterruptedException {Jedis jedis = new Jedis("localhost", 6379);RedisDistributedLock lock = new RedisDistributedLock(jedis, "myLock", 5000);// 尝试获取锁if (lock.acquireLock()) {System.out.println("获取锁成功!");// 模拟业务操作Thread.sleep(3000);// 释放锁if (lock.releaseLock()) {System.out.println("释放锁成功!");}} else {System.out.println("获取锁失败!");}jedis.close();}
}
代码解释:
acquireLock()
方法用于获取锁,锁的有效期通过px(expireTime)
设置,获取成功后启动一个定时任务用于锁的续约。releaseLock()
方法使用Lua脚本确保只有持有锁的客户端才能释放锁,避免误删其他客户端的锁。- 通过定时任务
renewLock()
来定期延长锁的有效期,确保锁不会在业务操作过程中过期。
第六部分:总结
Redis作为一种高性能的内存型数据库,因其对原子操作的支持和极高的吞吐量,被广泛应用于分布式锁的实现中。然而,使用Redis实现分布式锁时,开发者需要考虑多个问题,包括锁的获取与释放、超时处理、宕机容错、网络分区等。通过合理的设计和优化,可以保证Redis分布式锁在高并发环境下的稳定性和安全性。
本文详细分析了Redis分布式锁的常见问题及其解决方案,并结合代码示例讲解了如何正确实现锁的获取、释放、续约等机制。开发者可以根据实际业务需求选择合适的解决方案,并结合Redis的高可用架构,确保系统在分布式环境下的稳定运行。
通过合理地使用Redis分布式锁,我们能够在复杂的分布式系统中,确保共享资源的安全访问,进而提高系统的稳定性和性能。
相关文章:

Redis 实现分布式锁时需要考虑的问题
引言 分布式系统中的多个节点经常需要对共享资源进行并发访问,若没有有效的协调机制,可能会导致数据竞争、资源冲突等问题。分布式锁应运而生,它是一种保证在分布式环境中多个节点可以安全地访问共享资源的机制。而在Redis中,使用…...

百年极限论一直存在百年糊涂话:有正数小于所有正数
百年极限论一直存在百年糊涂话:有正数小于所有(任何、任意)正数。 “对于每个大于0的ε[ε>0],都有非0距离数小于ε”显然是病句:有正数小于每个(所有)正数ε。其中任意(任何&am…...

红日靶场1学习笔记
一、准备工作 1、靶场搭建 靶场地址 靶场描述 靶场拓扑图 其他相关靶场搭建详情见靶场地址相关说明 2、靶场相关主机信息 后续打靶场的过程中,如果不是短时间内完成,可能ip会有变化 主机ip密码角色win7192.168.122.131hongrisec2019!边界服务器win…...

【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
文章目录 从零实现 list 容器:细粒度剖析与代码实现前言1. list 的核心数据结构1.1节点结构分析: 2. 迭代器设计与实现2.1 为什么 list 需要迭代器?2.2 实现一个简单的迭代器2.2.1 迭代器代码实现:2.2.2 解释: 2.3 测试…...

【C#生态园】打造现代化跨平台应用:深度解析.NET桌面应用工具
选择最适合你的.NET UI框架:全面解析六种热门选择 前言 在现代软件开发中,选择合适的桌面应用框架和UI库对于开发人员来说至关重要。本文将介绍几种流行的.NET桌面应用框架和UI库,包括Eto.Forms、Avalonia、ReactiveUI、MahApps.Metro、Mat…...

第二十一章 (动态内存管理)
1. 为什么要有动态内存分配 2. malloc和free 3. calloc和realloc 4. 常⻅的动态内存的错误 5. 动态内存经典笔试题分析 6. 总结C/C中程序内存区域划分 1.为什么要有动态内存管理 我们目前已经掌握的内存开辟方式有 int main() {int num 0; //开辟4个字节int arr[10] …...

机器学习框架总结
机器学习框架是用于构建、训练、评估和部署机器学习模型的工具和库的集合。它们简化了模型开发过程,并提供了预构建的功能、优化的计算性能和对深度学习、监督学习、无监督学习等技术的支持。下面是一些主要的机器学习框架的详细介绍: 1. TensorFlow 1…...

docker pull 超时的问题如何解决
docker不能使用,使用之前的阿里云镜像失败。。。 搜了各种解决方法,感谢B站UP主 <iframe src"//player.bilibili.com/player.html?isOutsidetrue&aid113173361331402&bvidBV1KstBeEEQR&cid25942297878&p1" scrolling"…...

【数学分析笔记】第4章第3节 导数四则运算和反函数求导法则(2)
4. 微分 4.3 导数四则运算与反函数求导法则 双曲正弦函数 sh x e x − e − x 2 \sh x\frac{e^x-e^{-x}}{2} shx2ex−e−x 双曲余弦函数 ch x e x e − x 2 \ch x\frac{e^xe^{-x}}{2} chx2exe−x ch 2 x − sh 2 x 1 \ch^2 x-\sh^2 x1 ch2x−sh2x1 ( e…...

【2024】基于mysqldump的数据备份与恢复
基于mysqldump备份与恢复 mysqldump是一个用于备份 MySQL 数据库的实用工具。 它可以将数据库的结构(如数据库、表、视图、存储过程等的定义)和数据(表中的记录)导出为文本文件,这些文本文件可以包含 SQL 语句&#…...

家用无线路由器配置
一.首先进行线路连接。如下图:"光猫LAN口"—网线—"路由器WAN口"。 注意:家用光纤宽带一般选择使用200兆宽带到1000兆,如果网速不达标请查看路由器是否是千兆路由器。千兆路由器通常是双频的,支持两个信号一个…...

模拟算法(4)_外观数列
个人主页:C忠实粉丝 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C忠实粉丝 原创 模拟算法(4)_外观数列 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记,欢迎大家在评论区交流讨论💌 目录 1. 题目链…...

vsomeip用到的socket
概述: vsomeip用到的socket的代码全部都在implementation\endpoints目录下面,主要分布在下面六个endpoint类中: local_client_endpoint_impl // 本地客户端socket(UDS Socket或者127.0.0.1的socket)local_server…...

MFC有三个选项:MFC ActiveX控件、MFC应用程序、MFC DLL,如何选择?
深耕AI:互联网行业 算法研发工程师 目录 MFC ActiveX 控件 控件的类型 标准控件 自定义控件 ActiveX控件 MFC ActiveX控件 标准/自定义控件 MFC ActiveX控件分类 3种MFC如何选择? MFC ActiveX控件 MFC 应用程序 MFC DLL 总结 举例说明…...

边缘概率 | 条件概率
关于什么是边缘概率分布和条件概率分布,在理论上,我自己也还没有理解,那么现在就根据我学习到的理解方式来记录一下,有错误指出,请大家指正!!! 例如,一个箱子里有十个乒乓…...

深入浅出:现代JavaScript开发者必知必会的Web性能优化技巧
亲爱的读者们,欢迎来到本期博客。今天,我们将深入探讨JavaScript开发者在日常工作中如何提升Web性能。在快节奏的Web开发世界中,性能优化至关重要。本文将分享一些实用技巧,帮助你构建快速、高效的Web应用。 1. 使用CDN加速资源加…...

【S32K3 RTD LLD篇5】K344 ADC SW+HW trigger
【S32K3 RTD LLD篇5】K344 ADC SWHW trigger 一,文档简介二,ADC SW HW 触发2.1 软硬件平台2.2 SWADC 软件触发2.3 SWBCTUADC 软件BCTU触发2.4 PITTRIGMUXADC 硬件PIT TRIGUMX触发2.5 EMIOSBCTUHWADC硬件EMIOS BCTU触发2.6 EMIOSBCTUHW LISTADC硬件EMIOS …...

TransFormer 视频笔记
TransFormer BasicsAttention单头注意力 single head attentionQ: query 查寻矩阵 128*12288K key matrix 128*12288SoftMax 归一 Value matrix 12288*12288 MLP Bas…...

前端的混合全栈之路Meteor篇(三):发布订阅示例代码及如何将Meteor的响应数据映射到vue3的reactive系统
Meteor 3.0 是一个功能强大的全栈 JavaScript 框架,特别适合实时应用程序的开发。它的核心机制之一就包括发布-订阅(Publish-Subscribe)模型,它允许服务器端发布数据,客户端订阅并实时更新。本文将介绍如何在 Meteor 3…...

自动驾驶系列—颠覆未来驾驶:深入解析自动驾驶线控转向系统技术
🌟🌟 欢迎来到我的技术小筑,一个专为技术探索者打造的交流空间。在这里,我们不仅分享代码的智慧,还探讨技术的深度与广度。无论您是资深开发者还是技术新手,这里都有一片属于您的天空。让我们在知识的海洋中…...

Webstorm 中对 Node.js 后端项目进行断点调试
首先,肯定需要有一个启动服务器的命令脚本。 然后,写一个 debug 的配置: 然后,debug 模式 启动项目和 启动调试服务: 最后,发送请求,即可调试: 这几个关键按钮含义: 重启…...

VUE前后端分离毕业设计题目项目有哪些,VUE程序开发常见毕业论文设计推荐
目录 0 为什么选择Vue.js 1 Vue.js 的主要特点 2 前后端分离毕业设计项目推荐 3 后端推荐 4 总结 0 为什么选择Vue.js 使用Vue.js开发计算机毕业设计是一个很好的选择,因为它不仅具有现代前端框架的所有优点,还能让你专注于构建高性能、高可用性的W…...

一、Spring Boot集成Spring Security之自动装配
Spring Boot集成Spring Security之自动装配介绍 一、实现功能及软件版本说明二、创建Spring Boot项目三、查看自动装配配置类四、自动装配配置类之SecurityAutoConfiguration1、SecurityAutoConfiguration部分源码2、主要作用3、SpringBootWebSecurityConfiguration3.1、Spring…...

计数相关的题 Python 力扣
2284. 最多单词数的发件人 给你一个聊天记录,共包含 n 条信息。给你两个字符串数组 messages 和 senders ,其中 messages[i] 是 senders[i] 发出的一条 信息 。 一条 信息 是若干用单个空格连接的 单词 ,信息开头和结尾不会有多余空格。发件…...

Express内置的中间件(express.json和express.urlencoded)格式的请求体数据
目录 Express内置的中间件 express.json 中间件的使用 express.urlencoded 中间件的使用 express.urlencoded([options]) 解析req.body的兼容写法 Express内置的中间件 自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 …...

cmakelist加载Qt模块
Qt编程中,cmakelist会自动添加Core,Gui,Widgets模块,有时需要添加新的Qt的模块。在命令find_package中搜索要新增的模块,在命令target_link_libraries中添加要新增的模块。 比如要使用QUiLoader类,要增加对…...

8-2.Android 任务之 CountDownTimer 编码模板(开启计时器、取消计时器)
一、CountDownTimer 1、概述 CountDownTimer 是 Android 中一个用于执行定时操作的类 CountDownTimer 主要应用于在指定时间段内完成某项任务,或者每隔一段时间触发某项任务 2、使用步骤 创建 CountDownTimer:创建 CountDownTimer 就是创建它的匿名…...

Servlet的生命周期及用户提交表单页面的实现(实验报告)
一、实验目的、要求 1. 掌握Servlet的定义,即Servlet是运行在服务器端的Java程序,用于扩展服务器的功能。 2. 学习和掌握在开发环境中搭建Servlet应用所需的工具,如Tomcat服务器、IDEA等。 二、实验内容 根据本章所学知识,实验…...

【Router】路由功能之IP过滤(IP Filter)功能(基于端口)介绍及实现
IP过滤(IP Filter) IP Filter是一种通过对网络数据包中的 IP 地址进行分析和筛选,以实现对网络流量的控制和管理的技术。 IP过滤(IP Filter)作用 安全防护 可以阻止来自特定 IP 地址或 IP 地址范围的恶意攻击、非法访问等,增强网络的安全性。 流量管理 根据不同的 IP …...

数据结构_2.2、顺序表插入删除查找
1、线性表的顺序存储表示定义: 线性表:是具有相同数据类型的n (n≥0)个数据元素的有限序列 顺序表:用顺序存储的方式实现线性表 顺序存储:把逻辑上相邻的元素存储在物理 位置上也相邻的存储单元中&#…...