分布式锁完全指南:从一次"扩容后对账任务跑了三遍、结算被重复打款"看懂分布式锁

2023 年我维护一个对账定时任务,每天凌晨核对订单和支付流水、生成结算单、发结算邮件。单实例跑了大半年稳稳当当,直到运维把服务从 1 个实例扩容到 3 个,第二天就出事:同一用户收到三封结算邮件,有几笔结算被重复打款。翻代码一行 bug 都没有,盯着部署结构才反应过来:定时任务代码被原封不动部署到 3 个实例上,凌晨那一刻 3 个实例各自都跑了一遍。我以为代码里加了 Lock 就并发安全,真相是那个锁是进程内的锁,只能保证同一进程里多个线程不打架,而现在是 3 个独立进程跑在 3 台机器上,各有各的内存各有各的 Lock,锁跨不出进程边界。要让无论部署多少实例同一时刻只有一个能执行,需要一把所有实例都看得见抢得到的锁,放在它们共同的外部,这就是分布式锁。本文从头梳理。为什么单机锁失效:进程内锁只在单进程有效,多实例部署后多进程各持各的锁对象等于没锁。数据库做锁:靠唯一约束插行抢锁,不引入新组件但性能差、过期处理别扭。Redis 锁:必须 SET NX EX 一条命令原子完成占锁加设过期,拆两步中间崩溃会留下永不过期的死锁,必须设 TTL 作为持有者宕机的兜底。锁误删:锁过期被别人拿走后直接 del 会删别人的锁,value 要存唯一 token,释放用 Lua 脚本原子比对 token 才删。锁超时:业务跑太久锁会提前过期,TTL 设多少都是赌,正解是看门狗后台线程业务没结束就持续续期。工程坑:获取失败不能无限重试要分场景,可重入防自锁死,锁粒度要细只锁真正冲突的那一小块,分布式锁不是百分百可靠涉及钱的业务必须再用幂等兜底。核心一句:决定一段逻辑被几个实例同时执行的从来不是你的锁,而是你的部署架构。

2023 年我维护一个对账定时任务:每天凌晨跑一次,把前一天的订单和支付流水核对一遍,有差异的生成一份结算单,再给相关用户发一封结算邮件。这个任务在单实例的服务上跑了大半年,稳稳当当。直到有一次,为了扛住增长,运维把这个服务从 1 个实例扩容到了 3 个。第二天早上,投诉就来了:同一个用户,收到了三封一模一样的结算邮件;财务那边对账,发现有几笔结算被重复打款了。我第一反应是任务逻辑写错了,翻了半天代码,一行 bug 都没有。后来盯着部署结构才反应过来:那个定时任务的代码,被原封不动地部署到了 3 个实例上,于是凌晨那一刻,3 个实例各自都把这个任务跑了一遍——它们谁也不知道彼此的存在。我当时的认知是:"我在代码里加了锁(Lock),并发是安全的。"而真相是——我加的那个锁,是进程内的锁,它只能保证同一个进程里的多个线程不打架。可现在是 3 个独立的进程、跑在 3 台不同的机器上,它们各自有各自的内存、各自有各自的 Lock 对象,这个锁跨不出进程的边界,自然也就拦不住"3 个实例同时跑同一个任务"。要让"无论部署多少个实例,这段逻辑同一时刻只有一个能执行",你需要一把所有实例都看得见、抢得到的锁——一把放在它们共同的外部(比如 Redis、数据库)的锁。这就是分布式锁。我以为分布式锁不过是"用 Redis 的 SETNX 占个坑",结果真做下来坑一个接一个:锁会因为业务跑太久而提前过期;过期之后你以为还拿着锁,其实别人已经拿走了;你去释放锁,删掉的是别人的锁;就算这些都处理了,还有可重入、锁粒度、获取失败怎么办……那次之后我才认真把分布式锁从头搞明白。这篇文章就把它梳理一遍:为什么单机锁会失效、数据库怎么做锁、Redis 锁怎么写、锁误删和超时这两个致命坑怎么填、看门狗怎么续期,以及把分布式锁真正做进生产要避开的那些坑。

问题背景

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

现象:一个对账定时任务,在服务从单实例扩容到 3 个实例后,被 3 个实例各自完整执行了一遍,造成结算邮件重复发送、结算重复打款——而每个实例自己的日志里都看不出任何异常。

我当时的错误认知:"我在代码里用了锁,这段逻辑就是并发安全的;扩容只是多了几台机器,不影响我的锁。"

