分布式锁完全指南:从一次"加了锁还超卖、库存被扣成负数"看懂分布式锁

2022 年我做一个秒杀活动的库存扣减接口。逻辑很简单:用户下单先查库存够不够,够就扣减一个。第一版在单机上跑,为了防止两个请求同时扣减同一个库存导致超卖,我加了个进程内的锁 threading.Lock。本地压测单机上线都没问题。后来活动流量涨了,运维把服务部署成了三个实例挂在负载均衡后面。某次活动结束我对账,发现一件吓人的事:库存明明只有 100 件,却卖出去了 130 单,超卖了 30 件。我第一反应是锁没生效,翻代码 threading.Lock 明明好端端加着。我盯着看了很久才想明白:我加的那把锁是进程内的锁,三个实例是三个独立的进程,每个进程里都有一把自己的 threading.Lock,它们互相之间根本不知道对方的存在。实例 A 拿到 A 进程的锁,实例 B 拿到 B 进程的锁,各拿各的锁同时进了扣减逻辑同时把库存扣成负数。锁是加了但它只在一个进程内有效,拦不住跨进程的并发。要在多个进程多台机器之间互斥,必须用分布式锁,一把所有进程都看得见都来抢的进程之外的锁。本文从头梳理:为什么单机锁在多实例下会失效、分布式锁的本质是什么、锁为什么必须能自动释放、误删别人的锁是怎么发生的、校验和删除为什么必须原子,以及看门狗续期、获取锁超时、可重入这些把分布式锁真正做对要避开的坑。

2022 年我做一个秒杀活动的库存扣减接口。逻辑很简单:用户下单,先查库存够不够,够就扣减一个。第一版在单机上跑,为了防止两个请求同时扣减同一个库存导致超卖,我加了个进程内的锁——threading.Lock。本地压测、单机上线,都没问题。后来活动流量涨了,运维把服务部署成了三个实例,挂在负载均衡后面。某次活动结束我对账,发现一件吓人的事:库存明明只有 100 件,却卖出去了 130 单——超卖了 30 件。我第一反应是"锁是不是没生效",去翻代码,threading.Lock 明明好端端地加着。我盯着那段代码看了很久,才终于想明白:我加的那把锁,是进程内的锁。三个实例,是三个独立的进程,每个进程里都有一把自己的 threading.Lock,它们互相之间根本不知道对方的存在。实例 A 的请求,拿到的是 A 进程里那把锁;实例 B 的请求,拿到的是 B 进程里那把锁——它俩各拿各的锁,谁也拦不住谁,同时进了扣减逻辑,同时读到库存是 1,同时把它扣成 0、又扣成 -1。锁是加了,可它只在一个进程内有效,对跨进程的并发毫无办法。我后来才彻底想明白,第一版错在一个根本的认知上:我以为"加了锁,就能防并发"。可"进程内的锁"和"跨进程的锁",是两回事threading.Lock 这种锁,它的作用范围,就是它所在的那一个进程;一旦你的服务变成多实例、多进程,要在它们之间互斥,你需要的是一把所有进程都看得见、都来抢的、进程之外的锁——这就是分布式锁。我以为它不过是"RedisSETNX 一下",结果真做下来,坑一个接一个。这篇文章就把它梳理一遍:为什么单机锁在多实例下会失效、分布式锁的本质是什么、锁为什么必须能自动释放、误删别人的锁是怎么发生的、校验和删除为什么必须原子,以及看门狗续期、获取锁超时、可重入这些把分布式锁真正做对要避开的坑。

问题背景

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

现象:一个库存扣减接口,单机部署时用进程内的 threading.Lock 防并发,一切正常。服务扩成三个实例、挂上负载均衡后,出现超卖——100 件库存卖出了 130 单。代码里的锁好端端加着,却没拦住跨实例的并发。

我当时的错误认知:"只要加了锁,同一时刻就只有一个请求能进扣减逻辑,不会超卖。"

