分布式锁完全指南:从一次"两个人买走同一件库存"看懂 Redis 锁的所有坑

2023 年我负责电商库存扣减,逻辑很朴素:查库存、大于零就减一。Demo 毫无问题,直到一次大促对账发现可怕的事——一个标价 100 件的爆款,系统让 103 个人下单成功了,超卖。问题不在那三行业务,在并发:两个请求同时查到库存 1、同时判断大于零、同时各扣一次。我以为加把锁就行,用 synchronized 本地不超卖了,一上线超卖照旧——服务多实例部署,synchronized 只锁单个 JVM 进程。转向 Redis 分布式锁,坑反而更多。本文把分布式锁从头梳理。为什么需要:单机锁跨不过进程边界,锁必须放在所有实例共享的外部存储。加锁必须原子:SETNX 占位再单独 EXPIRE 设过期,两步之间崩溃会留下永久死锁,要用 SET key token NX PX 一条命令。最隐蔽的坑你删了别人的锁:A 业务超时锁自动过期、B 抢到锁、A 跑完一句 del 删掉了 B 的锁,要给每把锁写唯一 token 标记主人、释放前用 Lua 脚本原子地校验 owner 再删。锁会提前过期:业务耗时根本没法预估,固定过期时间设短了被趁虚而入、设长了崩溃后白占,用看门狗后台线程定期续期破解。别自己造轮子:Redisson 把原子加锁、token、Lua 释放、看门狗、可重入全封装好了。工程坑:锁粒度要按业务键细化别用一把大锁、抢不到锁要设等待超时快速失败兜底、Redis 主从异步复制加故障切换可能丢锁。核心一句:没有 100% 完美的分布式锁,它是一个有生命周期的东西,要被原子创建、持续续命、安全销毁,关键业务还要在数据层再加一道防线。

2023 年我负责一个电商的库存扣减功能。逻辑很朴素:用户下单时,先查一下这个商品还剩多少库存,大于零就把库存减一。单机测试、Demo 演示都毫无问题。直到一次大促,运营在后台对账时发现了一件可怕的事:某个爆款商品,系统记录卖出去的数量,比实际库存还多——超卖了。一个标价 100 件库存的商品,系统让 103 个人下单成功了。我第一反应是去看那段扣减代码,它明明就是"查库存、判断、减一",哪来的错?盯着日志复盘很久才想明白:问题不在那三行业务逻辑,而在并发。两个请求几乎同时进来,同时查到库存是 1,同时判断"大于零",于是同时各扣一次——库存变成了 -1,两个人都买到了那"最后一件"。我心想这不简单吗,加个锁不就行了。我先用 Java 的 synchronized 加了锁,本地一测,果然不超卖了。可一上线,超卖照旧。我又懵了。后来才反应过来:我们的服务是多实例部署的,synchronized 只能锁住一个 JVM 进程内部,实例 A 的锁和实例 B 的锁互不相干,跨进程的并发它根本管不着。于是我转向"分布式锁",用 Redis 来做。可接下来这条路上的坑,比我想象的多得多:锁加上了却不是原子的、锁会自己提前过期、我甚至亲手删掉了别的线程的锁……每一个坑都让超卖换一种方式卷土重来。那次之后我才认真把分布式锁这件事从头搞明白。这篇文章就把它梳理一遍:为什么需要分布式锁、怎么加锁才对、锁为什么会提前过期、为什么你会删掉别人的锁,以及把分布式锁真正做稳要避开的那些坑。

问题背景

先把那次事故的现象和我的误判讲清楚,后面所有的设计都是冲着纠正这个误判去的。

现象:多实例部署的服务做库存扣减,高并发下出现超卖——系统卖出的数量超过了实际库存。用 synchronized 加锁后本地不超卖,上线后超卖照旧。改用 Redis 分布式锁,又接连出现锁不原子、锁提前过期、误删他人锁等问题。

