分布式锁与幂等

分布式锁与幂等

服务部署到两台机器后,定时任务开始跑两次,优惠券被领了两回。单机锁在 JVM 里面,跨机器就失效了,于是分布式锁登场。但 setnx 一行命令的锁,坑比想象的多:锁自己会过期、会误删、会易主。把这三个坑和幂等的落地写法放在一起看,才是一套完整的防重复方案。

为什么单机锁不够

synchronized 和 ReentrantLock 锁的是一个 JVM 里的对象。两个进程各有各的锁,都以为自己是唯一持有者。分布式锁的本质是把「抢锁」变成一个所有参与者都能看到的原子操作,Redis 的 SET key value NX EX seconds 是最常用的实现:NX 保证只有一个客户端成功,EX 保证锁不会永久残留。

三个坑

第一个坑,业务比锁活得久。锁设了 30 秒过期,业务跑了 40 秒,第 31 秒锁没了,第二个进程抢到锁开始干活,两个进程同时在跑。解法是续期,Redisson 的看门狗就是干这个的:持有期间后台线程不断延长过期时间,业务执行完才释放。

第二个坑,误删别人的锁。A 的锁过期后被 B 抢到,A 干完活去删锁,把 B 的锁删了,B 的临界区门户大开。解法是锁认主人:value 存一个唯一标识(请求 id),删除前校验是不是自己的锁,校验和删除要原子,一段 Lua 脚本搞定。

第三个坑,主从切换丢锁。Redis 主节点加锁成功还没同步到从节点,主节点挂了、从节点升为主,锁凭空消失。这个坑的解法有争议,Redlock 方案主张多数派加锁,Kleppmann 的批评指出它依赖时钟假设。业界的实际态度多是认怂:分布式锁只当「效率优化」用,真正的正确性交给数据库的原子约束兜底。原则就一条------锁要认主人,还要活得比业务久。
#mermaid-svg-T64xwKMiB9OXdfcd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-T64xwKMiB9OXdfcd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-T64xwKMiB9OXdfcd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-T64xwKMiB9OXdfcd .error-icon{fill:#552222;}#mermaid-svg-T64xwKMiB9OXdfcd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-T64xwKMiB9OXdfcd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-T64xwKMiB9OXdfcd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-T64xwKMiB9OXdfcd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-T64xwKMiB9OXdfcd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-T64xwKMiB9OXdfcd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-T64xwKMiB9OXdfcd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-T64xwKMiB9OXdfcd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-T64xwKMiB9OXdfcd .marker.cross{stroke:#333333;}#mermaid-svg-T64xwKMiB9OXdfcd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-T64xwKMiB9OXdfcd p{margin:0;}#mermaid-svg-T64xwKMiB9OXdfcd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-T64xwKMiB9OXdfcd .cluster-label text{fill:#333;}#mermaid-svg-T64xwKMiB9OXdfcd .cluster-label span{color:#333;}#mermaid-svg-T64xwKMiB9OXdfcd .cluster-label span p{background-color:transparent;}#mermaid-svg-T64xwKMiB9OXdfcd .label text,#mermaid-svg-T64xwKMiB9OXdfcd span{fill:#333;color:#333;}#mermaid-svg-T64xwKMiB9OXdfcd .node rect,#mermaid-svg-T64xwKMiB9OXdfcd .node circle,#mermaid-svg-T64xwKMiB9OXdfcd .node ellipse,#mermaid-svg-T64xwKMiB9OXdfcd .node polygon,#mermaid-svg-T64xwKMiB9OXdfcd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-T64xwKMiB9OXdfcd .rough-node .label text,#mermaid-svg-T64xwKMiB9OXdfcd .node .label text,#mermaid-svg-T64xwKMiB9OXdfcd .image-shape .label,#mermaid-svg-T64xwKMiB9OXdfcd .icon-shape .label{text-anchor:middle;}#mermaid-svg-T64xwKMiB9OXdfcd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-T64xwKMiB9OXdfcd .rough-node .label,#mermaid-svg-T64xwKMiB9OXdfcd .node .label,#mermaid-svg-T64xwKMiB9OXdfcd .image-shape .label,#mermaid-svg-T64xwKMiB9OXdfcd .icon-shape .label{text-align:center;}#mermaid-svg-T64xwKMiB9OXdfcd .node.clickable{cursor:pointer;}#mermaid-svg-T64xwKMiB9OXdfcd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-T64xwKMiB9OXdfcd .arrowheadPath{fill:#333333;}#mermaid-svg-T64xwKMiB9OXdfcd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-T64xwKMiB9OXdfcd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-T64xwKMiB9OXdfcd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T64xwKMiB9OXdfcd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-T64xwKMiB9OXdfcd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T64xwKMiB9OXdfcd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-T64xwKMiB9OXdfcd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-T64xwKMiB9OXdfcd .cluster text{fill:#333;}#mermaid-svg-T64xwKMiB9OXdfcd .cluster span{color:#333;}#mermaid-svg-T64xwKMiB9OXdfcd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-T64xwKMiB9OXdfcd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-T64xwKMiB9OXdfcd rect.text{fill:none;stroke-width:0;}#mermaid-svg-T64xwKMiB9OXdfcd .icon-shape,#mermaid-svg-T64xwKMiB9OXdfcd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T64xwKMiB9OXdfcd .icon-shape p,#mermaid-svg-T64xwKMiB9OXdfcd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-T64xwKMiB9OXdfcd .icon-shape .label rect,#mermaid-svg-T64xwKMiB9OXdfcd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T64xwKMiB9OXdfcd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-T64xwKMiB9OXdfcd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-T64xwKMiB9OXdfcd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不校验
校验(Lua 原子)
进程 A setnx 抢到锁
业务跑到 31 秒,锁过期
进程 B 抢到锁,开始干活
A 干完活去删锁
校验 owner 吗
删掉 B 的锁,事故
发现不是自己的,拒绝

