iis 访问网站需要进行身份验证,文明网站的建设与管理几点思考,在哪里找个人做网站的,上海想找人设计网站文章目录 引言一、Redis的两种原子操作1.1 Redis 的原子性1.2 单命令1.3 Lua 脚本1.4 对比单命令与 Lua 脚本 二、Redis 实现分布式锁2.1 分布式锁的概念与需求2.1.1 什么是分布式锁#xff1f;2.1.2 分布式锁的常见应用场景 2.2 基于 Redis 的分布式锁实现2.2.1 锁的获取与释… 文章目录 引言一、Redis的两种原子操作1.1 Redis 的原子性1.2 单命令1.3 Lua 脚本1.4 对比单命令与 Lua 脚本 二、Redis 实现分布式锁2.1 分布式锁的概念与需求2.1.1 什么是分布式锁2.1.2 分布式锁的常见应用场景 2.2 基于 Redis 的分布式锁实现2.2.1 锁的获取与释放2.2.2 获取锁的实现2.2.3 释放锁的实现 2.3 失效机制与超时设置2.3.1 为什么需要超时机制2.3.2 使用 Redis 过期时间2.3.3 锁续约 2.4 RedLock 算法2.4.1 RedLock 的工作流程2.4.2 RedLock 的优缺点 三、分布式锁的优缺点与应用场景3.1 Redis 分布式锁的优点3.2 Redis 分布式锁的缺点3.3 分布式锁的典型应用场景3.3.1 单点任务执行3.3.2 秒杀场景的库存控制 3.4 Redis 分布式锁与其他实现方式的对比 四、Redis 事务4.1 Redis 事务回滚4.2 Redis 事务的行为4.4 Redis 为什么不支持回滚4.5 Redis 事务与传统事务的对比 五、Lua 脚本 vs Redis 事务5.1 Lua 脚本天然支持原子性5.2 Redis 事务的局限性5.3 Lua 脚本更加灵活 六、对比与总结6.1 Redis 分布式锁与其他锁实现方式的对比6.1.1 基于 Redis 的分布式锁6.1.2 基于数据库的分布式锁6.1.3 基于 Zookeeper 的分布式锁6.1.4 对比总结 6.2 实践中的最佳建议6.3 Redis 分布式锁的应用建议6.4 总结 引言 在现代分布式系统中分布式锁是一种核心的技术手段能够保证在多个节点或进程中对共享资源的安全访问。它通过提供互斥机制确保在同一时刻只有一个客户端能够操作关键资源。这种能力对于处理高并发请求和避免资源争夺至关重要。
随着微服务架构的广泛应用分布式锁的需求变得更加迫切。例如在订单系统中多个实例可能同时尝试更新同一库存数据在任务调度中确保定时任务不被多个实例重复执行是必要的。分布式锁不仅是技术实现中的关键模块也在业务逻辑中扮演着重要角色。
Redis 作为高性能的内存数据库以其简单易用、快速响应的特点成为实现分布式锁的常用选择。Redis 提供了丰富的原子操作如 SETNX 和 Lua 脚本为分布式锁的实现提供了坚实的基础。此外Redis 的 RedLock 算法更进一步解决了单点故障的问题为高可靠性需求的系统提供了有力支持。
然而在实际场景中Redis 分布式锁也面临一些挑战如如何设计锁的超时时间、如何防止锁误删以及如何在高可靠性场景下确保锁的有效性。本篇文章将深入探讨 Redis 分布式锁的原理与实现结合具体的 Go 语言示例代码逐步分析如何在分布式系统中高效且可靠地使用 Redis 分布式锁。 一、Redis的两种原子操作
1.1 Redis 的原子性
Redis 的所有命令天生具备原子性。这意味着每条命令在 Redis 服务器中要么完全执行要么完全失败绝不会中途打断。这一特性为并发控制提供了可靠的基础。
示例 考虑如下场景在高并发环境下多个客户端尝试同时获取同一分布式锁。假设只有一个客户端能够成功获取锁其余客户端必须排队等待或直接失败返回。Redis 的原子操作能够确保这种互斥性。 1.2 单命令
支持原子操作的常用命令 INCR 和 DECR原子递增/递减 这些命令常用于计数器场景通过单条命令完成值的增加或减少操作。 示例 INCR counter_key
DECR counter_key应用场景 适用于请求限流或资源配额管理场景。例如 用户访问次数计数。秒杀商品的库存控制。 SET 命令 SET 是 Redis 最灵活的原子操作命令之一支持多个选项能够同时完成键值设置和过期时间的配置。 示例 SET lock_key value NX EX 30NX仅在键不存在时设置值。EX 30设置键的过期时间为 30 秒。 优点 使用单条命令即可完成锁的获取与自动失效。高效、简单避免了竞争条件。 SETNX 命令 SETNXSET if Not Exists 是 Redis 专为互斥性操作设计的命令用于“仅在键不存在时设置值”。然而SETNX 本身不支持直接设置过期时间常需要与 EXPIRE 组合使用这可能导致非原子性问题。 示例 SETNX lock_key value
EXPIRE lock_key 30问题 如果在 SETNX 成功执行后EXPIRE 执行前发生宕机会导致锁没有设置过期时间从而引发死锁问题。 优化建议 在现代 Redis 应用中推荐使用 SET 命令代替 SETNX通过选项直接设置过期时间。 SET 和 SETNX 的对比
特性SETSETNX功能设置值并支持过期时间仅在键不存在时设置值灵活性高低常见使用场景分布式锁、缓存键设置简单的互斥操作推荐程度★★★★★★★
结论 在实现分布式锁时SET 是更优的选择能够简化逻辑并避免竞争条件。 1.3 Lua 脚本
Lua 脚本的功能与优势 Lua 脚本允许开发者将多个 Redis 命令组合成一个原子操作。这种组合方式特别适合复杂的并发场景。
示例通过 Lua 脚本实现锁的获取和过期时间设置
EVAL if redis.call(SETNX, KEYS[1], ARGV[1]) 1 then return redis.call(EXPIRE, KEYS[1], ARGV[2])
else return 0 end 1 lock_key value 30脚本逻辑
检查键是否存在。如果键不存在设置值并添加过期时间。返回操作结果。
优点
保证多步骤操作的原子性。避免传统组合命令可能引发的竞争条件。
Lua 脚本的性能 Redis 将 Lua 脚本加载到内存中执行性能非常高。脚本执行过程中不会被其他命令打断确保操作的完整性。
应用场景
分布式锁的获取与释放。批量处理复杂数据操作。 1.4 对比单命令与 Lua 脚本
使用单命令和 Lua 脚本实现分布式锁的获取和释放
单命令
SET lock_key value NX EX 30Lua 脚本
EVAL if redis.call(SETNX, KEYS[1], ARGV[1]) 1 then return redis.call(EXPIRE, KEYS[1], ARGV[2]) else return 0 end 1 lock_key value 30特性单命令 (SET)Lua 脚本简单性★★★★★★★★灵活性★★★★★★★★★性能★★★★★★★★★场景适配性常见锁场景自定义复杂逻辑 分布式锁的获取逻辑 #mermaid-svg-Bt33EXuCo68D1CsG {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Bt33EXuCo68D1CsG .error-icon{fill:#552222;}#mermaid-svg-Bt33EXuCo68D1CsG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Bt33EXuCo68D1CsG .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-Bt33EXuCo68D1CsG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Bt33EXuCo68D1CsG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Bt33EXuCo68D1CsG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Bt33EXuCo68D1CsG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Bt33EXuCo68D1CsG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Bt33EXuCo68D1CsG .marker.cross{stroke:#333333;}#mermaid-svg-Bt33EXuCo68D1CsG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Bt33EXuCo68D1CsG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Bt33EXuCo68D1CsG .cluster-label text{fill:#333;}#mermaid-svg-Bt33EXuCo68D1CsG .cluster-label span{color:#333;}#mermaid-svg-Bt33EXuCo68D1CsG .label text,#mermaid-svg-Bt33EXuCo68D1CsG span{fill:#333;color:#333;}#mermaid-svg-Bt33EXuCo68D1CsG .node rect,#mermaid-svg-Bt33EXuCo68D1CsG .node circle,#mermaid-svg-Bt33EXuCo68D1CsG .node ellipse,#mermaid-svg-Bt33EXuCo68D1CsG .node polygon,#mermaid-svg-Bt33EXuCo68D1CsG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Bt33EXuCo68D1CsG .node .label{text-align:center;}#mermaid-svg-Bt33EXuCo68D1CsG .node.clickable{cursor:pointer;}#mermaid-svg-Bt33EXuCo68D1CsG .arrowheadPath{fill:#333333;}#mermaid-svg-Bt33EXuCo68D1CsG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Bt33EXuCo68D1CsG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Bt33EXuCo68D1CsG .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-Bt33EXuCo68D1CsG .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-Bt33EXuCo68D1CsG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Bt33EXuCo68D1CsG .cluster text{fill:#333;}#mermaid-svg-Bt33EXuCo68D1CsG .cluster span{color:#333;}#mermaid-svg-Bt33EXuCo68D1CsG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Bt33EXuCo68D1CsG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不存在 存在 客户端尝试获取锁 键是否存在? 设置键值和过期时间 获取锁失败 返回锁获取成功 返回失败结果 二、Redis 实现分布式锁
分布式锁在分布式系统中至关重要特别是在多服务或多实例环境下确保同一时间只有一个客户端能够访问关键资源。Redis 提供了高效实现分布式锁的能力但正确使用这些能力仍需充分理解其工作原理和潜在问题。 2.1 分布式锁的概念与需求
2.1.1 什么是分布式锁
分布式锁是一种用于分布式环境中同步共享资源访问的机制它能够确保多进程、多节点环境中的互斥性。
分布式锁需要满足的核心特性
互斥性任意时刻只有一个客户端可以持有锁。无死锁即使持锁的客户端出现故障锁也能在合理时间后自动释放。容错性即使 Redis 实例发生部分故障锁仍然可用。高性能获取和释放锁的操作必须快速适合高并发场景。 2.1.2 分布式锁的常见应用场景
单点任务执行确保任务仅由一个节点执行例如数据库迁移。限流控制防止超出预期的流量保护后端服务。资源竞争在电商秒杀或抢购活动中确保同一商品不被超卖。跨服务事务协调多个微服务共同完成一个分布式事务。
案例秒杀商品库存控制 在秒杀活动中每次下单操作必须验证库存并减少相应数量。这需要分布式锁来确保多个客户端不会同时操作库存导致超卖。 2.2 基于 Redis 的分布式锁实现
2.2.1 锁的获取与释放
使用 Redis 实现分布式锁的基本过程包括两步
获取锁尝试在 Redis 中设置一个唯一键成功即表示锁定资源。释放锁仅持有锁的客户端可以释放锁以防止误操作。 2.2.2 获取锁的实现 基本实现 使用 SET 命令获取锁 SET lock_key value NX EX 30NX保证仅当键不存在时设置值避免覆盖现有锁。EX 30设置键的过期时间为 30 秒确保锁能够自动失效。 流程图获取锁逻辑 #mermaid-svg-VdogIEvu95ahuRNF {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-VdogIEvu95ahuRNF .error-icon{fill:#552222;}#mermaid-svg-VdogIEvu95ahuRNF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VdogIEvu95ahuRNF .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-VdogIEvu95ahuRNF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VdogIEvu95ahuRNF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VdogIEvu95ahuRNF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VdogIEvu95ahuRNF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VdogIEvu95ahuRNF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VdogIEvu95ahuRNF .marker.cross{stroke:#333333;}#mermaid-svg-VdogIEvu95ahuRNF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VdogIEvu95ahuRNF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VdogIEvu95ahuRNF .cluster-label text{fill:#333;}#mermaid-svg-VdogIEvu95ahuRNF .cluster-label span{color:#333;}#mermaid-svg-VdogIEvu95ahuRNF .label text,#mermaid-svg-VdogIEvu95ahuRNF span{fill:#333;color:#333;}#mermaid-svg-VdogIEvu95ahuRNF .node rect,#mermaid-svg-VdogIEvu95ahuRNF .node circle,#mermaid-svg-VdogIEvu95ahuRNF .node ellipse,#mermaid-svg-VdogIEvu95ahuRNF .node polygon,#mermaid-svg-VdogIEvu95ahuRNF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VdogIEvu95ahuRNF .node .label{text-align:center;}#mermaid-svg-VdogIEvu95ahuRNF .node.clickable{cursor:pointer;}#mermaid-svg-VdogIEvu95ahuRNF .arrowheadPath{fill:#333333;}#mermaid-svg-VdogIEvu95ahuRNF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VdogIEvu95ahuRNF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VdogIEvu95ahuRNF .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-VdogIEvu95ahuRNF .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-VdogIEvu95ahuRNF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VdogIEvu95ahuRNF .cluster text{fill:#333;}#mermaid-svg-VdogIEvu95ahuRNF .cluster span{color:#333;}#mermaid-svg-VdogIEvu95ahuRNF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VdogIEvu95ahuRNF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功 失败 客户端尝试获取锁 Redis SET命令 获取锁成功 获取锁失败 优化实现增加唯一标识 为锁的值添加一个唯一标识如 UUID确保只有持有该唯一标识的客户端能释放锁。 示例 SET lock_key uuid-12345 NX EX 302.2.3 释放锁的实现
释放锁时必须确保是锁的持有者操作避免误删其他客户端持有的锁。
基本实现 使用 DEL 命令释放锁DEL lock_key问题如果不检查锁的持有者身份可能导致误删。 举例如果客户端 A 执行了 SET 命令加锁后假设客户端 B 执行了 DEL 命令释放锁此时客户端 A 的锁就被误释放了。如果客户端 C 正好也在申请加锁 就可以成功获得锁进而开始操作共享数据。这样一来客户端 A 和 C 同时在对共享数据 进行操作数据就会被修改错误这也是业务层不能接受的。 为了应对这个问题我们需要能区分来自不同客户端的锁操作具体咋做呢?其实我们 可以在锁变量的值上想想办法。 改进实现检查锁的持有者 使用 Lua 脚本确保原子性 EVAL if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end 1 lock_key uuid-12345逻辑 获取键的值验证持有者身份。如果身份匹配则删除键。返回操作结果。 流程图释放锁逻辑 #mermaid-svg-rM6pRfJnoFGjL3Ag {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-rM6pRfJnoFGjL3Ag .error-icon{fill:#552222;}#mermaid-svg-rM6pRfJnoFGjL3Ag .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rM6pRfJnoFGjL3Ag .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-rM6pRfJnoFGjL3Ag .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rM6pRfJnoFGjL3Ag .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rM6pRfJnoFGjL3Ag .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rM6pRfJnoFGjL3Ag .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rM6pRfJnoFGjL3Ag .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rM6pRfJnoFGjL3Ag .marker.cross{stroke:#333333;}#mermaid-svg-rM6pRfJnoFGjL3Ag svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rM6pRfJnoFGjL3Ag .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rM6pRfJnoFGjL3Ag .cluster-label text{fill:#333;}#mermaid-svg-rM6pRfJnoFGjL3Ag .cluster-label span{color:#333;}#mermaid-svg-rM6pRfJnoFGjL3Ag .label text,#mermaid-svg-rM6pRfJnoFGjL3Ag span{fill:#333;color:#333;}#mermaid-svg-rM6pRfJnoFGjL3Ag .node rect,#mermaid-svg-rM6pRfJnoFGjL3Ag .node circle,#mermaid-svg-rM6pRfJnoFGjL3Ag .node ellipse,#mermaid-svg-rM6pRfJnoFGjL3Ag .node polygon,#mermaid-svg-rM6pRfJnoFGjL3Ag .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rM6pRfJnoFGjL3Ag .node .label{text-align:center;}#mermaid-svg-rM6pRfJnoFGjL3Ag .node.clickable{cursor:pointer;}#mermaid-svg-rM6pRfJnoFGjL3Ag .arrowheadPath{fill:#333333;}#mermaid-svg-rM6pRfJnoFGjL3Ag .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rM6pRfJnoFGjL3Ag .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rM6pRfJnoFGjL3Ag .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-rM6pRfJnoFGjL3Ag .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-rM6pRfJnoFGjL3Ag .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rM6pRfJnoFGjL3Ag .cluster text{fill:#333;}#mermaid-svg-rM6pRfJnoFGjL3Ag .cluster span{color:#333;}#mermaid-svg-rM6pRfJnoFGjL3Ag div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rM6pRfJnoFGjL3Ag :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 匹配 不匹配 客户端请求释放锁 检查持有者身份 删除锁 操作失败 2.3 失效机制与超时设置
2.3.1 为什么需要超时机制
在分布式环境中客户端可能因意外如网络故障、程序崩溃失去锁的控制权。超时机制可以防止锁无限期存在导致资源被长期占用。
2.3.2 使用 Redis 过期时间
在锁的获取时设置过期时间是最简单的失效机制
SET lock_key value NX EX 30关键点
过期时间必须合理设置避免锁在任务未完成时被释放。在任务可能超过预期时间的情况下需要考虑锁续约机制。
2.3.3 锁续约
为了确保任务能在复杂场景下顺利完成可能需要续约锁的过期时间。 实现方法
使用定时任务定期续约锁。检查锁的持有者身份后延长过期时间。
示例续约脚本
EVAL if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(EXPIRE, KEYS[1], ARGV[2]) else return 0 end 1 lock_key uuid-12345 302.4 RedLock 算法
上面我们已经了解了如何使用 SET 命令和 Lua 脚本在 Redis 单节点上实现分布式锁。但是我们现在只用了一个 Redis 实例来保存锁变量如果这个 Redis 实例发生故障宕机了那么锁变量就没有了。此时客户端也无法进行锁操作了这就会影响到业务的正常执行。所以我们在实现分布式锁时还需要保证锁的可靠性。 为了避免 Redis 实例故障而导致的锁无法工作的问题Redis 官方提出了分布式锁算法Redlock用于提高锁的可靠性。
它的核心思想是通过多个独立的 Redis 实例实现锁的容错。
2.4.1 RedLock 的工作流程
客户端尝试在所有实例上获取锁。如果在大多数实例上获取锁成功并且总时间小于锁的有效期锁定成功。如果失败释放已获取的锁并重试。
示意图 #mermaid-svg-HBXEnrW5JeBJCar4 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-HBXEnrW5JeBJCar4 .error-icon{fill:#552222;}#mermaid-svg-HBXEnrW5JeBJCar4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HBXEnrW5JeBJCar4 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-HBXEnrW5JeBJCar4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HBXEnrW5JeBJCar4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HBXEnrW5JeBJCar4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HBXEnrW5JeBJCar4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HBXEnrW5JeBJCar4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HBXEnrW5JeBJCar4 .marker.cross{stroke:#333333;}#mermaid-svg-HBXEnrW5JeBJCar4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HBXEnrW5JeBJCar4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HBXEnrW5JeBJCar4 .cluster-label text{fill:#333;}#mermaid-svg-HBXEnrW5JeBJCar4 .cluster-label span{color:#333;}#mermaid-svg-HBXEnrW5JeBJCar4 .label text,#mermaid-svg-HBXEnrW5JeBJCar4 span{fill:#333;color:#333;}#mermaid-svg-HBXEnrW5JeBJCar4 .node rect,#mermaid-svg-HBXEnrW5JeBJCar4 .node circle,#mermaid-svg-HBXEnrW5JeBJCar4 .node ellipse,#mermaid-svg-HBXEnrW5JeBJCar4 .node polygon,#mermaid-svg-HBXEnrW5JeBJCar4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HBXEnrW5JeBJCar4 .node .label{text-align:center;}#mermaid-svg-HBXEnrW5JeBJCar4 .node.clickable{cursor:pointer;}#mermaid-svg-HBXEnrW5JeBJCar4 .arrowheadPath{fill:#333333;}#mermaid-svg-HBXEnrW5JeBJCar4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HBXEnrW5JeBJCar4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HBXEnrW5JeBJCar4 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-HBXEnrW5JeBJCar4 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-HBXEnrW5JeBJCar4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HBXEnrW5JeBJCar4 .cluster text{fill:#333;}#mermaid-svg-HBXEnrW5JeBJCar4 .cluster span{color:#333;}#mermaid-svg-HBXEnrW5JeBJCar4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HBXEnrW5JeBJCar4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是 否 客户端请求分布式锁 Redis实例1获取锁 Redis实例2获取锁 Redis实例3获取锁 大多数锁是否成功 获取锁成功 获取锁失败 2.4.2 RedLock 的优缺点
优点
容错性高即使部分 Redis 实例故障锁仍然有效。提供更高的可靠性适合多数据中心部署。
缺点
实现复杂度较高适合对可靠性要求极高的场景。对网络延迟敏感。 总结
Redis 提供了简单高效的分布式锁实现方法但需要合理处理锁的过期和释放机制。对于高可靠性需求场景可以考虑使用 RedLock 算法。 三、分布式锁的优缺点与应用场景
Redis 提供了灵活高效的分布式锁解决方案但在实际使用中仍需要权衡其优缺点以满足具体业务需求。在本章中我们将深入探讨 Redis 分布式锁的优势和不足并通过 Go 语言实现具体业务场景的代码示例。 3.1 Redis 分布式锁的优点 高性能 Redis 是内存数据库读写速度极快能够支持高并发环境。单命令如 SET和 Lua 脚本的原子性保证锁操作的高效性。 简单易用 Redis 提供的锁机制易于实现仅需几行代码即可完成锁的获取与释放。通过 SET 命令或 Lua 脚本可以实现大多数锁的需求。 灵活性 支持多种方式实现分布式锁单命令、Lua 脚本、RedLock 算法。可通过多实例部署提升可靠性。 3.2 Redis 分布式锁的缺点 单点故障 如果 Redis 部署为单节点实例当节点故障时锁可能失效。解决方法使用 Redis 集群或 RedLock 算法。 网络延迟与时钟漂移 锁的过期时间依赖于客户端与 Redis 之间的通信延迟。如果网络异常或时钟漂移严重可能导致锁过早或过晚失效。 误删锁的风险 如果锁的释放操作未验证持有者身份可能误删其他客户端的锁。解决方法使用带唯一标识的值结合 Lua 脚本。 一致性问题 RedLock 算法在部分实例故障时可能无法满足严格一致性需求。 3.3 分布式锁的典型应用场景
3.3.1 单点任务执行
业务需求 确保同一时刻只有一个任务在某服务实例中执行。例如生成每日报告任务。
Go 语言实现
package mainimport (contextfmtgithub.com/go-redis/redis/v8time
)var ctx context.Background()func acquireLock(client *redis.Client, lockKey string, value string, expiration time.Duration) bool {// 尝试获取锁ok, err : client.SetNX(ctx, lockKey, value, expiration).Result()if err ! nil {fmt.Println(Error acquiring lock:, err)return false}return ok
}func releaseLock(client *redis.Client, lockKey string, value string) bool {// 使用 Lua 脚本释放锁script : if redis.call(GET, KEYS[1]) ARGV[1] thenreturn redis.call(DEL, KEYS[1])elsereturn 0endresult, err : client.Eval(ctx, script, []string{lockKey}, value).Result()if err ! nil {fmt.Println(Error releasing lock:, err)return false}return result.(int64) 1
}func main() {client : redis.NewClient(redis.Options{Addr: localhost:6379,})lockKey : daily_task_locklockValue : unique-id-12345lockExpiration : 30 * time.Secondif acquireLock(client, lockKey, lockValue, lockExpiration) {fmt.Println(Lock acquired. Executing task...)// 模拟任务执行time.Sleep(10 * time.Second)// 释放锁if releaseLock(client, lockKey, lockValue) {fmt.Println(Lock released.)} else {fmt.Println(Failed to release lock.)}} else {fmt.Println(Failed to acquire lock. Another instance might be running the task.)}
}运行结果
如果锁获取成功输出 Lock acquired. Executing task...并在完成任务后释放锁。如果锁获取失败输出 Failed to acquire lock. Another instance might be running the task. 3.3.2 秒杀场景的库存控制
业务需求 在秒杀场景中需要确保同一时刻只有一个客户端能够成功扣减库存避免超卖。
Go 语言实现 var decStockLua -- 参数local skey KEYS[1] -- 库存键local decrement tonumber(ARGV[1]) -- 要扣减的数量转换为数字-- 判断 key 是否存在if redis.call(EXISTS, skey) 0 thenreturn {err Key does not exist}end-- 获取库存值并转换为数字local stock tonumber(redis.call(GET, skey))-- 判断库存是否充足if stock decrement then-- 扣减库存redis.call(DECRBY, skey, decrement)return {ok Decrement successful}else-- 扣减失败return {err Insufficient stock}endfunc main() {client : redis.NewClient(redis.Options{Addr: localhost:6379,})lockKey : product_locknum : 3var ctx context.Background()client.SetNX(ctx, lockKey, 100, time.Hour) // 初始化库存为100result, err : client.Eval(ctx, decStockLua, []string{lockKey}, num).Result()if err ! nil {fmt.Println(Error :, err)return}if result.(string) Decrement successful {fmt.Println(Successful)} else {fmt.Println(Error)}return
}运行结果
如果库存充足返回 Decrement successful同时扣减库存。如果库存不足输出 Insufficient stock。如果库存没有初始化输出 Key does not exist。 3.4 Redis 分布式锁与其他实现方式的对比
实现方式优点缺点场景适配性Redis 分布式锁高性能、易用、灵活单点故障风险误删风险高并发、低延迟场景数据库分布式锁一致性强性能较差复杂度高高一致性需求场景Zookeeper 分布式锁高可靠性支持会话失效部署复杂性能不如 Redis跨节点、容错场景 结论
Redis 分布式锁凭借高效和灵活的特点在许多高并发场景中表现优异。然而根据具体业务需求选择合适的实现方式才是设计高质量分布式系统的关键。对于具有高一致性要求的系统可以考虑 Zookeeper 或数据库锁而对于性能敏感的系统Redis 是不二之选。 问题MySQL是通过事务来保证原子性的Redis也是支持事务的那为什么Redis不使用事务来保证原子性呢 四、Redis 事务
在回答上文的问题之前先了解Redis事务只有了解了Redis事务才能更好地理解为什么Redis不使用事务来保证原子性。
4.1 Redis 事务回滚
在传统数据库中事务的回滚是指在事务执行过程中如果出现错误或未满足某些条件可以撤销事务中已经成功执行的操作将数据状态恢复到事务开始时的状态。
关键点
回滚范围包括事务中已经成功执行的操作。保障一致性事务失败时数据完全恢复不留下任何副作用。
Redis 的 DISCARD 命令用于在 EXEC 之前放弃事务队列。
DISCARD 的使用
MULTI
SET key1 value1
INCR key2
DISCARDMULTI 开启事务。SET key1 value1 和 INCR key2 被放入队列。执行 DISCARD 时队列被清空事务未提交任何命令都不会执行。
与回滚的区别 DISCARD 仅用于放弃事务队列完全不会执行任何操作。它无法撤销已经执行的命令因为 Redis 的事务命令只有在 EXEC 后才会实际执行。 4.2 Redis 事务的行为
Redis 的事务通过 MULTI 和 EXEC 命令定义但它的机制与传统数据库不同。Redis 的事务是 命令队列化执行
MULTI开启事务之后的命令被放入队列。EXEC提交事务执行队列中的所有命令。事务执行过程 队列中的所有命令会按顺序一次性执行。单个命令是原子的但 Redis 不支持事务整体的原子性。
示例Redis 事务
MULTI
SET key1 value1
INCR key2
EXECSET key1 value1 会加入队列。INCR key2 也会加入队列。执行 EXEC 时这些命令会按顺序执行。
如果事务中的某条命令失败
Redis 的事务不会停止或回滚。失败的命令会返回错误其他命令继续执行。
示例
MULTI
SET key1 value1
INCR key2 ## 如果 key2 不是整数这里会报错
SET key3 value3
EXEC结果
SET key1 value1 成功。INCR key2 失败如果 key2 不是整数。SET key3 value3 成功。事务不会自动回滚SET key1 和 SET key3 的结果会保留。 4.4 Redis 为什么不支持回滚
Redis 的设计哲学决定了它不支持回滚机制原因如下
1. 性能优先
回滚机制需要在事务开始前记录所有被修改数据的快照如数据库中的 Undo Log。这会显著增加内存开销并降低 Redis 的写入性能。
2. 数据模型的简单性
Redis 是一个高性能的内存数据库其设计目标是简单高效。引入回滚机制会增加 Redis 的实现复杂度与其设计理念不符。
3. 操作原子性
Redis 保证每条命令的原子性这在大多数使用场景中已经足够。对于需要更复杂事务机制的场景通常会选择其他工具如传统关系型数据库。 4.5 Redis 事务与传统事务的对比
特性Redis 事务传统数据库事务回滚支持不支持支持错误处理单命令错误不会中止自动回滚或重试原子性范围单命令整体事务性能高较低复杂性低高 五、Lua 脚本 vs Redis 事务
再来对比一下 Lua 脚本和 Redis 事务看看它们在保证原子性方面的差异。
5.1 Lua 脚本天然支持原子性
Lua 脚本在 Redis 内部执行时是完全原子的
当 Lua 脚本运行时Redis 不会处理其他命令。整个脚本的执行要么全部成功要么全部失败。
这一点非常类似于事务的概念但实现方式更简单且性能更高。Lua 脚本将逻辑和操作打包为一个命令发送到 Redis因此避免了事务中潜在的竞争条件。
5.2 Redis 事务的局限性
Redis 的事务通过 MULTI 和 EXEC 命令实现但它的机制并不像传统数据库中的事务那样强大主要体现在以下几个方面 没有回滚机制 Redis 事务不支持回滚。如果事务中的某条命令执行失败其余命令仍会继续执行。 示例 MULTI
SET key1 value1
INCR key2 # 如果 key2 不是数字这里会报错
EXEC在上面的例子中即使 INCR key2 出错SET key1 value1 仍然会被执行。对于需要强一致性的场景这可能会引发问题。 不支持条件判断 Redis 事务中无法直接进行条件判断。例如要实现“如果某个键的值满足条件则执行某个操作”需要依赖客户端逻辑而 Lua 脚本可以直接在脚本中实现。 多次通信带来的性能开销 Redis 事务的执行需要客户端与服务器之间多次交互 客户端发送 MULTI 开启事务。客户端逐条发送事务中的命令。客户端发送 EXEC 提交事务。 这种交互模式相比 Lua 脚本一次性发送脚本的方式性能要低。
5.3 Lua 脚本更加灵活
Lua 脚本可以实现复杂的逻辑包括条件判断、循环等而 Redis 事务只是一系列命令的简单打包无法动态调整逻辑。
示例Lua 脚本实现条件判断
EVAL if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end 1 lock_key value上述脚本实现了一个简单的条件判断仅当键的值等于指定值时才删除键。这样的逻辑在事务中无法实现。 Lua 脚本与 Redis 事务的对比表
特性Lua 脚本Redis 事务原子性天然支持部分支持命令级原子性错误处理全部失败或全部成功单命令失败不影响其他命令条件判断支持不支持性能高一次性发送脚本较低多次通信实现复杂度灵活可实现复杂逻辑简单适合基础操作应用场景高级逻辑如分布式锁基础事务操作 实际场景中的选择建议 使用 Lua 脚本的场景 需要条件判断的复杂操作例如分布式锁的释放时校验持有者身份。多步操作需要原子性保障例如获取锁的同时设置过期时间。性能敏感的高并发场景。 使用 Redis 事务的场景 操作相对简单且对回滚和条件判断没有要求。确保操作序列的基本一致性而不是严格的一致性。对性能要求不高或者已经使用 Redis Pipeline 优化通信延迟。 总结
性能Lua 脚本因为减少了客户端与 Redis 的通信开销性能优于 Redis 事务。灵活性Lua 脚本能实现复杂逻辑而事务只适合简单操作。原子性Lua 脚本具有天然的原子性而 Redis 事务的原子性较为有限。
因此在需要原子性保障和复杂逻辑的场景中如分布式锁Lua 脚本通常是更优的选择。如果场景较简单且对一致性要求不高可以考虑 Redis 事务。 六、对比与总结
6.1 Redis 分布式锁与其他锁实现方式的对比
分布式锁的实现方式有多种常见的有基于 Redis、数据库、以及 Zookeeper 的方案。它们各自有不同的优缺点适用于不同的业务需求。
6.1.1 基于 Redis 的分布式锁
优势
性能优越基于内存操作读写速度快能够支持高并发场景。实现简单通过单命令 SET 或 Lua 脚本可以快速实现分布式锁。灵活性强支持多种实现方式单命令、Lua 脚本、RedLock 算法。
劣势
可靠性较低单点故障可能导致锁失效需通过集群或 RedLock 增加容错性。一致性问题锁的释放、超时机制对网络延迟和时钟同步敏感。
6.1.2 基于数据库的分布式锁
优势
一致性强数据库天生支持事务能确保锁的严格一致性。依赖性低无需额外引入中间件只需数据库即可实现。
劣势
性能瓶颈数据库操作的性能低于内存数据库难以支撑高并发。实现复杂实现事务性锁机制需要精心设计和调优。
示例基于 MySQL 的分布式锁 通过使用 SELECT FOR UPDATE 来加锁但性能远不如 Redis。
6.1.3 基于 Zookeeper 的分布式锁
优势
高可靠性基于强一致性协议如 ZAB 协议在节点故障时仍能保证锁的一致性。天然分布式适用于多节点、多数据中心部署。
劣势
实现复杂需要额外部署和维护 Zookeeper 集群。性能一般不适合极高并发场景。 6.1.4 对比总结
特性Redis 分布式锁数据库分布式锁Zookeeper 分布式锁性能★★★★★★★★★★一致性★★★★★★★★★★★★★实现复杂度★★★★★★★★★容错性★★★★★★★★★★★适用场景高并发、低延迟高一致性要求跨节点高可靠性场景 6.2 实践中的最佳建议
根据 Redis 分布式锁的特性以下是实践中需注意的关键点 使用 SET 替代 SETNX 推荐使用 SET key value NX EX 的方式获取锁避免 SETNX 与 EXPIRE 组合操作导致的非原子性问题。 使用唯一标识避免误删 为锁的值添加唯一标识如 UUID并通过 Lua 脚本释放锁确保只有持锁者能删除锁。 设计合理的超时时间 锁的超时时间应根据任务的预计执行时间设置避免锁在任务未完成时过早失效。 在高可靠性场景中使用 RedLock 对于分布式系统的核心模块如订单处理、支付系统可考虑使用 RedLock 算法实现更高的容错性。 配合业务逻辑处理锁失败情况 在锁获取失败时需明确业务逻辑的补偿机制如重试或降级处理。 6.3 Redis 分布式锁的应用建议
Redis 分布式锁适合以下场景
高并发环境如限流、库存控制。中等可靠性要求如日志处理、异步任务调度。
不适合的场景
严格一致性需求如金融交易建议使用数据库或 Zookeeper。极高可靠性需求如跨区域分布式事务建议结合其他技术如 Kafka。 6.4 总结
Redis 实现分布式锁在现代分布式系统中占据重要地位凭借其高性能和灵活性广泛应用于高并发场景。通过结合正确的实现方式和实践建议可以进一步提升锁的可靠性。
然而Redis 分布式锁仍存在单点故障、网络延迟等潜在问题。未来可以通过以下方向改进
更高效的 RedLock 算法优化锁的容错性能减少延迟对锁的影响。结合多种技术实现混合锁利用 Redis 提供性能结合 Zookeeper 或数据库保障一致性。
Redis 分布式锁是开发分布式系统的重要工具但不是万能的。在实践中需根据业务需求选择合适的锁实现方式打造高效、可靠的系统。 附录 Redis 命令参考
SET 命令官方文档Lua 脚本官方文档