我当时的错误认知:"并发问题加把锁就解决了;分布式环境就把锁换成 Redis 的 SETNX,加上、用完删掉,就这么简单。"

真相:分布式锁真正的难点,根本不在"加锁"那一下。SETNX 谁都会写。难的是锁的整个生命周期:加锁和设过期时间必须是原子的、锁的过期时间和业务耗时根本对不齐、释放锁时你怎么确认删的是自己那把。一把分布式锁的正确性,是由"加锁、持有、续期、释放"这四个环节共同保证的,任何一环偷懒,互斥就会在某个并发时刻悄悄失效。

要把分布式锁做稳,需要几块认知:

  • 为什么单机锁(synchronized)在多实例下必然失效;
  • 为什么"SETNX 占位 + EXPIRE 设过期"这两步会出大事;
  • 锁为什么会在业务还没干完时就提前过期,看门狗怎么救;
  • 为什么你会在不知情的情况下删掉别的线程的锁;
  • 可重入、锁粒度、Redis 单点这些工程坑怎么处理。

一、为什么需要分布式锁:单机锁跨不过进程边界

先看清我第一版用 synchronized 为什么注定失败,这决定了后面所有方案的方向。

synchronized(以及 ReentrantLock 这些)是JVM 进程内的锁。它的工作原理,是在当前这个 Java 进程的内存里,对一个对象做标记——同一个进程里的多个线程来抢,它能保证只有一个抢到。注意"同一个进程"这个前提:

// 反面教材:用 JVM 的 synchronized 给库存扣减加锁。
public synchronized void deductStock(Long skuId) {
    int stock = stockMapper.selectStock(skuId);   // 查库存
    if (stock > 0) {
        stockMapper.updateStock(skuId, stock - 1); // 减一
    }
}
// 问题:synchronized 锁的是"当前这个 JVM 进程内"的对象。
// 服务一旦多实例部署(实例 A、实例 B 各是一个独立的 JVM 进程),
// A 的锁和 B 的锁是两个毫不相干的东西 ——
// 实例 A 的某个线程和实例 B 的某个线程,可以同时进入这段代码。超卖照旧。

现代的后端服务,为了扛流量、为了高可用,几乎都是多实例部署的:同一份代码,起好几个进程,可能还分布在好几台机器上,前面用负载均衡把请求分摊过来。这个架构下,"同一个进程"这个前提就被打破了——一个用户的请求落到实例 A,另一个落到实例 B,它们是两个独立的 JVM,各自的 synchronized 拦不住对方。

所以我们需要的,是一把所有实例都能看见、都来这里抢、谁抢到了别人就一定知道的锁。这把锁不能放在任何一个进程的内存里,它必须放在一个所有进程共享的、外部的地方。这个"外部的地方",最常见的选择就是 Redis——它本身就是个所有实例共享的内存存储,而且单线程执行命令,天然适合做这种"抢占"。这就是分布式锁的出发点。

二、加锁必须原子:SETNX + EXPIRE 的陷阱

用 Redis 做锁,核心思路是"占坑":往 Redis 里写一个约定好的 key,写成功就代表抢到了锁,别人再写同一个 key 会失败。Redis 的 SETNX(SET if Not eXists,不存在才设置)正是干这个的。

但这里立刻有个问题:如果只是 SETNX 写了个 key,那持有锁的进程万一崩溃了,这个 key 就永远留在 Redis 里,锁永远不会释放,后面所有人都卡死——这叫死锁。所以锁必须有过期时间作兜底:就算持有者崩了,过一会儿锁也能自动消失。于是很自然地,我写出了下面这版"先占位、再设过期":