真相:语言自带的锁(Java 的 synchronized、Python 的 threading.Lock)是进程内的锁,它只在同一个进程内有效。一旦你的服务多实例部署,多个实例是多个独立进程,各自持有各自的锁对象,进程内的锁互相看不见,等于没锁。要在多实例之间互斥,必须把锁放到一个所有实例共同访问的外部存储里(Redis、数据库、ZooKeeper),让"抢锁"这个动作有一个全局唯一的裁判。而分布式锁真正的难点不在"抢到锁",在于:锁会过期、持有者会宕机、释放时可能误删别人的锁——这些分布式环境特有的故障,才是要小心对付的。

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

  • 为什么进程内的锁在多实例部署下会彻底失效;
  • 怎么用数据库当锁,它的能力边界在哪;
  • Redis 分布式锁的正确写法——为什么必须 SET NX EX 一步到位;
  • 锁误删和锁超时这两个致命问题怎么填;
  • 看门狗续期、可重入、锁粒度这些工程坑怎么处理。

一、为什么需要分布式锁:单机锁的边界

先看清单机锁到底"锁"的是什么。语言层面的锁,比如 Python 的 threading.Lock,它本质上是进程内存里的一个对象。多个线程能靠它互斥,是因为这些线程共享同一块进程内存,看得到同一个锁对象。

import threading

# 单机锁:一个进程内存里的 Lock 对象。
_local_lock = threading.Lock()


def reconcile_accounts():
    # 反面教材:用进程内的锁去保护一个"全局只能跑一次"的任务。
    if not _local_lock.acquire(blocking=False):
        return                       # 没抢到,说明本进程已有线程在跑
    try:
        do_reconcile()               # 对账、生成结算单、发邮件
    finally:
        _local_lock.release()
    # 问题:这把锁只在【本进程】有效。服务扩容到 3 个实例后,
    # 是 3 个独立进程、3 块独立内存、3 个互不相识的 _local_lock。
    # 3 个实例会【各自】都抢到自己那把锁,然后各跑一遍 —— 锁形同虚设。

这段代码在单实例时是完全正确的——它确实拦住了"同一进程里多个线程重复跑对账"。它的问题不在代码本身,在一个隐含的前提:它默认"全世界只有我这一个进程"。可一旦多实例部署,这个前提就崩了。3 个实例是 3 个操作系统进程,它们的内存空间彼此完全隔离,_local_lock 在每个进程里都是一个全新的、独立的对象。实例 A 抢到了 A 的锁,实例 B 抢到了 B 的锁,它们都觉得"我抢到了,可以跑了"——锁根本没起到互斥作用。

问题想清楚了,解法也就清楚了:既然进程内的锁互相看不见,那就把锁挪到一个所有实例都看得见的地方去。这个地方,必须是独立于所有业务实例之外的一个共享存储——数据库、Redis 都行。所有实例抢锁时,都去问同一个裁判:"这把锁现在没人占吧?我能占吗?"由这个全局唯一的裁判来保证"同一时刻只有一个实例占住锁"。下面几节,就是这个"裁判"的几种实现。

二、用数据库做锁:能用,但要看清边界

最容易想到的共享存储,是你本来就有的那个数据库。用数据库做锁,最直白的方式是利用唯一约束:建一张锁表,"抢锁"就是往里 INSERT 一行,"释放锁"就是把这行 DELETE 掉。谁能插入成功,谁就拿到了锁;插入时撞上唯一约束冲突的,就是没抢到。

-- 一张专门的锁表,lock_name 上加唯一约束。
CREATE TABLE distributed_locks (
    lock_name   VARCHAR(128) PRIMARY KEY,   -- 锁的名字,唯一
    holder      VARCHAR(64)  NOT NULL,      -- 谁持有这把锁
    expire_at   DATETIME     NOT NULL       -- 锁的过期时间
);

抢锁就是尝试插入一行;能插进去就是抢到,撞上主键冲突就是没抢到。注意一定要带上 expire_at 这个过期时间——否则一旦持锁的实例宕机、没来得及删除这行,这把锁就永远占着,谁也再拿不到了:

import pymysql
import datetime


