1997 字
约 6 分钟
4
幂等的两块地基:怎么"记好账",怎么"锁住门"

幂等的两块地基:怎么"记好账",怎么"锁住门"

通用工程思想 · 编程博客 · 第 6 篇 主题:凡是"做一次就不可逆"的操作(发消息、下单、扣款、派任务),都怕同一件事被做两遍。怎么从根本上防住?靠两样东西:一块记"做过什么"的账本,和一把保证"一次只有一个人在干"的锁。这篇讲它们分别怎么设计。


开场一个比喻

想象一间仓库,管理员最怕一件事:同一件活被派出去两遍——搬货的人跑了两趟,白费力气甚至出错。

他防这个靠两样东西:

  • 一块本子:每派一次,记一笔"这个活了"——下次一看本子,就知道不该再派;
  • 一把锁:验收/搬运时,一次只让一个工头进来,别的在门口排队——同时只有一个人在动。

"一次只做一遍"这件事,用工程话说叫幂等(idempotency)。搞懂它,是写这类系统的基本功。今天拆成两块讲:怎么记账、怎么上锁。


先讲:为什么"防重复"这么难?

防重复看着简单,真正难在两种情况:

  1. 做了,但结果没确认到:你可能已经把消息发出去了/下单成功了,只是那一刻程序没"看到"确认,于是怀疑"到底做了没"。该不该重做一次?
  2. 两件事同时在做:定时任务和手动操作同时触发,同一批对象被两处各发一次。

真正稳的防重复,不是靠"小心翼翼别出错",而是设计成"哪怕出错,也不会重复"。下面两块地基地就是这个意思。


地基一:记账本(怎么"记好做过什么")

核心思路:做之前,先记一笔;做完,改一笔。

具体是"两段式":

 准备做"这件事"时:
   1. 先查账:这件事做过/没做过?
       做过 → 直接跳过,别再做
   2. 没做过 → 先记一笔"占用中/未知"(占位)
   3. 真正去做
   4. 做完确认成功 → 把这一笔改成"已成功"

关键点睛:为什么"做之前就先占位"?

因为你拦不住"做到一半崩溃"。如果不占位就去做,刚做完、正要记成功时崩溃了——那这一笔没记上,下次会以为"没做过",又做一遍 → 重复!

先占位的话,哪怕中途崩溃、结果不明,账上"这一笔"已经在——下次一看,当"已经处理过",跳过 → 不会重做

一句话:"先占位"让崩溃也翻不了天——要么完成,要么留个'未知'占位,总之不会变成'没做过'。 这就是"宁可当已发生,也不冒重做的险"。


记账还有个"硬伤要治":账本别写烂

账本是反复改写的文件。最怕:写一半崩溃,把账本写成"半截烂文件"——那就连"查账"都查不了。

治法的核心不是"小心写",而是"原子写":

 写账本时:
   1. 先写一个"临时文件"
   2. 再把临时文件"整体替换"掉原账本
  └─ 好处:要坏也最多坏最近一次替换,绝不会留下写一半的烂账本

再加一道保险(fail-closed / 宁停勿错):万一账本还是坏了,就直接停,别在"账不可信"的情况下继续做高危操作——因为账一旦不实,防重复就失灵了,继续下去比停更危险。


记账的"钥匙"长什么样

让每条"活"能被唯一识别,得给它一把唯一钥匙,一般由几段拼出来,能让你唯一认出"是这回事":

常见拼法:什么任务 : 哪天 : 谁/对谁 : 做的具体那件事的代号

有了唯一钥匙,"查有没有做过"就是"账本里有没有这把钥匙"。钥匙设计得够唯一,判重才准——这是记账的关键细节(拼少了会误判"重"或"漏")。


地基二:单实例锁(怎么保证"一次只有一个人在干")

记账挡住"重复做一遍";锁挡住"同时两个人做"。原理极简:

 开工前:
   试着在磁盘上"新建"一把锁文件
     新建成功 → 没人抢,我拿到权,继续
     新建失败(文件已在) → 已有一个在跑,我退出
 干完 → 删掉锁文件,把权还回去

它的优点:是最轻、零依赖、几乎每个语言都支持的自带方式——不必引入什么重型中间件,一个"独占创建文件"的原子操作就能实现互斥。

一个绕不开的小坑:陈旧锁。 如果进程被强杀,锁文件会残留在磁盘上——之后系统一直误以为"有人在跑",不让新进程干。

破法:报错信息里明确提示"如果确认没有进程在跑,请手动删除这个锁文件",把自救方法告诉用户。更高阶的方案(如"锁里记 PID + 超时、自动检测僵死进程")更智能,但复杂度更高,要按需取舍。


两块地基怎么配合

把两块合起来,防重就立体了:

 第 1 道:记账(往前的防)
   做之前查"做过没",做过就跳过 —— 防止"这次"把"上次"再做一遍
 第 2 道:锁(往后的防)
   开工前抢锁,抢不到就退出 —— 防止"两个同时"各做一遍

有些平台还自带更上层的并发控制(比如 CI 的"并发组"),可以当第三道再兜底一层。守得越多,越稳。


用的时候注意(边界 / 取舍)

  • 先占位 ≠ 真的做:占位就先留"未知",是防御性设计——代价是"结果未知"会暂时占着名额、以后得人工补看。要能接受这个折中。
  • 原子写 ≠ 彻底不坏:它只是让"写一半崩溃"不产生烂文件,防止的是"写入中断"这一坏;不是解决所有存储问题。别指望它包治百病。
  • 陈旧锁方案有代价:提示"手动删"最简单但靠人;自动检测僵死更聪明但要对付"误判在跑的进程"的风险。按"你的崩溃频率、影响大小"取平衡,别一上来就堆复杂方案。
  • 钥匙要够唯一:是你写这套时最容易大意的地方——拼少了,真重复会漏判。

落地检查清单

  • 高危操作是否用"先占位→做事→确认成功"的两段式?
  • 占位是否保证"哪怕中途崩溃,下次也会当已处理跳过"?
  • 每件事的"唯一钥匙"是否足够唯一、不会漏判或误判?
  • 常改写的账本/记录文件,是否用"临时文件+原子替换"来写?
  • 账本损坏时,是否选择"fail-closed(停),而非在账不可信下继续"?
  • 需要互斥的场景,是否有"单实例锁",防止两个进程同时跑?
  • 陈旧锁是否至少给出了"确认无进程后手动删除"的自救提示?

一句话带走

"防重复这件事,不能靠'小心',要靠设计:一块先占位、原子写的账本防'再做一遍',一把独占创建的锁防'同时两个人在干'——外加账不可信就宁停勿错。把账记明白、把门锁好,重复根本进不来。"

幂等的两块地基:怎么"记好账",怎么"锁住门"
http://clxhxhhr.top/posts/544/
作者
clxstart
发布于
2026-09-08
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。