// 反面教材:加锁分两步 —— 先 setnx 占位,再单独 expire 设过期。
public boolean tryLockBad(String key) {
    Long ok = jedis.setnx(key, "1");        // 第一步:占位,成功返回 1
    if (ok == 1) {
        jedis.expire(key, 30);              // 第二步:给它设 30 秒过期
        return true;
    }
    return false;
}
// 致命问题:这两步不是一个原子操作。如果进程恰好在
// "setnx 已成功、expire 还没来得及执行"的那一瞬间崩溃,
// 这把锁就变成了一把没有过期时间的锁 —— 它会永远占着,
// 谁也拿不到,谁也清不掉。死锁。

这个 bug 极其隐蔽:两步之间的窗口只有几微秒,平时跑一百万次都不会出事,可一旦在那个窗口里赶上进程崩溃、被 kill、或 GC 长暂停,就会留下一把永久死锁。它和上一篇缓存里"SETNXEXPIRE 分两步"是同一类病根:两个本该一起成功或一起失败的操作,被拆成了两步

正确的做法是把"占位"和"设过期"合并成一条命令。Redis 的 SET 命令支持 NXPX 参数,一条命令同时完成两件事,而单条 Redis 命令的执行是原子的:

// 正确做法:用一条 SET 命令同时完成"占位 + 设过期",保证原子性。
//   NX:仅当 key 不存在时才设置 —— 这实现了互斥
//   PX:设置过期时间(毫秒)—— 它和占位是同一条命令、同一个原子操作
public boolean tryLock(String key, String token, long ttlMillis) {
    SetParams params = new SetParams().nx().px(ttlMillis);
    String result = jedis.set(key, token, params);
    return "OK".equals(result);   // 返回 OK 表示这条 SET 生效了,抢锁成功
}

注意这里我没有像反面教材那样写一个固定的 "1" 当 value,而是传了一个 token 参数。这个 token 是什么、为什么必须是它,是下一节的主题——它正是解决"误删别人锁"那个坑的关键。

三、你删了别人的锁:owner 校验与 Lua 原子释放

加锁解决了,接着是释放。释放锁,直觉上不就是把那个 key 删掉吗?

// 反面教材:释放锁时,直接把 key 删掉。
public void unlockBad(String key) {
    jedis.del(key);   // 直接删除,简单粗暴
}

这行 del 藏着分布式锁里最隐蔽的一个坑。我们顺着一个时间线看:线程 A 抢到了锁,锁的过期时间是 30 秒;但 A 的业务跑得慢,跑了 35 秒——在第 30 秒,A 的锁自动过期了;第 31 秒,线程 B 来抢,key 已经不存在,B 成功抢到了锁,开始干自己的活;第 35 秒,A 终于跑完了,它老老实实地执行 jedis.del(key)——可它删掉的,是 B 的锁!于是第 36 秒,线程 C 又能抢到锁了,此刻 B 和 C 同时持有锁。互斥,就在这一连串"看似合理"的步骤里彻底失效了。

问题的根源是:del 这个动作,分不清这把锁到底是不是自己的。要解决它,就得让每把锁带上"主人是谁"的标记——这就是上一节那个 token 的用途:加锁时,每个线程生成一个全局唯一的 token(比如 UUID)写进 value;释放锁时,先读出 value、确认它等于自己的 token,才删

但"先读、再比、再删"又是三步——如果在"比对通过"和"执行删除"之间锁恰好过期、被别人抢走,你还是会删掉别人的锁。这三步也必须打包成一个原子操作。Redis 给的工具是 Lua 脚本:Redis 会把整段 Lua 脚本作为一个不可分割的整体来执行,中途绝不会插进别的命令。下面是一个把这些都做对的完整锁实现:

public class RedisLock {
    private final Jedis jedis;

    // Lua 脚本:先比对 value 是不是自己的 token,是才删 —— 整段原子执行
    private static final String UNLOCK_LUA =
        "if redis.call('get', KEYS[1]) == ARGV[1] " +
        "then return redis.call('del', KEYS[1]) else return 0 end";

    public RedisLock(Jedis jedis) { this.jedis = jedis; }