真相:threading.Lock 这类锁是进程内的,它的作用范围只有当前这一个进程。多实例部署下,每个进程都有自己独立的一把锁,互相看不见。要在多个进程、多台机器之间互斥,必须用分布式锁——一把存在于所有进程之外、所有进程都能看见并争抢的锁,通常用 Redis 实现。但分布式锁远不止"SETNX 一下"那么简单:锁会因为进程崩溃而永远不释放,会被别的进程误删,校验和删除不原子会出竞态,业务执行超时锁会提前过期——每一个都是坑。

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

  • 为什么进程内的锁,在多实例部署下会彻底失效;
  • 分布式锁的本质——一个所有进程都看得见的"标记";
  • 锁为什么必须带过期时间,否则持锁进程一崩就死锁;
  • 为什么锁要写唯一标识,以及释放为什么必须用 Lua 保证原子;
  • 看门狗续期、获取锁超时、可重入这些工程坑怎么处理。

一、为什么单机锁在多实例下会失效

先把这件最根本的事钉死:threading.Lock 这类锁,锁住的是"这一个进程内部"的并发;它对"进程之间"的并发,一无所知、也无能为力。

设想一下我那个场景:服务部署成三个实例,就是三个独立的操作系统进程,可能还在三台不同的机器上。每个进程启动时,都会各自创建一个 threading.Lock 对象。这三把锁,是三个毫不相干的对象,它们之间没有任何通道能互相通气。下面这段代码,就是我那个会超卖的第一版:

import threading

stock = 100               # 库存,这里用变量模拟
_lock = threading.Lock()  # 进程内的锁


def deduct_naive() -> bool:
    # 反面教材:threading.Lock 只在【当前这一个进程】内有效。
    with _lock:
        global stock
        if stock > 0:
            stock -= 1
            return True
        return False
    # 问题:服务部署成 3 个实例 = 3 个独立进程,
    # 每个进程都有自己的一把 _lock,互相看不见。
    # 实例 A 和实例 B 各拿各的锁,同时进到这里,
    # 同时把 stock 从 1 扣成 0、再扣成 -1 —— 超卖。

这段代码,在单机单进程下是完全正确的——同一个进程里,不管来多少个线程,都得排队抢那唯一的一把 _lock,临界区里永远只有一个线程。它的错,只在一个前提变了之后才暴露:当服务变成多实例。这时,实例 A 进程里的线程抢的是 A 的 _lock,实例 B 进程里的线程抢的是 B 的 _lock——它们抢的压根不是同一把锁,自然谁也排不住谁的队

所以问题的根子很清楚:互斥要生效,前提是所有竞争者抢的是同一把锁。单机锁做不到这一点,因为它天生只属于一个进程。要让分散在多个进程、多台机器上的请求抢同一把锁,这把锁就不能待在任何一个进程里面,它必须待在所有进程外面的某个公共的地方

二、分布式锁的本质:一个所有进程都看得见的"标记"

上一节的死结是:锁待在进程里面,就只有这个进程能用它。分布式锁的破局点就一句话:把锁挪到所有进程都能访问的、进程之外的公共存储里去

这个"公共存储",最常用的就是 Redis——所有实例都连着同一个 Redis。于是"锁"这个概念,就被翻译成了一件极朴素的事:在 Redis 里,约定一个 key 代表这把锁。谁能成功地把这个 key 创建出来,谁就算"拿到了锁";用完了,把这个 key 删掉,锁就"释放"了。因为 Redis 是所有进程共享的,这个 key 要么存在、要么不存在,全世界看到的是同一个事实——这就保证了所有进程抢的是同一把锁

关键在于"创建 key"这个动作必须是抢占式的:已经存在了就不能再创建成功。Redis 的 SETNX(SET if Not eXists)正好就是干这个的:

import redis

r = redis.Redis(host="localhost", port=6379)


def acquire_naive(key: str) -> bool:
    # SETNX:key 不存在才设置成功,返回 1;已存在则失败,返回 0。
    # 谁设置成功,谁就算"抢到了这把锁"。
    return r.setnx(key, "locked") == 1


def release_naive(key: str):
    r.delete(key)            # 用完把 key 删掉,锁就释放了
    # 问题:如果抢到锁的那个进程,在 release 之前【崩溃了】,
    # 这个 key 就【永远】不会被删 —— 锁被永久占住,
    # 之后所有进程再也抢不到锁,整个功能彻底死锁。

这个版本,已经能跨进程互斥了:三个实例同时调 acquire_naive,Redis 保证只有一个SETNX 成功。但它有一个致命缺陷,我在注释里写了:如果持锁的进程在 release 之前崩溃了——进程被 kill、机器断电、程序抛异常没走到 delete——那个 key 就再也没人删了。锁被一具"尸体"永久占着,后面所有进程都卡死在抢锁上。要补这个洞,锁必须能自动释放