def acquire_db_lock(conn, name: str, holder: str, ttl_sec: int = 30) -> bool:
    """靠唯一约束抢锁:能 INSERT 进去就是抢到,主键冲突就是没抢到。"""
    now = datetime.datetime.now()
    expire = now + datetime.timedelta(seconds=ttl_sec)
    try:
        with conn.cursor() as cur:
            # 先清掉【已过期】的锁行 —— 防止持锁者宕机后锁永远占着
            cur.execute(
                "DELETE FROM distributed_locks "
                "WHERE lock_name = %s AND expire_at < %s",
                (name, now),
            )
            cur.execute(
                "INSERT INTO distributed_locks (lock_name, holder, expire_at) "
                "VALUES (%s, %s, %s)",
                (name, holder, expire),
            )
        conn.commit()
        return True                       # 插入成功 = 抢到锁
    except pymysql.err.IntegrityError:
        conn.rollback()
        return False                      # 主键冲突 = 锁已被别人占着

数据库锁的好处是不引入新组件——你本来就有数据库,锁数据和业务数据还能在同一个事务里。但它的边界你必须看清。其一,性能:每次抢锁、释放锁都是一次数据库写操作,在高并发下,数据库会成为瓶颈。其二,过期处理很别扭:数据库不会自动删除过期的行,你只能像上面那样,在抢锁时顺手清理过期行,这不够及时。其三,没有阻塞等待:数据库锁天然是"抢不到就立刻返回失败",想实现"等一会儿再抢"得自己写轮询。所以数据库锁适合并发不高、本来就重度依赖数据库的场景。并发一上来,大家更常用的是 Redis。

三、Redis 分布式锁:SET NX EX 一步到位

Redis 做分布式锁,核心是一条命令:SET key value NX EX seconds。这条命令把三件事原子地合在了一起——NX 表示"只有 key 不存在时才设置",EX 给 key 设一个过期时间,设置成功才返回 OK。谁的 SET ... NX 成功了,谁就抢到了锁。

这里有个新手极易踩的坑:把"设置 key"和"设置过期时间"拆成两条命令写。下面就是这个反面教材:

import redis

r = redis.Redis()


def acquire_lock_wrong(name: str) -> bool:
    # 反面教材:把"占锁"和"设过期"拆成两步。
    if r.setnx(name, "locked"):          # 第 1 步:占锁
        r.expire(name, 30)               # 第 2 步:设过期时间
        return True
    return False
    # 致命缝隙:如果进程在第 1 步成功、第 2 步执行【之前】崩溃了,
    # 这个 key 就成了一个【永不过期】的锁 —— 死锁,谁也别想再拿到。

这个写法的问题是:第 1 步和第 2 步之间有一道缝。万一进程恰好在这道缝里崩溃了,这个 key 就被占住、却永远不会过期,变成一把谁也解不开的死锁。正确的写法是用一条命令把占锁和设过期原子地做掉:

import uuid
import redis

r = redis.Redis()


def acquire_lock(name: str, ttl_sec: int = 30) -> str | None:
    """抢分布式锁:SET NX EX 一条命令原子完成占锁 + 设过期。"""
    # token:本次持锁的唯一身份证 —— 释放锁时要靠它证明"这锁是我的"
    token = str(uuid.uuid4())
    # nx=True:只有 key 不存在才设置成功;ex:过期时间,一步设好
    ok = r.set(name, token, nx=True, ex=ttl_sec)
    return token if ok else None         # 抢到返回 token,没抢到返回 None

这里有两个关键点。其一,nxex 写在同一条 set 里,占锁和设过期是原子的,杜绝了上面那个反面教材的死锁。其二,必须设过期时间(ex):分布式锁的持有者随时可能宕机,如果宕机前没来得及释放锁,这个过期时间就是唯一的兜底——时间一到,Redis 自动删掉这个 key,锁自动释放,系统不至于永久死锁。还要注意我们存进去的 value 不是固定的 "locked",而是一个唯一的 token——这个 token 是下一节"安全释放锁"的命根子。

四、锁误删与锁超时:两个致命问题

抢到锁是容易的,难的是正确地释放锁。先看一个看起来天经地义的释放写法:

def release_lock_wrong(name: str):
    # 反面教材:直接删掉这个 key,就当释放了锁。
    r.delete(name)
    # 看起来没毛病,但它有个致命假设:删的时候,锁还是【我】持有的。
    # 这个假设在分布式环境里根本不成立。

这个 delete 为什么危险?设想这样一串时序:实例 A 抢到锁,锁的过期时间是 30 秒;可 A 的业务跑得很慢,跑了 35 秒。在第 30 秒,锁自动过期了,Redis 把 key 删掉;第 31 秒,实例 B 来抢锁,key 不存在,B抢到了;到了第 35 秒,A 的业务终于跑完,它调用 release_lock_wrong——把 B 的锁删掉了。现在 B 还以为自己持有锁,实际上锁已经没了,第三个实例 C 又能抢到……互斥彻底崩塌。