    public String tryLock(String key, long ttlMillis) {
        // token 用全局唯一值,作为"这把锁属于谁"的凭证
        String token = UUID.randomUUID().toString();
        SetParams params = new SetParams().nx().px(ttlMillis);
        boolean ok = "OK".equals(jedis.set(key, token, params));
        return ok ? token : null;       // 抢到锁,返回 token;没抢到返回 null
    }

    public boolean unlock(String key, String token) {
        // 把"判断 owner + 删除"交给 Lua,一个原子操作完成,杜绝误删
        Object r = jedis.eval(UNLOCK_LUA,
                Collections.singletonList(key),
                Collections.singletonList(token));
        return Long.valueOf(1).equals(r);
    }
}

用的时候,tryLock 返回的 token 一定要保存好,释放时原样传回 unlock,并且释放动作务必放在 finally 里——否则业务一抛异常,锁就只能傻等过期了。但你可能已经发现:这套方案里还有个没解决的问题,就是上面那个时间线的起点——"A 的业务跑了 35 秒,锁 30 秒就过期了"。锁提前过期本身,就是个大麻烦。

四、锁会提前过期:看门狗自动续期

上一节那个误删的时间线,真正的起因是一个我们无力回答的问题:锁的过期时间该设多久?

设短了,业务还没干完锁就过期了,别的线程趁虚而入,互斥失效;设长了,万一持有锁的进程真的崩了,这把锁要白白占用很久,期间谁也别想干活。更要命的是,业务的真实耗时根本没法预估——平时一次库存扣减 50 毫秒,但赶上数据库慢查询、网络抖动、GC 长暂停,它可能突然要跑 5 秒、10 秒。你设任何一个固定值,都是在赌"业务一定在这个时间内跑完",而这个赌注迟早会输。

业界对这个两难的标准解法,是看门狗(watchdog)机制。思路很巧:加锁时先给一个不长的过期时间(比如 30 秒);同时,起一个后台线程,每隔一小段时间(通常是过期时间的 1/3,即 10 秒)就去检查——只要业务还在跑,就把锁的过期时间重新延长回 30 秒。这样一来:业务正常跑着,锁就被这条后台线程一次次"续命",永不过期;而一旦持有锁的进程崩溃,这条后台线程也跟着没了,再没人续期,锁就会在最多 30 秒后自然过期——死锁兜底依然成立。它同时拿到了"锁不会提前过期"和"崩溃能自动释放"两个好处。

// 看门狗:加锁后起一个后台线程,定期把锁的过期时间往后延。
// 业务没跑完就一直续,业务跑完(或进程崩了)就停止续 —— 锁自然到期。
public class LockWatchdog {
    private final ScheduledExecutorService scheduler =
            Executors.newSingleThreadScheduledExecutor();

    // 续期脚本:同样要先校验 owner —— 只续"自己还持有的"那把锁
    private static final String RENEW_LUA =
        "if redis.call('get', KEYS[1]) == ARGV[1] " +
        "then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";

    public ScheduledFuture<?> start(Jedis jedis, String key,
                                    String token, long ttlMillis) {
        // 每过 ttl 的 1/3 续一次:留足提前量,别等锁快过期了才动手
        return scheduler.scheduleAtFixedRate(
            () -> jedis.eval(RENEW_LUA,
                    Collections.singletonList(key),
                    Arrays.asList(token, String.valueOf(ttlMillis))),
            ttlMillis / 3, ttlMillis / 3, TimeUnit.MILLISECONDS);
    }
}
// 业务结束、释放锁时,记得 future.cancel(true) 停掉这条续期线程。

看门狗也不是没有代价:如果持有锁的进程没崩、但卡死了(比如陷入死循环),看门狗会一直忠实地续期,这把锁就真的永远不会释放了。所以实践中通常还会再加一道保险——给锁一个"绝对持有上限",续期最多续到某个总时长就不再续。但无论如何,看门狗已经把"固定过期时间"这个最大的坑填上了。到这里,一把正确的分布式锁需要的所有零件——原子加锁、owner 校验、Lua 释放、自动续期——都凑齐了。

