ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Redisson MultiLock分布式锁原理与实战:Windows搭建多Redis实例

2026/10/5 10:49:25 拓冰建站 浏览量
Redisson MultiLock分布式锁原理与实战:Windows搭建多Redis实例 做高并发项目做到一定阶段分布式锁这道坎是绕不过去的。最近我在Windows本机上同时起了三个Redis实例用Redisson的MultiLock把加锁、续期、解锁整套流程完整跑了一遍顺手把分布式锁里最容易含糊的几个原理点都验证清楚了。这里不打算讲虚的我直接按“为什么需要多个Redis、Windows下怎么搭多实例、MultiLock内部到底做了什么、代码怎么落地、坑在哪里”这个顺序来写有环境有代码有结论希望能给同样在研究分布式锁的Java后端一个完整的参考。这个实验源自一个需要动手实操点评的项目项目编号是p68主题正好落在Redisson的MultiLock上。别以为MultiLock就是同时调三个RLock那么简单真跑起来才会发现里面全是细节。下面开始。1. 为什么我非要在Windows上折腾三个Redis1.1 一个Redis实例的时代过去了分布式锁最常见的三个场景秒杀扣库存、订单状态流转、定时任务防重。这三个场景的本质是同一件事——多个进程并发操作同一份数据必须保证同时只有一个进程在写。最粗暴的方案是在Redis里写一个带过期时间的key用SETNX或SET NX EX拿到key就代表拿到锁。单业务节点、单Redis的时候这套方案确实能用我早期很多项目就是这么干的代码简单性能也好。一旦环境复杂化问题就来了。最要命的是主从切换线程A在主节点加锁成功这条写命令还没来得及同步给从节点主节点挂了哨兵把从节点提升为新的主节点新主节点上根本没有锁这个key。线程B在此时来加锁一样能成功于是两个线程同时进入临界区。库存扣成负数、订单重复创建这些故障全都指向同一个源头——单点Redis的锁不具备故障转移能力。就算你把架构简化成单节点Redis、不搞主从业务的执行时长也会让锁很难受。锁过期时间设短了业务没跑完锁就没了设长了客户端一旦真的崩溃锁要很久才能释放后续请求全被堵住。这些痛点合在一起就是Redisson这类组件存在的理由。1.2 MultiLock想解决的正是单点背后的互斥保证Redisson在RLock接口下封装了可重入、自动续期、阻塞等待这些能力。RLock之上Redisson还提供了两个组合锁RedissonMultiLock和RedissonRedLock。这次实验重点研究的是MultiLock。MultiLock的核心语义非常严格同时对多个Redis实例加同一个key的锁全部实例都拿到锁整个加锁才算成功。只要有一台失败之前成功的锁会全部被回收。这样设计的直接收益是锁的安全性不再押在单台Redis上——只要这三台实例不是同时宕机锁就不会因为主从切换或单点故障而凭空丢失。代价同样明显任何一个节点不可用加锁都会失败。我在本机第一次跑MultiLock时就被这个语义坑过三个实例里有一个端口写错了代码一直在等待后来才知道是“全部成功”这个条件卡住了。把这个特性理解透以后再去选择用普通锁、MultiLock还是RedLock你就知道各自承受的故障模型是什么了。1.3 这套方案适合谁参考如果你是一名Java后端正在准备分布式锁相关的面试题或者手上正好有一个需要动手点评的实操项目那这篇内容的节奏会比较合适。我会从零开始在Windows 10/11上搭出三个互不干扰的Redis实例配置Redisson连接写完整的MultiLock加锁代码再用Redis命令行验证底层数据结构最后整理这段时间踩过的坑。Linux环境完全同理只是安装Redis的方式略有差别。说句真心话很多人在笔记本上看源码看得头头是道但一旦落地到“怎么让MultiLock在本地真正跑起来”这一步就开始卡壳。问题往往不在代码本身而是环境细节三个Redis实例没有真正独立运行或者配置里端口和日志路径写错或者压根没意识到MultiLock要求所有节点全部成功。下面先把环境问题彻底解决掉。2. Windows下创建多个Redis实例的完整操作2.1 方案选型解压版、Docker、WSL还是服务化Windows上跑Redis的常见路子有四条。第一是Docker Desktop里跑redis:7容器环境干净但依赖Hyper-V或WSL2不少公司电脑的虚拟化功能被禁用连起容器这一步都过不去。第二是WSL2里装Linux原生Redis能用但涉及子系统路径、端口转发、文件权限这些额外概念对只想验证锁原理的人有点绕。第三是Memurai这是Windows原生的Redis实现稳定性和性能都不错但主要用于商业环境学习场景没必要。第四条路子就是社区编译的Redis-x64 zip解压包解压即用没有任何额外的平台依赖。我选的是第四条。原因很简单MultiLock实验需要同时跑多个实例zip包复制三份改个端口就是三个独立节点。这个模型比容器更接近真实的物理部署每个实例有独立的配置目录、独立的日志文件、独立的端口。虽然Docker做起来也快但在多实例场景下你还得额外管理端口映射和容器网络反而干扰对锁本身的理解。2.2 从解压到三节点运行一步步来先说下载。从GitHub的Redis-x64仓库下载zip包解压后目录里应该有redis-server.exe、redis-cli.exe、redis.windows.conf这些文件。建议解压到类似D:\dev\redis这样的纯英文路径不带空格不加中文这一步能省掉后面很多编码和路径解析的麻烦。然后把整个目录复制三份分别是redis-6379、redis-6380、redis-6381。每个目录就是一个独立实例数据文件、日志、配置文件互不干扰。接下来修改每个目录里的redis.windows.conf。以redis-6379为例至少要调整这几项port 6379 bind 127.0.0.1 appendonly yes appendfilename appendonly-6379.aof dir D:/dev/redis/redis-6379/ logfile redis-6379.log另外两个目录把端口、日志文件名、dir路径换成6380、6381。有几个细节一定要提醒redis配置文件不允许出现缩进所有配置项顶格写文件编码用UTF-8无BOMWindows记事本默认保存容易带BOM可能导致解析异常appendonly建议改成yes锁实验里重要数据别只放在内存里。配置改完后开三个PowerShell窗口分别启动cd D:\dev\redis\redis-6379 redis-server.exe redis.windows.conf其他两个窗口同理。启动后看到“Ready to accept connections”就是成功了。三个窗口保持前台运行日志实时滚动排查问题非常方便。接着验证每个实例的连通性redis-cli.exe -p 6379 ping redis-cli.exe -p 6380 ping redis-cli.exe -p 6381 ping三个都返回PONG说明三节点环境已经就绪。2.3 让Redis实例更贴近生产密码与服务化本地实验不加密码能跑通但研究分布式锁最好还是把密码加上。在配置文件里加一行requirepass例如统一设置成Test123那么Redisson连接串里也要带对应密码。加密码有个额外好处——你之后看任何连接报错会下意识先怀疑认证问题排查思路会更有条理。Windows服务化则是另一个可选项。用sc.exe能把redis-server注册成系统服务开机自启命令大概是这样sc.exe create Redis6379 binPath \D:\dev\redis\redis-6379\redis-server.exe\ \D:\dev\redis\redis-6379\redis.windows.conf\但我建议做原理验证时先别服务化。服务化以后端口被抢占、重启后数据路径不对、日志写到哪都不直观。前台窗口虽然简陋却是学习阶段最好的调试手段。等这套流程完全跑熟再考虑服务化不迟。3. Redisson MultiLock 原理拆解3.1 MultiLock 和 RedLock 是什么关系先说结论RedissonMultiLock和RedissonRedLock是两个东西。RedLock是Redis原作者提出的一种分布式锁算法核心观点是在N个互相独立的Redis节点上依次加锁只要超过半数的节点成功这次加锁就算成功从而容忍少量节点故障。Redisson实现了这套算法类名是RedissonRedLock它继承自RedissonMultiLock额外引入了failedLocksLimit参数允许指定数量的节点加锁失败。RedissonMultiLock本身没有那么强的容错语义。它的要求是“所有节点全部成功”一个都不能少。这种设计思想和RedLock有明显区别MultiLock不打算容忍节点故障而是假设你有三台独立且稳定的Redis用全量成功换取锁不会在任一台单点丢失。实际项目中如果你只能保证两台可用RedLock可能更合适如果三台都能保证MultiLock的互斥性更强。这个区别面试时经常被拿出来问。只记住“MultiLock是全部成功RedLock是多数成功”还不够更要能说出为什么会有这种设计差异以及使用场景分别是什么。3.2 一次完整的加锁过程是怎么走的当代码调用multiLock.lock()时Redisson内部遍历持有的RLock列表对每个RLock依次执行tryLock。以单个RLock来看底层执行了一段Lua脚本我来贴一个简化版本方便对照看if (redis.call(exists, KEYS[1]) 0) then redis.call(hincrby, KEYS[1], ARGV[2], 1) redis.call(pexpire, KEYS[1], ARGV[1]) return nil end if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then redis.call(hincrby, KEYS[1], ARGV[2], 1) redis.call(pexpire, KEYS[1], ARGV[1]) return nil end return redis.call(pttl, KEYS[1])这段脚本的含义如果锁key不存在创建一个Hash并把当前线程的标识写入设置过期时间返回nil表示加锁成功如果key存在且持有者就是当前线程计数加一刷新过期时间这实现的就是可重入否则返回剩余过期时间表示锁被别人持有。MultiLock在这一步有个关键行为只要有一个节点加锁失败它不会装作什么都没发生而是把之前已经成功获取的锁全部释放掉。我最初以为MultiLock只是简单的循环加锁看了源码才发现它还负责失败后的补偿回收。这也是为什么MultiLock的失败路径比普通锁复杂得多。加锁成功的总耗时大约是N个节点的RTT之和因为所有调用是串行执行的。节点数和加锁延迟是线性关系这一点在做性能评估时要有心理准备。3.3 看门狗、重入、解锁的核心机制Redisson最让人踏实的机制是看门狗Watchdog。调用lock()不传leaseTime时锁默认的过期时间是30秒Redisson同时会启动一个后台定时任务每隔10秒检查一次锁是否还在如果还在就把过期时间重新刷回30秒。业务方法哪怕跑了几十秒只要持有锁的客户端进程活着锁就一直有效。看门狗的生命周期绑定在客户端进程上所以存在一个优雅的兜底客户端突然宕机后后台任务也随之消失锁最多30秒就会自动过期其他请求不会无限等待。这个设计比“永远不释放”的死锁方案高明得多。但看门狗有一个重要前提不能手动传leaseTime。只要你在tryLock里传了第二个参数Redisson会认为你希望锁按固定时间释放看门狗自动关闭。我在一次测试里给锁设了3秒业务逻辑执行了4秒结果第二个线程在第3秒以后成功进来两个线程同时操作了同一份数据。这不是Redisson的问题而是我把业务超时和锁过期时间的关系想得太简单了。再聊重入和解锁。锁在Redis里的存储结构是Hashfield是客户端唯一ID加线程IDvalue是重入次数。同一线程重复加锁value加一解锁时value减一减到零才删除key。解锁Lua脚本会检查持有者身份不是自己的锁绝不会乱删。这个设计保证了两个客户端即便拿到同一个key也不能互相解锁。发布订阅机制也值得一提。RedissonLock在拿锁失败后会订阅锁释放的channel。持有锁的线程解锁并删除key后会向channel发布一条释放消息所有阻塞中的等待线程收到消息后醒来重新竞争。这个机制避免了无意义的固定轮询也是Redisson锁响应很快的原因之一。MultiLock的解锁则是对所有节点逐个执行unlock。正因为如此释放锁时一定要小心如果某个节点的锁已经因为超时被自动释放unlock时可能抛出IllegalMonitorStateException。实战里我会先调用isHeldByCurrentThread确认自己还持有锁再决定是否执行unlock。4. 实操用MultiLock写一段可运行的分布式锁代码4.1 引入依赖并初始化RedissonClient代码用Java配合Spring Boot实现。先引入Redisson的Spring Boot Starter版本选3.17.x以上dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.17.7/version /dependency然后初始化三个独立的RedissonClient。这里要特意强调MultiLock里的RLock必须来自不同的RedissonClient否则等于三把锁全落在同一个Redis节点上MultiLock直接退化成一个普通锁所有高可用意义全部消失。我在实验初期就在这个点上踩过坑用一个client创建了三个RLock用hgetall看数据才发现三个节点上根本没有三份锁记录。Config config1 new Config(); config1.useSingleServer() .setAddress(redis://127.0.0.1:6379); // 如果有密码再加 .setPassword(Test123) RedissonClient client1 Redisson.create(config1); // config2 - 127.0.0.1:6380, config3 - 127.0.0.1:6381每个Config对应一个节点配置互相独立。这里还要提醒一点多个RedissonClient会各自创建线程池和连接池内存占用会比单client高不少本地实验时给JVM留够堆内存别在项目里同时初始化一堆用不到的client。4.2 加锁、业务执行、解锁的标准写法下面是一段完整的MultiLock业务代码public void processOrder(Long orderId) { String lockKey lock:order: orderId; RLock lock1 client1.getLock(lockKey); RLock lock2 client2.getLock(lockKey); RLock lock3 client3.getLock(lockKey); RedissonMultiLock multiLock new RedissonMultiLock(lock1, lock2, lock3); boolean locked false; try { locked multiLock.tryLock(3, TimeUnit.SECONDS); if (!locked) { throw new RuntimeException(获取分布式锁失败订单 orderId); } // 业务逻辑查询库存、扣减、写订单 TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (locked) { multiLock.unlock(); } } }这里的tryLock只传了一个参数等待锁的最长时间3秒不传leaseTime。这种情况下锁默认30秒过期由看门狗自动续期业务方法跑多久都安全。如果你业务上需要固定过期时间也可以改成 tryLock(3, 30, TimeUnit.SECONDS)第二个参数就是锁的自动过期时间此时看门狗不生效锁到达30秒准点释放需要自己评估业务最长执行时间。把unlock放进finally是铁的纪律。我在评审别人代码时见过不少Lock成功但finally没释放的案例业务一抛异常锁只能等超时被动释放整个集群的请求瞬间全堵在这个key上故障级别直接拉满。4.3 实测验证Redis里的数据长什么样代码跑起来后去三个Redis节点查询同一个keyredis-cli.exe -p 6379 type lock:order:1001 redis-cli.exe -p 6379 hgetall lock:order:1001预期输出hash 1) a5342f90-bc9c-4a3f-8e0a-123456789abc:38 2) 1field里的前半段是客户端生成的唯一ID冒号后是线程IDvalue为1表示重入次数。三个节点上的数据应当完全一致这才说明MultiLock确实在多个节点上同时加锁成功。如果你想更直观地验证互斥效果开两个线程同时抢同一个锁key等所有线程结束再看日志会看到只有一个线程成功进入临界区另一个线程在等待后返回false或者等锁释放后再进入。这种观测方式比只看理论描述更有说服力。5. 常见问题与排查技巧5.1 问题速查表把这段时间遇到的高频问题整理成一个速查表按“现象-原因-解决”三列排开方便遇到同样情况时快速定位。现象原因解决redis-server启动后立即闪退配置文件存在缩进、路径含中文、出现Windows不支持的参数检查配置格式换纯英文路径去掉daemonize yesredis-cli连接不上端口写错、bind限制、密码不匹配用ping命令逐个验证逐项排查连接配置端口被占用上一个实例没有退干净netstat -ano | findstr 6379taskkill对应PIDMultiLock一直获取锁失败某个节点不可用全成功语义触发分别验证三个节点的单锁加锁是否正常看门狗不续期tryLock里传了leaseTime看门狗自动关闭不传leaseTime交给看门狗处理续期unlock抛IllegalMonitorStateException锁已被到期释放或当前线程不持有锁释放前用isHeldByCurrentThread判断5.2 几个不容易发现的坑第一个坑是Windows配置文件的编码问题。用记事本改redis.windows.conf再保存很容易存成带BOM的UTF-8Redis解析时可能报错或者干脆读不到配置。建议用VS Code打开文件保存时直接选UTF-8无BOM或者干脆从干净的Linux配置模板改起。第二个坑是AOF路径。dir配置如果不写绝对路径Redis启动后会跑到当前工作目录去找数据文件在Windows服务化或从不同目录启动时特别容易出问题。我的习惯是dir和logfile全部写成绝对路径并且路径统一用正斜杠。第三个坑是MultiLock里所有RLock必须来自不同RedissonClient这个前面已经强调过。再补充一个容易混淆的情况三个RLock的key必须一致如果不一致MultiLock就退化成简单地对三个不同锁分别加锁完全丧失了“同一把锁跨节点”的语义。第四个坑是锁粒度。把整个订单模块用同一个key加锁并发一高全部请求排队系统的吞吐量瞬间归零。实战里应该按订单ID、用户ID、商品ID来拆锁让不同业务请求尽量不争抢同一个锁。5.3 性能与替代方案的个人建议MultiLock的代价在加锁速度上非常明显三个节点串行加锁延迟大约是三倍RTT再加上失败后的回收处理整体耗时远超单实例锁。如果业务对延迟敏感一定要提前做好压测确定这个增加的安全性是值得的。我个人在选型时有个倾向如果能依赖公司现成的Redis高可用集群优先使用Redisson普通分布式锁把高可用交给集群只有在需要跨多套独立Redis环境保证锁安全时才考虑MultiLock或RedLock。此外RedLock本身在学术界和工程界都存在争议很多人认为它仍然不够安全所以更要结合自己的故障模型来决策。面试层面把这几个概念讲清楚就够了普通锁依赖Redis高可用MultiLock是全节点成功RedLock是多数节点成功。如果还能补充一两个本地实验观察到的细节比如三节点里的锁数据结构完全一致、失败后补偿释放已加锁节点等那就是真正做过实验的人和只背理论的人之间的差距。我在本机把三台Redis跑起来做完整套实验后最大的感受是分布式锁的原理在纸面上看非常抽象一旦你看到了锁在Redis里的真实结构理解了看门狗的触发条件再亲手让MultiLock的三个节点同时加锁、同时释放所有概念一下子就串起来了。如果你也要做类似的实验我的建议是先单机锁跑通再加到三个节点最后去翻Redisson源码里RedissonLock和RedissonMultiLock这两个类顺着加锁Lua脚本往下走收获会比单纯背面试题大得多。再说一个最值得记住的教训锁的释放一定要写在finally里别把系统安全寄托在Redis超时兜底上——真等超时兜底时你的服务大概率已经被锁拖垮一阵子了。