问题的根源是:你不能假设"我释放锁时,锁还是我的"。所以释放锁之前,必须先验明正身:这把锁的 value,还是不是我当初存进去的那个 token?这正是上一节要存唯一 token 的原因。但"先 get 比对、再 delete"又是一个 check-then-act 陷阱——比对完、删除前的那一瞬间,锁可能正好过期、被别人拿走。所以这个"比对 + 删除"必须是原子的,而 Redis 保证原子性的手段,是 Lua 脚本:

# Lua 脚本:在 Redis 服务端【原子地】执行"比对 token,一致才删除"。
# 整段脚本执行期间不会被其他命令打断 —— 这就堵死了 check-then-act 缝隙。
_RELEASE_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
"""


def release_lock(name: str, token: str) -> bool:
    """安全释放锁:只有 value 仍是我的 token 时,才删除这个 key。"""
    # 把脚本、key、token 一起发给 Redis,服务端原子执行
    result = r.eval(_RELEASE_SCRIPT, 1, name, token)
    return result == 1                   # 返回 1 说明确实是我删的

这段 Lua 脚本做的事是:"取出这个锁的 value,只有当它等于我的 token 时,才删除它"。因为 Redis 执行 Lua 脚本是原子的,整个"比对 + 删除"中间不会被打断,就彻底堵死了"比对完、删除前锁被别人抢走"的缝隙。如果 value 已经不是我的 token(说明锁早过期、被别人拿走了),脚本返回 0,我什么也不删——绝不会误删别人的锁。

但锁误删只是问题的一半。另一半是:为什么 A 的锁会过期?因为它的业务执行时间超过了锁的 TTL。这个问题用 Lua 脚本是解决不了的——你设 30 秒,业务跑 35 秒就是会过期;你设 5 分钟,万一业务真卡了 6 分钟还是会过期,而且 TTL 设太长,持有者一旦宕机,别人要白等 5 分钟。TTL 设多少都是赌。真正的解法,是让锁在业务还没跑完时自动续期——这就是看门狗。

五、看门狗:给锁自动续期

看门狗(watchdog)的思路是:抢到锁之后,另起一个后台线程,让它定期检查"业务是否还在跑";只要还在跑,就把锁的过期时间往后延一延。这样,锁的有效期会一直跟着业务走,业务不结束,锁就不会过期;而一旦持有锁的进程宕机,这个续期线程也跟着没了,锁便会在下一个 TTL 到点时自然过期——既不会中途误失,也不会宕机后死占。

续期这个动作本身也得是安全的:只能续自己的锁。所以续期同样要用 Lua 脚本,先比对 token:

import threading

# 续期脚本:token 一致才把过期时间重设为 ARGV[2] 秒。
_RENEW_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('expire', KEYS[1], ARGV[2])
else
    return 0
end
"""


def start_watchdog(name: str, token: str, ttl_sec: int,
                   stop_event: threading.Event):
    """看门狗线程:只要业务没结束,就周期性把锁的 TTL 续回去。"""
    interval = ttl_sec / 3               # 经验值:每过 1/3 TTL 续一次
    while not stop_event.wait(interval):
        # stop_event 没被置位 = 业务还在跑 —— 给锁续期
        ok = r.eval(_RENEW_SCRIPT, 1, name, token, ttl_sec)
        if ok != 1:
            # 续期失败:锁已经不是我的了(过期 + 被抢),看门狗退出
            break

把抢锁、看门狗、释放锁三件事串起来,做成一个上下文管理器,用起来就只是一个 with——这是分布式锁在生产里最常见的封装形态:

import contextlib


@contextlib.contextmanager
def distributed_lock(name: str, ttl_sec: int = 30):
    """分布式锁上下文管理器:自动抢锁、自动续期、自动安全释放。"""
    token = acquire_lock(name, ttl_sec)
    if token is None:
        raise RuntimeError(f"未能获取锁: {name}")
    stop_event = threading.Event()
    # 起看门狗线程,业务跑多久它就续多久
    dog = threading.Thread(
        target=start_watchdog, args=(name, token, ttl_sec, stop_event),
        daemon=True)
    dog.start()
    try:
        yield token                      # 把锁交给业务代码,中间随便跑多久
    finally:
        stop_event.set()                 # 通知看门狗:业务结束,别再续了
        release_lock(name, token)        # 用 Lua 脚本安全释放