五、别自己造轮子:用 Redisson

看完前面四节你应该有个感受:一把正确的分布式锁,要操心的细节多得吓人。原子加锁、唯一 token、Lua 原子释放、看门狗续期、续期里还要再校验 owner……自己手写,极容易在某个角落留下 bug。

好消息是,这些坑前人早就踩遍并封装好了。Java 生态里,Redisson 就是事实标准——它是一个基于 Redis 的工具库,提供的 RLock 把上面所有细节全部包在了内部。你只管像用 JDK 的锁一样用它:

// 生产环境别自己造轮子:Redisson 把前面所有坑都封装好了。
RedissonClient redisson = Redisson.create(config);

public void deductStock(Long skuId) {
    RLock lock = redisson.getLock("stock:lock:" + skuId);
    // lock() 内部已包含:原子加锁 + 唯一 token + 看门狗自动续期
    lock.lock();
    try {
        int stock = stockMapper.selectStock(skuId);
        if (stock > 0) {
            stockMapper.updateStock(skuId, stock - 1);
        }
    } finally {
        // unlock() 内部:校验 owner + Lua 原子释放,绝不会误删别人的锁
        lock.unlock();
    }
}

Redisson 还顺手解决了一个我们前面一直没提的问题:可重入。设想方法 A 加了锁,在锁里又调用了方法 B,而 B 内部也想加同一把锁——如果是裸 SETNX 实现,B 会发现 key 已存在而拿不到锁,于是当前线程把自己锁死了。可重入锁能识别"来加锁的还是同一个线程",允许它重复加锁,内部用一个计数器记录加了几层,解锁时减到零才真正释放:

// Redisson 的锁是可重入的:同一线程可重复 lock 同一把锁,不会自己锁死自己。
public void outer(Long skuId) {
    RLock lock = redisson.getLock("stock:lock:" + skuId);
    lock.lock();                 // 第 1 次加锁,重入计数:0 -> 1
    try {
        inner(skuId);            // 调用内层方法,它还会再 lock 同一把锁
    } finally {
        lock.unlock();           // 重入计数:1 -> 0,锁此刻才真正释放
    }
}

private void inner(Long skuId) {
    RLock lock = redisson.getLock("stock:lock:" + skuId);
    lock.lock();                 // 同一线程再次加锁,重入计数:1 -> 2
    try {
        // ... 内层业务逻辑 ...
    } finally {
        lock.unlock();           // 重入计数:2 -> 1,锁还没释放
    }
}
// 若用裸 SETNX 自己实现,inner 会因为 key 已存在而拿不到锁 —— 线程自我死锁。

结论很直接:除非是学习原理,生产环境请直接用 Redisson 这类成熟方案。前面四节手写代码的价值,不在于让你照抄,而在于让你看懂 Redisson 内部到底在替你操心什么——这样它出问题时你才排查得动。

六、工程坑:锁粒度、加锁失败兜底、Redis 单点

锁本身做对了,但要把它真正用进生产,还有几个绕不开的工程坑。

坑 1:锁粒度要尽量细。一个常见错误是用一把"大锁"锁住所有商品的库存扣减——那意味着商品 X 的下单和商品 Y 的下单会互相排队,完全不相干的请求被硬生生串行化,吞吐量被锁死。正确的做法是按业务键加锁:锁的 key 里带上 skuId(像前面代码里的 "stock:lock:" + skuId),不同商品用不同的锁,互不阻塞。锁的范围越小,并发能力越强。

坑 2:抢不到锁,要有兜底,不能无限等。高并发下,大量请求会抢同一把锁,抢不到的怎么办?如果让它们都无限期阻塞等待,请求会越堆越多,最终拖垮整个服务。务必给加锁设一个等待超时:在限定时间内没抢到,就快速失败、返回一个友好的兜底结果(对秒杀就是"系统繁忙,请重试"),绝不让请求无止境地堆积:

