Redis怎么实现分布式锁的?

1 个回答

  • 水云
    自己喜欢的东西,就不要问别人好不好。
    从获取→续租→释放 # 1) 核心思路(租约型锁) * **原子加锁**:一次命令同时“占坑+设置过期” ```redis SET lock:key <token> NX EX <ttl> ``` * `NX`:不存在才设置(互斥) * `EX/PX`:过期时间(**必须**,防死锁) * `<token>`:**随机唯一标识**(区分锁持有者) * **释放锁**:先比对 token 再删除,避免删到别人的锁(Lua 原子脚本) ```lua -- unlock.lua if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end ``` * **续租(看门狗)**:业务可能超过初始 TTL,需定期续期(仅持有者能续) ```lua -- renew.lua if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) -- 续到新的 ttl(ms) else return 0 end ``` * **获取失败的等待**:**指数退避+抖动**重试,设总等待上限(避免惊群与长阻塞)。 # 2) Java 实用伪码(AutoCloseable) ```java class RedisLock implements AutoCloseable { final String key, token = UUID.randomUUID().toString(); final int ttlMs; volatile boolean locked; ScheduledFuture<?> renewTask; boolean tryLock(int waitMs) { long end = System.currentTimeMillis() + waitMs; while (System.currentTimeMillis() < end) { String ok = jedis.set(key, token, SetParams.setParams().nx().px(ttlMs)); if ("OK".equals(ok)) { locked = true; startWatchdog(); // 长任务建议开启 return true; } Thread.sleep(ThreadLocalRandom.current().nextInt(5, 30)); // 抖动退避 } return false; } void startWatchdog() { renewTask = scheduler.scheduleAtFixedRate(() -> { jedis.evalsha(renewSha, Collections.singletonList(key), Arrays.asList(token, String.valueOf(ttlMs))); }, ttlMs/2, ttlMs/2, TimeUnit.MILLISECONDS); } @Override public void close() { if (renewTask != null) renewTask.cancel(true); jedis.evalsha(unlockSha, Collections.singletonList(key), Collections.singletonList(token)); locked = false; } } ``` # 3) 关键细节 & 典型坑 1. **TTL 必须有**:`SET NX EX` 一条命令完成;否则宕机会死锁。 2. **释放必须校验 token**:老持有者卡顿后晚到的 `DEL` 可能删掉新锁;必须用 Lua 比对。 3. **续租也要校验 token**:只允许当前持有者续租,避免续别人的锁。 4. **GC/网络抖动**:可能导致锁过期被别人拿走,而旧持有者晚到继续写 → **加入 Fencing Token(围栏令牌)**: * 获取锁时用 `INCR lock:seq` 生成单调递增 `tokenId`,写入下游资源时**只接受更大 token**,拒绝过期持有者的写。 5. **主从复制一致性**:Redis 复制是**异步**,主写成功但未复制就主挂了,提升从库→可能出现**锁丢**。 * 降风险:短 TTL + 续租;或获取后执行 `WAIT 1 <ms>` 等待至少1个副本确认(牺牲延迟提升安全)。 * 对一致性极其敏感场景,考虑 **etcd/ZooKeeper**(线性一致 CAS)。 6. **惊群**:大量竞争者同时被唤醒猛撞,加入**抖动退避**、本地 singleflight/队列限流。 7. **可重入**:业务需要可重入时,以线程/请求维度维护计数(同 token 下 `count++/- -`),释放到 0 再真正解锁。 # 4) Redlock 要不要用? * **Redlock**:在多个独立 Redis 实例上取多数派成功才算加锁,用于应对单机/主从切换问题。 * 社区对其在实际网络模型下的安全性有争议;如果你的业务对一致性要求很强,**优选 etcd/ZK**。如果仍用 Redis,**单机主 + WAIT 副本确认 + Fencing Token** 往往更简单可控。 # 5) 何时选 Redis 锁 / 何时别用 * **适合**:跨进程互斥、短临界区、允许极小概率失效且有业务幂等/对账兜底。 * **不适合/谨慎**:强一致全局单主、金融强约束关键路径 → 用 **etcd/ZK**(`lease + keepAlive + compare-and-swap`)。 # 6) 最小化清单(上线必做) * `SET key val NX PX ttl` 获取; * `unlock.lua` 比对 token 删除; * 长任务开 **watchdog** 定期 `renew.lua`; * 竞争用**退避+抖动**; * 重要写路径加 **Fencing Token**; * 主从环境关键路径可加 `WAIT`; * 业务层**幂等**,即便锁边界条件失效也不致命。 **一句话总结** > Redis 分布式锁的正确姿势 = **原子占坑(SET NX EX)+ 鉴权释放(Lua)+ 自动续租(watchdog)+(可选)副本确认与 Fencing Token**。强一致需求优先 etcd/ZK,普通互斥场景这套就够稳。
    发布于 2025-08-12 05:10 •IP属地 安徽
    举报
    评论
    收藏