三、锁必须能自动释放:给它一个过期时间

上一节的洞是:持锁进程一崩,锁就永久泄漏。修补的思路很自然:给锁一个"保质期"——就算没人主动释放它,到了时间它也自己消失

Redis 的 key 本来就支持过期时间(TTL):给一个 key 设上 10 秒过期,10 秒后 Redis 会自动把它删掉。把这个能力用到锁上,死锁问题就解了:就算持锁进程崩了、永远不来 release,这个 key 也会在 TTL 到期后被 Redis 自动清掉,锁自动释放,别的进程就能继续抢。

但这里有个容易踩的坑:"设置 key"和"设置过期时间",必须是同一条命令、一次完成。如果你先 SETNX 成功、再用另一条命令去设过期时间,那么在这两条命令之间,进程一旦崩溃,这个 key 就成了一个没有过期时间的永久 key——死锁问题原样复活。Redis 的 SET 命令带上 NXEX 参数,能把这两件事一次性原子地做掉:

def acquire_with_ttl(key: str, ttl: int = 10) -> bool:
    # SET key value NX EX ttl:一条命令,同时做两件事——
    #   NX:key 不存在才设置(保证互斥)
    #   EX ttl:给 key 设过期时间(单位秒),到点 Redis 自动删
    # 这两件事【原子】完成,中间不可能被进程崩溃劈开。
    return r.set(key, "locked", nx=True, ex=ttl) is not None
    # 这样,即使持锁进程崩溃、没来得及 release,
    # 最多过 ttl 秒,Redis 也会自动把这个 key 删掉,
    # 锁自动释放 —— 死锁问题被堵死。

到这里,锁已经能跨进程互斥,也能在持锁者崩溃后自动释放了。看起来很完整。但"加过期时间"这个补丁,在堵上一个洞的同时,又悄悄开了一个新洞:既然锁会"自己到期消失",那就有可能在持锁者还在干活的时候就消失了——这会引出一个比死锁更隐蔽的问题。

四、误删别人的锁:给每把锁写上唯一标识

上一节末尾那个新洞,具体是这样的。设想进程 A 抢到锁,TTL 设的是 10 秒。可 A 的业务执行得比较慢,跑了 12 秒。那么在第 10 秒,锁就自动过期了;进程 B 一直在抢,这一刻立即抢到了锁,开始干自己的活。到第 12 秒,进程 A 终于执行完了,它不知道自己的锁早过期了,照常调 release 去删 key——可它删掉的,是 B 刚刚抢到的那把锁。于是 B 还在临界区里干活,锁却被 A 删了,进程 C 又能抢到锁……互斥彻底崩坏

问题的根子在于:release无脑 delete 这个 key,而它根本没法确认"这个 key 此刻还是不是自己当初设的那一个"。要修它,就得让每个进程的锁带上一个只有自己知道的、独一无二的标识:加锁时把这个标识写进 key 的 value,释放时先比对 value——只有 value 还是自己那个标识,才说明"这把锁确实还是我的",才能删。

import uuid


def acquire(key: str, ttl: int = 10) -> str | None:
    # 锁的 value 不再是固定的 "locked",而是一个全局唯一的 token。
    token = uuid.uuid4().hex
    ok = r.set(key, token, nx=True, ex=ttl)
    # 抢到锁,就把这个【只有自己知道】的 token 返回出去;
    # 没抢到,返回 None。
    return token if ok else None
    # 这个 token,就是"这把锁是我加的"的唯一凭证。
    # 释放锁时,必须凭它来证明身份,不能无脑删。

现在每把锁都有了"身份证"。acquire 成功后返回的那个 token,进程必须自己拿好,release 时凭它说话。逻辑上,释放就该是:"get 出 key 当前的 value,如果它等于我手里的 token,再 delete"听上去严丝合缝——但如果你真的把"get 校验"和"delete"写成两步,你会发现自己又掉进了一个一模一样的竞态陷阱

五、原子地释放锁:用 Lua 脚本

上一节结尾说的那个陷阱,是这样的。先看一个看起来对、其实有 bug 的释放写法:

def release_unsafe(key: str, token: str):
    # 反面教材:先 get 校验,再 delete —— 这两步【不是原子的】。
    if r.get(key) == token.encode():
        # 致命窗口:就在【校验通过】和【执行下面这行 delete】之间,
        # 锁可能恰好 TTL 到期、被 Redis 自动删,
        # 然后别的进程立刻抢到了一把【新的】锁。
        # 此刻这行 delete 删掉的,就是【别人刚抢到的锁】。
        r.delete(key)

看出来了吗?它和第四节那个"误删"的本质完全一样:第四节是锁过期导致误删,这里是校验和删除之间的时间缝隙导致误删。get 返回的结果只代表"那一瞬间"的事实,等你的代码走到下一行 delete 时,这个事实可能早就变了。只要"判断"和"动作"是分开的两步,它们之间就有缝,有缝就有竞态。

要根治,只有一个办法:让"校验 token"和"删除 key"这两件事,合并成一个不可分割的原子操作。Redis 提供的手段是 Lua 脚本——Redis 保证一整段 Lua 脚本在执行期间,绝不会被其他任何命令打断。把"校验 + 删除"写进一段 Lua,它就要么整体成功、要么整体不做,中间那条缝被彻底焊死:

# Lua 脚本:在 Redis 内部【一次性、原子地】完成"校验 + 删除"。
_RELEASE_LUA = """
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
"""


def release(key: str, token: str) -> bool:
    # 整段脚本执行期间,Redis 不会插进任何别的命令。
    # "key 的 value 还等于我的 token 吗"和"删掉 key",
    # 要么一起发生,要么都不发生 —— 没有中间缝隙。
    result = r.eval(_RELEASE_LUA, 1, key, token)
    return result == 1     # 返回 1 才是真的把【自己的】锁删了

有了原子的 acquirerelease,就能把它们封装成一个好用、不易出错的锁对象。用 Python 的上下文管理器(with 语法),能保证无论业务正常结束还是抛异常,锁都一定会被释放:

class DistributedLock:
    """一个可以用 with 语句使用的 Redis 分布式锁。"""

    def __init__(self, key: str, ttl: int = 10):
        self.key = key
        self.ttl = ttl
        self.token = None

    def __enter__(self):
        self.token = acquire(self.key, self.ttl)
        if self.token is None:
            raise RuntimeError(f"获取锁失败: {self.key}")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # with 块无论是正常结束、还是中途抛异常,
        # __exit__ 都一定会被调用 —— 锁一定会被原子释放。
        if self.token is not None:
            release(self.key, self.token)

到这里,一把"能跨进程互斥、能崩溃自动释放、不会误删别人"的分布式锁,主干就算搭好了。但要把它真正用在生产上,还有几个绕不开的工程坑。

六、工程坑:看门狗续期、获取锁超时与可重入

分布式锁的主干通了,但有几个工程坑,不处理就会在生产上出事。

坑 1:业务还没干完,锁先过期了——要用"看门狗"续期。这是第四节那个问题的正面解法。TTL 设短了,业务没跑完锁就过期;设长了,持锁进程一旦真崩了,别人要白等很久。两难。正解是:TTL 还是设一个不太长的值(比如 10 秒),同时另起一个后台线程当"看门狗"——只要业务还在跑,它就每隔一段时间(比如 TTL 的三分之一)给锁续一次命,把过期时间重新拉回 10 秒。业务一结束,就停掉看门狗。这样,锁不会在业务进行中过期;而一旦进程真崩了,看门狗也跟着没了,锁会在最后一次续期后的 TTL 内正常过期释放。续期同样要校验 token、同样要原子:

import threading

# 续期脚本:仍然是自己的锁,才把过期时间重新设回 ttl 秒。
_RENEW_LUA = """
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('expire', KEYS[1], ARGV[2])
else
    return 0
end
"""


def start_watchdog(key: str, token: str, ttl: int) -> threading.Event:
    """看门狗:后台线程,持锁期间每隔 ttl/3 秒给锁续一次命。"""
    stop = threading.Event()

    def loop():
        # stop.wait 返回 True 说明收到停止信号,退出;
        # 返回 False 说明是等够了 ttl/3 秒,该续期了。
        while not stop.wait(ttl / 3):
            r.eval(_RENEW_LUA, 1, key, token, ttl)

    threading.Thread(target=loop, daemon=True).start()
    return stop          # 业务做完,调 stop.set() 让看门狗退出