// 工程实践:加锁要设"等待超时",抢不到就快速失败,别让请求无限堆积。
public boolean deductWithTimeout(Long skuId) throws InterruptedException {
    RLock lock = redisson.getLock("stock:lock:" + skuId);
    // 最多花 3 秒去抢锁;抢到后锁的持有上限 10 秒(兜底,防看门狗失效)
    boolean got = lock.tryLock(3, 10, TimeUnit.SECONDS);
    if (!got) {
        // 没抢到:直接返回"系统繁忙",把请求快速放掉,不堆积
        return false;
    }
    try {
        int stock = stockMapper.selectStock(skuId);
        if (stock <= 0) return false;
        stockMapper.updateStock(skuId, stock - 1);
        return true;
    } finally {
        lock.unlock();
    }
}

坑 3:Redis 本身是单点。我们把锁放在 Redis 上,可如果这台 Redis 挂了呢?锁服务就整个没了。生产环境的 Redis 至少要主从加哨兵。但这里还藏着一个更微妙的问题:Redis 主从是异步复制的——线程 A 在主节点上加锁成功,主节点还没来得及把这条数据同步给从节点就宕机了,从节点被哨兵提升为新主,而新主上根本没有这把锁;于是线程 B 来加锁,也成功了——两个线程同时持有了"同一把"锁。

坑 4:Redlock 与它的争议。为了解决上面这个主从切换丢锁的问题,Redis 作者提出过 Redlock 算法:部署多个独立的 Redis 节点(不是主从),加锁时要往大多数节点(比如 5 个里的 3 个)都加成功才算数。但 Redlock 在分布式领域是有争议的——有专家指出它在时钟漂移、GC 暂停等极端情况下依然不能保证绝对安全。这里你需要记住的结论是:没有 100% 完美的分布式锁。对绝大多数业务,Redisson + 主从哨兵已经足够;但如果你的场景绝对不能容忍哪怕极小概率的互斥失效(比如涉及资金),那正确的做法不是去迷信某个更复杂的锁,而是在最终的数据落地处再加一道防线——比如给库存扣减的 SQL 加上 WHERE stock > 0 的乐观条件,让数据库本身成为最后一道不会骗人的关卡。

下面这张图,把一把分布式锁从抢占、续期到释放的完整生命周期串起来:

关键概念速查

概念 / 手段 说明
单机锁的局限 synchronized 只锁单个 JVM 进程,多实例部署下完全失效
分布式锁 把锁放在 Redis 等所有实例共享的外部存储,实现跨进程互斥
SET NX PX 一条命令原子完成"占位 + 设过期",取代不原子的 SETNX+EXPIRE
过期时间兜底 锁必须设过期,否则持有者崩溃会留下永久死锁
owner token 每把锁写入唯一 token 标记主人,释放前校验,防误删他人锁
Lua 原子释放 把"判断 owner + 删除"打包成 Lua 脚本,整段原子执行
看门狗续期 后台线程定期延长过期时间,业务没完就续、进程崩了自然过期
可重入 同一线程可重复加同一把锁,用计数器记层数,防自我死锁
锁粒度 按业务键(如 skuId)加锁,不同对象用不同锁,别用一把大锁
Redlock 争议 多节点加锁算法,仍不保证绝对安全;关键业务要在数据层再兜底