幂等才是兜底

锁是防「同时」,幂等是防「先后重复」。消息重投、用户双击、网络重试,都不需要两把锁同时在,只要同一个动作进来两次结果一样。三个落地写法。

数据库唯一约束,同一个业务单据号只许插一条,第二次插入自然失败,最硬的兜底。状态机流转,已支付的订单再收到支付请求,状态不允许回到「待支付」再支付一遍。乐观锁版本号,update 时带上 where version = 旧版本,重复更新撞版本返回 0 行。

和消息队列篇的三板斧是一套东西,那边讲消息,这边讲请求,道理同源。跑完自问一句,同一个请求进来两次,我的结果一样吗?

亲手看锁易主

下面用一段 Python 用内存模拟 Redis,复现「业务比锁活得久」导致的锁易主和误删拒绝。直接运行 python dist_lock_demo.py。

python 复制代码
# 运行:python dist_lock_demo.py
# 分布式锁的两个坑:锁过期易主、误删被 owner 校验拦下。
import threading
import time


class FakeRedis:
    """内存版的 SET NX EX 与带 owner 校验的删除。"""

    def __init__(self):
        self.store = {}
        self.expire_at = {}
        self._lock = threading.Lock()

    def set_nx(self, key, owner, ttl):
        with self._lock:
            now = time.time()
            if key in self.store and self.expire_at.get(key, 0) > now:
                return False
            self.store[key] = owner
            self.expire_at[key] = now + ttl
            return True

    def del_if_owner(self, key, owner):
        """Lua 校验删除的模拟:不是自己的锁,拒绝删除。"""
        with self._lock:
            if self.store.get(key) == owner and self.expire_at.get(key, 0) > time.time():
                del self.store[key]
                return True
            return False


def work(name, redis, slow=False):
    ok = redis.set_nx("job", name, ttl=0.3)
    print(f"{name}: {'抢到锁' if ok else '没抢到'}")
    if not ok:
        return
    if slow:
        time.sleep(0.6)   # 业务比锁活得久,锁在 0.3 秒时就没了
    released = redis.del_if_owner("job", name)
    print(f"{name}: 释放锁 {'成功' if released else '被拒绝(锁已易主,正确拦下误删)'}")


def main():
    r = FakeRedis()
    print("现场:A 抢到锁但业务要 0.6 秒,锁只活 0.3 秒")
    t1 = threading.Thread(target=work, args=("A", r, True))
    t2 = threading.Thread(target=work, args=("B", r, False))
    t1.start()
    time.sleep(0.45)      # A 的锁已过期,B 这时来抢
    t2.start()
    t1.join()
    t2.join()
    print("结论:看门狗负责活得久,owner 校验负责不误删,幂等负责兜底。")


if __name__ == "__main__":
    main()

适用边界

这一篇管 Redis 锁的坑与幂等落地。ZooKeeper 和 etcd 的锁实现、事务消息、以及分布式事务(Seata 那套),是另外的题。演示的 FakeRedis 只模拟了过期与 owner 校验,没模拟主从复制。跑完演示对照你线上的锁代码,三个坑逐个数,缺续期的加看门狗,缺校验的上 Lua,都没有把握的,把幂等做扎实,锁只是锦上添花。

相关推荐
wifi___5 小时前
Redis分布式锁
数据库·redis·分布式
方方洛8 小时前
ray教程-15-RayServe与LLM服务
人工智能·分布式·机器学习
m0_639906949 小时前
MySQL 实现分布式锁:悲观锁、乐观锁、唯一索引锁
数据库·分布式·mysql
T量子永生10 小时前
Android SISO 分布式编译集群配置方案
android·分布式·云计算·源代码管理
KaiwuDB1 天前
KaiwuDB 分布式集群扩缩容,从原理到完整操作踩坑总结
运维·数据库·分布式
泡海椒1 天前
深度解析JQuick-SQL AST到逻辑计划转换机制|分布式SQL原理
数据库·分布式·sql
泡海椒1 天前
深度解析JQuick-SQL SQL解析与AST生成机制|分布式SQL原理
数据库·分布式·sql
2603_954708311 天前
微能网协调控制箱的架构优势对能源管理的影响?
大数据·人工智能·分布式·安全·架构
方方洛1 天前
ray教程-14-RayTune与超参搜索
人工智能·分布式·机器学习