坑 2:抢不到锁怎么办——不能死等,也不能立刻就放弃。前面的 acquire 是"抢一次,抢不到立刻返回 None"。但很多业务场景里,抢不到锁不该马上失败——也许只是另一个请求刚好在用,稍等一下(几十毫秒)它就放了。所以实际要的是一个带超时的获取:在一个有限的时间窗口内不断重试,真到了超时还抢不到,才放弃。有限很关键——绝不能写成 while True 的无限死等,那会把请求永久挂住

import time


def acquire_blocking(key: str, ttl: int = 10,
                     wait: float = 3.0) -> str | None:
    """带超时的获取锁:在 wait 秒内不断重试,超时就放弃。"""
    deadline = time.time() + wait
    while time.time() < deadline:
        token = acquire(key, ttl)
        if token is not None:
            return token         # 抢到了,直接返回 token
        time.sleep(0.05)         # 没抢到,歇 50ms 再抢,别空转打爆 CPU
    return None                  # 等够 wait 秒还没抢到,放弃

坑 3:同一个请求里重复加同一把锁——可重入问题。如果你的方法 A 加了锁,方法 A 又调用了方法 B,而 B 也去加同一把锁,就会自己把自己锁死:B 永远抢不到那把已经被 A(也就是它自己)占着的锁。解决要靠可重入设计——锁的 value 里除了 token,再记一个重入计数,同一个持有者再次加锁时计数 +1,释放时计数 -1,减到 0 才真正删 key。如果用不到可重入,那就守住一条纪律:一次业务流程里,同一把锁只在最外层加一次,别在嵌套调用里重复加。

把上面这些拼起来,就是一个生产可用的库存扣减。对比第一节那个会超卖的 deduct_naive,它的临界区,现在真正做到了"全局只有一个进程能进":

def deduct_stock(product_id: int) -> bool:
    """改造后的库存扣减:用分布式锁把跨进程的并发挡在外面。"""
    key = f"lock:stock:{product_id}"
    token = acquire_blocking(key, ttl=10, wait=3.0)
    if token is None:
        raise RuntimeError("系统繁忙,请稍后重试")   # 超时没抢到
    stop = start_watchdog(key, token, ttl=10)        # 启动看门狗续期
    try:
        # 这一段临界区,全公司所有实例加起来,同一时刻只有一个进程能进
        stock = get_stock(product_id)
        if stock > 0:
            set_stock(product_id, stock - 1)
            return True
        return False
    finally:
        stop.set()                  # 先停掉看门狗,不再续期
        release(key, token)          # 再用 Lua 原子地释放锁

下面这张图,把一次带分布式锁的操作,从抢锁到释放的完整路径串起来:

关键概念速查

概念 / 手段 说明
单机锁的局限 threading.Lock 只在一个进程内有效,多实例下每个进程一把,互不可见
分布式锁的本质 把锁挪到所有进程共享的外部存储,约定一个 key 代表锁
SETNX 抢锁 key 不存在才设置成功,谁设成功谁拿到锁,保证跨进程互斥
崩溃不释放 持锁进程崩在 release 之前,key 永不被删,锁永久泄漏成死锁
过期时间 TTL SET NX EX 一条命令原子地加锁并设过期,进程崩了锁也自动释放
误删别人的锁 锁过期后被别人抢走,原持有者无脑 delete 会删掉别人的锁
唯一 token 锁的 value 写一个唯一标识,释放前先比对,证明锁还是自己的
Lua 原子释放 校验 token 和删除 key 必须在一段 Lua 里原子完成,否则仍有竞态
看门狗续期 后台线程持锁期间定时续期,业务不会因锁过期而失去互斥
获取锁超时 抢不到锁要在有限时间内重试,绝不能无限死等挂住请求

避坑清单

  1. threading.Lock 这类单机锁只在一个进程内有效,多实例部署下每个进程各有一把,拦不住跨进程并发。
  2. 分布式锁的本质是把锁挪到所有进程共享的外部存储,让所有竞争者抢的是同一把锁。
  3. 用 Redis 的 SETNX 抢锁,谁能创建出代表锁的 key 谁就拿到锁,Redis 保证只有一个能成功。
  4. 持锁进程在 release 之前崩溃会导致 key 永不被删,锁永久泄漏,必须给锁加过期时间。
  5. 加锁和设过期时间必须用 SET NX EX 一条命令原子完成,分两步会在缝隙处崩溃留下永久 key。
  6. 锁会因 TTL 到期被别人抢走,原持有者无脑 delete 会误删别人的锁,要给每把锁写唯一 token。
  7. 释放锁时先 get 校验再 delete 不是原子的,缝隙里锁可能过期,必须用 Lua 脚本原子完成。
  8. 业务执行可能超过 TTL,要用看门狗后台线程定时续期,业务结束再停掉看门狗。
  9. 获取锁要带超时重试,在有限时间窗口内重试,绝不能写成无限死等把请求永久挂住。
  10. 同一请求嵌套加同一把锁会自己锁死自己,要么做可重入设计,要么纪律上只在最外层加一次。