避坑清单

  1. synchronized、ReentrantLock 只在单个 JVM 进程内有效,服务多实例部署后必然拦不住跨进程并发。
  2. 分布式锁要放在所有实例共享的外部存储(如 Redis),"写 key 成功"即"抢锁成功"。
  3. 锁必须带过期时间兜底,否则持有者崩溃会留下永久死锁。
  4. 加锁的"占位"和"设过期"必须原子完成,用 SET key token NX PX 一条命令,别用 SETNX + EXPIRE 两步。
  5. 释放锁前必须校验 owner:每把锁写入唯一 token,只删 value 等于自己 token 的锁,否则会误删别人的锁。
  6. "判断 owner + 删除"要用 Lua 脚本原子执行,否则判断和删除之间的窗口仍会误删。
  7. 固定过期时间无法兼顾"业务跑完"和"崩溃释放",用看门狗后台线程定期续期来破解这个两难。
  8. 锁要可重入,否则方法嵌套加同一把锁会让线程自我死锁;生产环境直接用 Redisson 等成熟方案。
  9. 锁粒度要细,按业务键加锁,别用一把大锁串行化所有不相干请求;抢不到锁要设等待超时并快速失败兜底。
  10. Redis 主从异步复制 + 故障切换可能丢锁;没有 100% 完美的分布式锁,关键业务要在数据层(如 WHERE stock > 0)再加一道防线。

总结

回头看那次超卖事故,以及后面我在分布式锁这条路上接连踩的坑,最该记住的不是某一段 Lua 脚本,而是我动手前那个想当然的判断——"并发问题加把锁就解决了"。这句话错在它把"锁"理解成了一个一加上就生效的开关。可一把分布式锁根本不是开关,它是一个有生命周期的东西:它要被原子地创建、要在持有期间被妥善照看、要在不确定的耗时里被持续续命、要被它真正的主人安全地销毁。这中间任何一个环节你没照看到,它就会在某个并发的瞬间悄悄失效——而失效的锁,比没有锁更危险,因为它会让你误以为自己有保护

所以做分布式锁,真正的工程量根本不在 SETNX 那一下。那行命令谁都会写,Demo 里它也确实能挡住并发。真正的工程量在加锁之后:这把锁会不会因为我没原子地设过期而变成永久死锁?会不会因为业务跑太久而提前过期、让别人趁虚而入?我释放它的时候,删的到底是不是我自己那一把?这篇文章的几节,其实就是顺着一把锁的生命周期展开的:先认清单机锁为什么跨不过进程边界;再看加锁为什么必须原子;然后是它会提前过期、会被误删这两个最隐蔽的坑;接着是看门狗续期、用 Redisson 这样的成熟方案;最后是锁粒度、兜底、单点这些工程细节。

你会发现,这套思路和我们处理任何"共享资源"的工程经验是相通的。我们不相信一个写操作一定原子,所以有事务;不相信一个连接一定健康,所以有探活;不相信一次调用一定成功,所以有重试和超时。分布式锁只是把这种"不轻易相信"用在了"互斥"这件事上——你不能相信锁加上了就一定互斥,你得追问它在崩溃时、在超时时、在主从切换时分别会怎样,并为每一种"不互斥"的可能准备好应对。也正因为没有 100% 完美的锁,真正稳的系统从不把宝全押在锁上,而是在最终的数据落地处再留一道关卡。

最后想说,分布式锁做没做对,差距永远不会在 Demo 里暴露——Demo 的并发量不够、进程不会崩、业务跑得飞快、Redis 也不会故障切换,怎么写都不超卖。它只在真实的大促流量、真实的进程崩溃、真实的慢查询、真实的主从切换面前才显形。那时候它会用最难看的方式给你结账:一个标价 100 件的商品,系统让 103 个人付了款。所以别等运营拿着对不上的账目来找你,在你写下第一行 jedis.set 的时候就该想清楚:这把锁原子吗?它会提前过期吗?我释放的是我自己的锁吗?它背后的 Redis 倒下时会怎样?这几个问题都有了答案,你的分布式锁才不只是 Demo 里那个看起来很安心的开关,而是一道在洪峰、在崩溃、在故障切换面前都真正守得住的互斥防线。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

LLM 流式输出完全指南:从一次"前端等了 20 秒白屏"看懂 SSE 流式响应

2026-5-21 18:41:43

技术教程

Function Calling 完全指南:从一次"AI 调用了一个根本不存在的接口"看懂工具调用

2026-5-21 18:54:15

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索