Redis 主从切换导致分布式锁丢失的解决方案?

1 个回答

  • 格列柯南
    杯中之物从来不能消愁补过
    问题的根因先说清:**Redis 复制是异步的**。当你在主库 `SET key val NX PX ttl` 成功拿到锁,但**还没来得及复制到从库**主库就挂了/切主了——新主没有这把锁,于是**两方都可能认为自己持有锁** → 锁丢失。 # 一、降低“切主窗口”里丢锁的概率 ### 1)SET 之后立刻做副本确认(**WAIT**) * 流程: 1. `SET key token NX PX ttl` 成功; 2. 立刻 `WAIT 1 <timeout_ms>`(或 `WAIT 2`,取决于你有几台副本)。 3. 如果返回的副本数 < 期望值 → **立即释放锁**(Lua 比对 token `DEL`)并**重试**。 * 作用:把“主写成功从没收到”的窗口,缩到 **WAIT 超时**这几十\~几百毫秒的概率事件。 * 代价:增加一次阻塞等待(延迟↑)。 ### 2)开启“最少副本可写”保护(**min-replicas-to-write**) ```conf min-replicas-to-write 1 min-replicas-max-lag 5 ``` 当在线副本数不足或复制延迟过大时,主库会**拒绝写入**(包括加锁),宁可暂时不可写,也不承担大窗口的数据丢失风险。 ### 3)短 **TTL + 看门狗续租** * 初次 TTL 设得**偏短**(如 10\~30s),并用 watchdog 定期 `PEXPIRE` 续租(Lua 脚本校验 token 才能续)。 * 目的:就算真的丢锁,新主上**很快就能重新拿到锁**,缩小“伪持有者”的有效期。 ### 4)部署与持久化 * **主从跨可用区**、Sentinel/Cluster 节点分散,减少同源故障。 * 开启 **AOF everysec** 或混合持久化,降低进程崩溃后“重启就丢锁”的概率(注意:持久化对主从切换窗口**不兜底**,只是重启恢复更稳)。 # 二、即便“锁丢”,也不让业务写坏(**强烈建议**) ### 5)引入 **Fencing Token(围栏令牌)** * 获取锁时,原子递增一个全局计数:`INCR lock:seq -> tokenId`。 * 将 `tokenId` 传给下游**被保护资源**(DB/队列/存储),**只接受更大的 token** 的写入: ```sql -- 资源侧只接受更大的 token UPDATE resource SET token=?, ... WHERE ? > token; ``` * 作用:就算旧持有者因为切主“回魂”,它的 token **更小**,写入会被拒绝,**业务不被破坏**。 ### 6)持有期“心跳失败就自杀” * 加锁后定期续租;**任一次续租失败**(键不存在/GET!=token/角色切换异常),**立刻中断临界区**并回滚/退出。 * 旧持有者在切主后会发现**续租失败**,不能继续执行。 ### 7)业务幂等与回滚/对账 * 临界区落库操作**必须幂等**、带唯一键; * 外发消息/调用都要么成功要么可重试,有“补账/对账”兜底。 # 三、释放锁的一定之规(避免删错别人的锁) * **必须**用 Lua 原子校验 token 再 `DEL`: ```lua if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end ``` * 续租也要校验 token(只有持有者能续)。 # 四、Redlock 要不要上? * **Redlock**:在 N 个独立 Redis 实例上取多数派才算成功,旨在降低单点/切主风险。 * 现实网络模型下其严格性有争议;如果你的场景需要**强一致**的锁(金融强约束、全局单主),优先**etcd/ZooKeeper**(`lease + keepAlive + compare-and-swap`,线性一致)。 * 如果仍用 Redis,\*\*“SET+WAIT + Fencing Token + 短TTL+续租”\*\*更易实施且效果好。 # 五、可直接复用的加锁流程(伪代码) ```java // 1) 尝试获取 OK = SET key token NX PX ttl if (!OK) return false; // 2) 副本确认(降低切主窗口) int ack = WAIT replicas timeoutMs if (ack < replicas) { // 复制保障不足,释放并重试 EVAL unlock.lua key token return retry(); } // 3) 发号(围栏令牌) long fencing = INCR lock:seq; // 4) 启动续租(心跳),失败则中止临界区 startWatchdog(() -> { long r = EVAL renew.lua key token ttlMs; if (r == 0) abortCriticalSection(); }); // …临界区对下游写入时携带 fencing,资源侧只接受更大 token… // 5) 完成后释放 EVAL unlock.lua key token ``` # 六、最小可执行清单(落地用这个) * `SET key token NX PX ttl` + **Lua 解锁**(必备) * `WAIT N <timeout>`(建议) * `min-replicas-to-write / min-replicas-max-lag`(建议) * **短 TTL + 看门狗续租**(建议) * **Fencing Token**(强烈建议,业务级兜底) * 续租失败即**终止临界区** * 业务操作**幂等**,必要时对账/补偿 * 对强一致锁:**etcd/ZK** 替代 Redis **一句话总结** > 切主导致锁丢是 Redis 异步复制的本性。工程上用 **SET+WAIT** 降低丢锁概率,用 **Fencing Token + 续租失败自杀 + 幂等** 保业务不被写坏;对极致一致性,换 **etcd/ZK**。
    发布于 2025-08-13 11:17 •IP属地 福建
    举报
    评论
    收藏