善忘技术夹 Logo
善忘技术夹
后端与运维

单机 synchronized 不管用?一文搞懂基于 Redis 与 ZooKeeper 的分布式锁

一个秒杀系统,库存只有 100 件,10000 个人同时抢。

单机时代,写 synchronizedReentrantLock 就能保证同一时刻只有一个线程修改库存。

但系统上线后变成了 3 台服务器。synchronized 只能锁住同一台 JVM 里的线程——服务器 A 的线程 1 和服务器 B 的线程 2,可以同时扣库存。 库存从 100 变成了 -50。

单机锁锁不住分布式环境下的资源竞争。 你需要一把”跨服务器的锁”——分布式锁。

白话版

插图:单机锁vs分布式锁

单机锁和分布式锁的区别,就像宿舍插销和小区公共健身房的电子钥匙箱:

  • 单机锁(synchronized) — 宿舍卫生间的插销,只能锁住同一个房间的人
  • 分布式锁(Redis / ZooKeeper) — 小区公共健身房的电子钥匙箱。不同栋楼(不同服务器)的人要用同一个设施,都得去中央钥匙箱申请唯一钥匙,用完归还

所有服务器都去同一个”中央钥匙箱”申请锁,谁拿到锁,谁就有权限操作共享资源。

基于 Redis 的分布式锁

Redis 实现分布式锁,靠的是 SETNX 命令——“SET if Not eXists”(不存在才设置)。

最简单的实现

import redis

cache = redis.Redis()

def deduct_stock(product_id, quantity):
    lock_key = f"lock:stock:{product_id}"

    # SETNX:如果 key 不存在就设置,存在则返回失败
    # NX = 只在不存在时设置
    # EX = 过期时间(秒),防止死锁
    locked = cache.set(lock_key, "1", nx=True, ex=10)

    if not locked:
        # 没拿到锁,说明有其他线程在操作
        return False  # 返回失败,让调用方重试或等待

    try:
        # 拿到锁,安全操作共享资源
        stock = cache.get(f"stock:{product_id}")
        if not stock or int(stock) < quantity:
            return False

        cache.decrby(f"stock:{product_id}", quantity)
        return True
    finally:
        # 释放锁
        cache.delete(lock_key)

这个实现有什么问题?

问题场景后果
锁过期业务执行超过 10 秒,锁自动释放了其他线程拿到锁,同时操作,数据不一致
误删锁线程 A 的锁过期了,线程 B 拿到了锁,A 执行完了去删锁把 B 的锁删了,B 的锁白拿
死锁获取锁后程序崩溃,没执行到 finally 释放锁锁永远不释放

进阶实现:Redisson 看门狗机制

插图:Redis看门狗

Redis 官方推荐的 Java 客户端 Redisson,内置了看门狗(Watchdog) 机制解决锁过期问题:

// 使用 Redisson 的分布式锁
@Autowired
private RedissonClient redisson;

public boolean deductStock(String productId, int quantity) {
    RLock lock = redisson.getLock("lock:stock:" + productId);

    try {
        // 尝试加锁,最多等 5 秒,拿到锁后 30 秒自动释放
        if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
            // 看门狗机制:每 10 秒自动续期,锁不会"过期被误删"
            // 只要业务没执行完,锁就一直有效

            int stock = getStock(productId);
            if (stock < quantity) {
                return false;
            }
            deductStock(productId, quantity);
            return true;
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        // 释放锁(同时会关闭看门狗)
        lock.unlock();
    }
    return false;
}

看门狗的工作原理:

线程 A 获取锁,锁超时 30 秒
  → 看门狗启动,每 10 秒检查一次
  → 如果线程 A 还在运行,把锁的过期时间重置为 30 秒
  → 线程 A 执行完毕,释放锁,看门狗也关闭

线程 A 获取锁,锁超时 30 秒
  → 看门狗启动
  → 线程 A 代码有 bug,卡住了(死循环、无限等待)
  → 看门狗不断续期,但锁永远不释放
  → 其他线程永远拿不到锁
  → 这个问题怎么解决?别担心,看门狗续期也有上限
  → 如果线程 A 彻底挂了(JVM 崩溃),看门狗也停了,锁自然过期