有了它,文章开头那个对账任务就好办了:3 个实例每个都执行 with distributed_lock("job:reconcile"): do_reconcile(),但同一时刻只有一个实例能进到 with 里面,另外两个会在 acquire_lock 那里拿到 None、抛错退出。对账,就只跑一次了。

六、工程坑:Redlock 争议、可重入、锁粒度

机制都有了,但要把分布式锁真正做进生产,还有几个绕不开的工程坑。

坑 1:获取锁失败,不能无限重试,要分场景决定。抢不到锁怎么办?这取决于业务。像对账定时任务这种,"已经有实例在跑了"本身就是正确状态,抢不到就直接退出,什么也不做。而像"用户点了提交按钮"这种,抢不到可能要带退避地重试几次,或者直接告诉用户"系统繁忙稍后再试"。绝对不要写一个 while True 死循环去抢锁——一旦持锁者卡住,所有实例都会陷在这个循环里空转。

import time


def acquire_with_retry(name: str, ttl_sec: int = 30,
                       max_retry: int = 3) -> str | None:
    """带退避的有限重试:抢不到就等一会儿再试,但绝不无限等。"""
    for attempt in range(max_retry):
        token = acquire_lock(name, ttl_sec)
        if token is not None:
            return token
        # 退避:每次多等一点,把抢锁机会让给当前持有者把活干完
        time.sleep(0.1 * (2 ** attempt))
    return None                          # 重试若干次仍失败,交给调用方决定

坑 2:同一个线程重复获取同一把锁会自己锁死自己——需要可重入。设想方法 A 拿了锁,在锁里又调用了方法 B,而 B 也想拿同一把锁。如果锁不可重入,B 就会卡在那里等一把永远不会释放的锁(因为持有者正是在等 B 返回的 A)——这就是自己把自己锁死。解决办法是可重入锁:锁的 value 里记上"持有者身份 + 重入次数",同一个持有者再次获取就把计数加一,释放时减一,减到 0 才真正删除 key。Redisson 这类成熟的库默认就是可重入的。

坑 3:锁粒度要尽量细。千万别用一把"全局大锁"把所有业务都串起来。比如要扣减库存,该锁的是具体某个商品的库存,锁的名字应该是 lock:stock:{商品ID},而不是一把 lock:stock所有商品的扣减全锁住。锁粒度越细,能并行的业务就越多,锁的竞争就越小,系统吞吐才上得去。粒度的原则是:只锁真正会互相冲突的那一小块

坑 4:单 Redis 节点的锁,在主从切换时有丢失风险。上面所有代码都是基于单个 Redis 节点的。如果这个 Redis 是主从架构:实例 A 在主节点抢到锁,但锁还没同步到从节点,主节点就宕机了,从节点被提升为主——新主上没有这把锁,实例 B 于是又抢到了。针对这个问题,Redis 作者提出过 Redlock 算法(向多个独立 Redis 节点同时抢锁,过半成功才算拿到),但它也一直存在争议。一个务实的态度是:认清分布式锁不是 100% 可靠的,所以真正涉及钱、涉及数据正确性的业务,不能只靠锁——锁负责"大概率不重复执行",业务本身还要再用幂等设计兜底,做到"万一真重复执行了,结果也不会错"。下面这张图,把一次完整的加锁执行流程串起来:

关键概念速查

概念 / 手段 说明
进程内锁 synchronized / threading.Lock 只在单进程有效,多实例部署下形同虚设
分布式锁 把锁放到所有实例共同访问的外部存储,实现跨进程跨机器的互斥
数据库锁 靠唯一约束插行抢锁,不引入新组件,但性能差、过期处理别扭
SET NX EX Redis 一条命令原子完成占锁 + 设过期,杜绝拆两步的死锁
过期时间 分布式锁必须设 TTL,作为持有者宕机后的兜底自动释放
锁误删 锁过期被别人拿走后,直接 del 会删掉别人的锁,必须先比对 token
Lua 脚本 Redis 原子执行,用来做"比对 token 才删除 / 才续期"
看门狗 后台线程周期性续期,让锁的有效期跟着业务走,防中途过期
可重入 记录持有者身份 + 重入计数,同一持有者重复获取不会自锁死
锁粒度 只锁真正冲突的那一小块,粒度越细并发越高