总结

回头看那次"加了锁还超卖"的事故,以及我后来在分布式锁上接连踩的坑,最该记住的不是某一段 Lua 脚本,而是我动手前那个想当然的判断——"加了锁,就能防并发"。这句话错在它把""当成了一个抽象的、放之四海皆准的概念,而忽略了每一把锁都有它实实在在的作用边界threading.Lock 的边界,就是它所在的那一个进程;出了这个进程,它什么也锁不住。我在单机时代用熟了它,就下意识地以为它在多实例时代照样管用——可"多实例"恰恰意味着"多进程",意味着我的并发越过了那把锁的边界。分布式锁想清楚的,正是这件事:当你的竞争者散落在多个进程、多台机器上时,你需要的锁,也必须住在它们之外的、共同的地方

所以做分布式锁,真正的工程量不在"SETNX 一下"那个一目了然的主干上。那个主干,三行代码就能写完。真正的工程量,在于你有没有把那一连串"万一"都想到、并堵上:万一持锁的进程崩了呢?——所以要有 TTL。万一业务跑得比 TTL 久呢?——所以要有看门狗。万一锁过期后被别人抢走、原主却来删呢?——所以要有唯一 token。万一校验和删除之间那一瞬锁恰好过期呢?——所以要有 Lua。万一抢不到呢?——所以要有带超时的重试。这篇文章的几节,其实就是顺着这一串"万一"展开的:每一节,都是在修补上一节留下的那个新洞

你会发现,分布式锁的演进,和我们生活里"多人共用一样东西"的智慧惊人地相似。一间公司只有一个会议室,大家怎么不打架?靠的不是每个人心里默念"我要用",而是门口那块所有人都看得见的预约牌(这是把锁挪到进程之外)。预约牌上要写用到几点,免得有人占着不走、牌子永远翻不过来(这是 TTL)。会还没开完、眼看要到点了,得有人出来把时间往后顺一顺(这是看门狗)。你要摘牌走人,得先看一眼牌子上写的还是不是你的名字——别把下一拨人的预约给撕了(这是 token 校验)。分布式锁所有的复杂,归根到底,都是在一个没有中心、人人平等的环境里,把"排队"这件事老老实实地、不留缝隙地做对。

最后想说,分布式锁做没做扎实,差距永远不会在开发期暴露——开发时你单机跑一个实例,有锁没锁、锁对锁错,功能跑起来一模一样。它只在真实的、多实例的、被高并发流量冲刷的生产环境里才显形。那时候它会用最难堪的方式给你结账:做不好,你要么像我一样在对账时发现超卖了一批货、得自己去赔,要么某天一个进程崩在了持锁的瞬间,整个功能集体卡死,所有用户都在转圈,而你查了半天才发现是 Redis 里躺着一个永不过期的死锁 key。而做了,它会安安静静地、不被任何人注意地,把成百上千个并发请求一个一个地排好队,让那段绝不容许两个人同时进的临界区,永远只有一个人在里面。所以别等超卖的账单和卡死的告警一起找上门,在你把服务从一个实例扩到第二个实例的那一刻就该想清楚:我的并发,现在越过进程边界了吗?我的锁,跟得上吗?它会因为进程崩溃泄漏吗?它会误删别人吗?这几个问题都有了答案,你的锁才不只是开发库里那个单实例下有它没它都一样的摆设,而是一个无论服务扩到多少个实例,都能稳稳地、不留缝隙地守住互斥的可靠系统。

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

大模型语义缓存完全指南:从一次"同一个问题换种说法、模型又花钱重答一遍"看懂语义缓存

2026-5-21 20:42:16

技术教程

RAG 检索增强生成完全指南:从一次"问公司年假、大模型张口胡编"看懂 RAG

2026-5-21 20:52:20

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