基于 ZooKeeper 的分布式锁

插图:ZooKeeper锁

ZooKeeper 实现分布式锁的原理和 Redis 不同,它用的是临时顺序节点(Ephemeral Sequential Node)

// ZooKeeper 分布式锁的伪代码
public class ZkDistributedLock {

    private ZooKeeper zk;
    private String lockPath = "/locks/resource";

    public boolean tryLock() {
        // 1. 在 /locks/resource 下创建临时顺序节点
        //    比如 /locks/resource/lock_0000000001
        String currentNode = zk.create(
            lockPath + "/lock_",  // 路径
            null,                 // 数据
            EPHEMERAL_SEQUENTIAL  // 临时 + 顺序
        );

        // 2. 获取当前节点下的所有子节点,排序
        List<String> children = zk.getChildren(lockPath, false);
        Collections.sort(children);

        // 3. 判断自己是不是最小的节点
        if (currentNode.equals(children.get(0))) {
            // 是最小的节点 → 拿到锁 ✅
            return true;
        } else {
            // 不是最小的 → 监听前一个节点(等它释放)
            String previousNode = children.get(children.indexOf(currentNode) - 1);
            zk.exists(lockPath + "/" + previousNode, watcher);
            // 等待前一个节点释放
            return false;
        }
    }

    public void unlock() {
        // 删除临时节点 → 后一个节点会收到通知,拿到锁
        zk.delete(currentNode);
    }
}

ZooKeeper 锁的优势:

  • 不需要设置过期时间 — 临时节点会随着客户端会话断开自动删除,不会死锁
  • 顺序公平 — 先到的人先拿锁,不会有”抢锁”的竞争
  • 监听机制 — 释放锁时主动通知下一个等待者,不需要轮询

Redis 锁 vs ZooKeeper 锁

特性Redis 分布式锁ZooKeeper 分布式锁
性能极高(纯内存操作)中等(ZAB 协议,写入需过半确认)
可靠性中等(可能丢锁,需要 Redlock 算法)高(临时节点自动清理)
死锁靠过期时间防止会话断开自动删除
公平性非公平(抢锁)公平(顺序节点)
实现复杂度简单(SETNX 即可)较复杂
适用场景高并发、对一致性要求不极端的场景对一致性要求高、并发量不太大的场景

分布式锁的常见问题

1. 锁的粒度

插图:锁粒度

锁的粒度越细,并发能力越强,但实现越复杂。

❌ 锁整个库存系统:
   锁 "lock:stock" → 所有商品扣库存都串行化

✅ 锁单个商品:
   锁 "lock:stock:product_10086" → 不同商品互不影响

✅ 锁单个用户的单个商品:
   锁 "lock:stock:product_10086:user_999" → 同一个用户的操作串行化

2. 锁的可重入

同一个线程(同一个请求链)多次获取同一把锁,应该允许通过:

// Redisson 的可重入锁
RLock lock = redisson.getLock("lock:stock:10086");

lock.lock();  // 第一次获取锁 ✅
lock.lock();  // 同一个线程再次获取 ✅(可重入)

// 业务逻辑...

lock.unlock();
lock.unlock();  // 释放两次

3. 锁的续期

业务执行时间不可预测,不能依赖固定的过期时间。看门狗机制解决了这个问题——自动续期,直到业务完成。

小结

分布式锁的核心思路是:所有服务器统一去一个中央存储申请锁。

  • Redis 锁 — 用 SETNX + 过期时间,性能高,适合大多数场景
  • Redisson 看门狗 — 解决锁自动续期问题,防止锁过期被误删
  • ZooKeeper 锁 — 临时顺序节点,公平且不会死锁,适合对一致性要求高的场景

单机锁管线程,分布式锁管服务器。

关注「善忘技术夹」全媒体矩阵

扫描上方宣传海报二维码,第一时间获取最新技术文章、开源项目与免安装小程序体验。

善忘技术夹 微信公众号宣传图
微信公众号 (扫码关注)
善忘技术夹 微信小程序宣传图
微信小程序 (扫码即用)