避坑清单

  1. 语言自带的锁是进程内锁,只在单进程有效;服务多实例部署后,多个进程各持各的锁对象,等于没锁。
  2. 分布式锁要把锁放到所有实例共同访问的外部存储(Redis、数据库),由一个全局裁判保证互斥。
  3. 数据库锁靠唯一约束插行实现,不引入新组件,但每次抢锁都是数据库写,高并发下会成瓶颈。
  4. Redis 锁必须用 SET NX EX 一条命令,绝不能把占锁和设过期拆成两步——中间崩溃会留下永不过期的死锁。
  5. 分布式锁必须设过期时间,这是持有者宕机、没来得及释放锁时唯一的兜底。
  6. 锁的 value 要存一个唯一 token,作为持有者的身份证,释放和续期时都要靠它验明正身。
  7. 释放锁不能直接 del:锁可能已过期被别人拿走,直接删会误删别人的锁,要用 Lua 脚本比对 token 后再删。
  8. 锁会因业务执行超时而提前过期,TTL 设多少都是赌;正解是用看门狗后台线程,业务没结束就持续续期。
  9. 获取锁失败不能无限重试,要按业务分场景:定时任务直接退出,用户操作带退避有限重试。
  10. 分布式锁不是 100% 可靠的(单点主从切换会丢锁),涉及钱和数据正确性的业务必须再用幂等设计兜底。

总结

回头看那次"扩容之后对账跑了三遍"的事故,以及我后来在分布式锁上接连踩的坑,最该记住的不是某一段 Lua 脚本,而是我动手前那个想当然的判断——"我代码里加了锁,并发就是安全的"。这句话错在它没分清"锁"也是分层的:进程内的锁,保护的是同一进程里的多个线程;它从设计上就看不见、也管不着别的进程。你的服务只要从 1 个实例变成 2 个,这把锁的保护范围就立刻被现实甩在了身后。决定"一段逻辑会被几个实例同时执行"的,从来不是你的锁,而是你的部署架构

所以做分布式锁,真正的工程量不在"抢到锁"那一下。SET key value NX EX 谁都会写,它在 Demo 里、在单实例时也确实跑得好好的。真正的工程量在抢到锁之后:锁会过期,你怎么让它跟着业务走(看门狗);锁过期后可能被别人拿走,你释放时怎么不误删别人的锁(token + Lua);你抢不到锁时,该退出还是该重试(分场景);以及最根本的——你怎么接受"锁可能失效"这个事实,并用幂等给业务再上一道保险。这篇文章的几节,其实就是顺着这条思路展开的:先想清楚单机锁为什么失效,再看数据库锁和 Redis 锁两种实现,然后是锁误删、锁超时这两个致命坑,最后是看门狗、可重入、锁粒度这几个把分布式锁真正做稳的工程细节。

你会发现,分布式锁的思路和我们处理任何"共享资源"的工程经验都是相通的。单机时代,多个线程争一个变量,我们用进程内的锁;进入分布式时代,多个进程、多台机器争一份数据,我们就得把锁外置到一个公共的裁判那里。变的是锁的"位置",不变的是那个最朴素的诉求:让同一时刻只有一个执行者能动这份资源。理解了这一点,你也就明白为什么分布式锁绕不开过期、续期、误删这些麻烦——因为锁一旦离开了进程内存、放到了网络对面,它就要直面网络会断、节点会宕、时钟会偏这些分布式系统的固有现实。

最后想说,分布式锁做没做扎实,差距永远不会在 Demo 里暴露——Demo 里你就一个实例,加不加锁、锁写得对不对,跑起来一模一样。它只在服务真正扩容、真正多实例部署、真正遇上某个实例卡顿或宕机的那一刻才显形。那时候它会用最难堪的方式给你结账:一个定时任务被几个实例各跑一遍,一笔结算被重复打款,一件库存被超卖。所以别等扩容之后用户拿着重复的账单来找你,在你写下第一个 Lock() 的时候就该想清楚:这段逻辑将来会部署几个实例?这把锁跨得出进程的边界吗?锁会过期吗,过期了我续期了吗?释放的时候我删的确定是自己的锁吗?这几个问题都有了答案,你的服务才不只是单实例 Demo 里那个"看起来线程安全"的样子,而是一个无论扩容到多少个实例,关键逻辑都始终只有一个执行者的可靠系统。

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

LLM 上下文管理完全指南:从一次"聊到一半 AI 突然失忆又报错"看懂 token 与对话历史

2026-5-21 19:21:31

技术教程

Function Calling 完全指南:从一次"AI 假装查了数据库、其实把结果编了出来"看懂大模型工具调用

2026-5-21 19:31:02

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