幂等性检查

1 min read
Zekari
系统设计分布式系统软件工程可靠性工程

重复执行,结果不变

幂等性是一个数学词汇,但它的核心思想很简单:无论你做多少次,结果都是一样的。

在计算机系统中,这意味着一个操作可以安全地重复执行,不会产生意外的副作用。

转账 100 元,执行一次是转 100,执行十次还是转 100。删除一个文件,删除一次是删除,删除十次还是那个状态。

这听起来理所当然,但现实的系统充满了不确定性。网络会超时,服务会重启,请求会重发。如果没有幂等性保障,重复就会带来灾难。

💡 Click the maximize icon to view in fullscreen

不确定性是常态

分布式系统的本质是不确定性。

消息可能丢失,可能延迟,也可能重复。你发出一个请求,等了 5 秒没有响应。你不知道是服务器没收到,还是收到了但正在处理,还是已经处理完了但响应丢失了。

在这种不确定性面前,重试是最常见的策略。超时了就重试,失败了就重试。但重试带来一个问题:如果服务器其实已经处理过了,重试会不会造成重复执行?

这就是为什么需要幂等性。它不是为了理想情况设计的,而是为了应对混乱。

在一个完美的世界里,每个请求只会被处理一次,消息不会丢失,网络不会中断。但现实不是这样。幂等性是对现实的妥协,也是对不确定性的应对。

两种策略

实现幂等性有两种基本思路。

第一种是唯一标识。 每个请求带一个唯一的 ID,服务器记录下所有处理过的 ID。收到请求时先检查这个 ID 是否已经处理过。如果处理过,直接返回之前的结果;如果没有,执行操作并记录 ID。

这个策略简单直接,但需要存储。你要保存所有处理过的请求 ID,这在高频操作下会成为负担。你还要决定保存多久——永久保存不现实,但保存时间太短可能遇到延迟很久的重复请求。

第二种是操作本身的幂等性。 有些操作天然就是幂等的。查询操作不改变状态,执行多少次都一样。删除操作也是幂等的,删除一个不存在的东西不会出错。设置操作通常也是幂等的,把一个值设为 X,设一次和设十次结果相同。

问题在于,不是所有操作都能设计成天然幂等的。累加、扣减、追加,这些操作本质上不幂等。执行一次和执行十次的结果完全不同。

对这类操作,你需要转换思路。不要设计成"账户余额减 100",而是设计成"将订单 X 的支付状态设为已支付,相应调整余额"。前者不幂等,后者可以通过状态判断实现幂等。

优先设计天然幂等的操作

与其在非幂等操作上加幂等性检查,不如从设计层面就让操作变得幂等。

状态机是一个好工具。操作不是改变数据,而是推进状态。同一个状态转换请求,只会生效一次。

边界与成本

幂等性不是免费的。

存储请求 ID 需要空间。检查 ID 是否存在需要查询。这些都是成本。

更复杂的是时间边界。你要保存请求 ID 多久?一小时?一天?一周?

保存时间太短,延迟很久的重复请求会被当作新请求处理。保存时间太长,存储成本和查询性能都会成为问题。

这里没有完美答案。你需要根据系统特点做权衡。

如果你的系统网络稳定,超时重试间隔短,那么保存时间可以短一些。如果你的系统会有人工重试,或者消息队列可能延迟很久,那么保存时间需要长一些。

幂等性的边界,反映了你对系统混乱程度的预期。

不只是技术问题

幂等性检查背后,是一种思维方式的体现。

它承认失败是必然的。网络会断,服务会挂,请求会重复。系统设计不应该假设一切正常,而应该假设一切都可能出错。

它拥抱防御性编程。不要期待客户端表现完美,不要期待网络永远可靠,不要期待用户只点一次按钮。做好准备,让重复变得安全。

它追求的是稳定性,而不是效率。幂等性检查增加了复杂度,可能降低性能,但它换来了确定性。在混乱的环境中,确定性比效率更重要。

更广的视角

幂等性的思想不局限于技术系统。

生活中也有类似的场景。你发了一封重要邮件,不确定对方收到没有,你会不会再发一次?如果再发,你希望对方不会因为收到两封而困惑或产生误解。

这就是人际交往中的"幂等性"。重复的沟通不应该产生负面影响。"我再说一次"不应该被理解为"我认为你没听懂",而应该被理解为"我想确保信息传达准确"。

组织协作也是如此。一个决策被重复确认,不应该导致重复执行。"确认一下我们是不是要做 X"不应该触发 X 的重复启动。

系统需要有机制来识别:这是新的指令,还是对旧指令的确认?

稳定性的代价

追求幂等性是有成本的。

你需要额外的存储,额外的检查,额外的逻辑。你的代码会变得更复杂,你的系统会增加延迟。

但这个成本是值得的。

因为不幂等的代价更高。用户重复点击按钮导致重复扣款,消息队列的重复投递导致数据错乱,分布式事务的失败重试导致状态不一致。

这些不是理论上的风险,而是真实发生的事故。

幂等性是一种保险。 你为稳定性付出一些代价,来避免混乱带来的灾难。

在一个充满不确定性的世界里,能够安全地重复,本身就是一种确定性。


幂等性检查是否可以完全消除重复执行的风险?在什么情况下幂等性机制本身会失效?

如何权衡幂等性检查的存储成本和查询性能?在高并发场景下,幂等性验证可能成为瓶颈吗?

除了技术系统,生活中还有哪些场景需要"幂等性思维"?人际沟通、决策流程、协作机制中,如何设计"安全的重复"?

幂等性设计是否总是必要的?在哪些场景下,接受非幂等的风险反而是更好的选择?

分布式系统中,幂等性、一致性、可用性之间有怎样的权衡关系?

如何验证一个系统真的实现了幂等性?测试幂等性的策略是什么?


博客内链

Related Posts

Articles you might also find interesting

错误隔离

3 min read

失败是必然的。真正的问题不是失败本身,而是失败如何蔓延。错误隔离不是为了消除失败,而是为了控制失败的范围。

系统设计可靠性工程
Read More

监听 Redis 连接事件 - 让不可见的脆弱变得可见

4 min read

连接看起来应该是透明的。但当它断开时,你才意识到透明不等于可靠。监听不是多余,而是对脆弱性的承认。

系统设计Redis
Read More

文档标准是成本计算的前提

3 min read

API文档不只是写给开发者看的。它定义了系统的边界、成本结构和可维护性。统一的文档标准让隐性成本变得可见。

API文档
Read More

CRUD 操作

2 min read

四个字母背后,是数据的生命周期,是权限的边界,也是系统设计的基础逻辑

系统设计软件工程
Read More

端到端 Postback 模拟测试

2 min read

真实的测试不是模拟完美的流程,而是重现真实世界的混乱。Postback 测试的价值在于发现系统在不确定性中的表现。

测试API
Read More

实现幂等性处理,忽略已处理的任务

3 min read

在代码层面识别和忽略已处理的任务,不是简单的布尔检查,而是对时序、并发和状态的深刻理解

系统设计并发控制
Read More

单例模式管理 Redis 连接

5 min read

连接不是技术细节,而是系统与外部世界的第一次握手,是可靠性的起点

系统设计后端架构
Read More

监控观察期法

3 min read

部署不是结束,而是验证的开始。修复代码只是假设,监控数据才是证明。48小时观察期:让错误主动暴露,让数据证明修复。

系统设计监控
Read More

资源不会消失,只会泄露

2 min read

在积分系统中,用户的钱不会凭空消失,但会因为两个时间窗口而泄露:并发请求之间的竞争,和回调永不到达的沉默。

系统设计并发控制
Read More

RPC函数的原子化处理

1 min read

当一个远程函数做太多事情,失败就变得难以理解

分布式系统RPC
Read More

RPC函数

2 min read

关于远程过程调用的本质思考:当你试图让远方看起来像眼前

分布式系统RPC
Read More

指数退避超时 - 防止无限重试循环

3 min read

失败后立即重试是本能。但有些失败,需要时间来消化。指数退避不是逃避失败,而是尊重失败。

系统设计可靠性工程
Read More

管理后台需要两次设计

3 min read

第一次设计回答"发生了什么",第二次设计回答"我能做什么"。在第一次就试图解决所有问题,结果是功能很多但都不够深入。

系统设计API 设计
Read More

告警分级与响应时间

2 min read

不是所有问题都需要立即响应。RPC失败会在凌晨3点叫醒人。安全事件每15分钟检查一次。支付成功只记录,不告警。系统的响应时间应该匹配问题的紧急程度。

系统设计监控
Read More

API 测试各种边界情况

2 min read

边界情况是系统最脆弱的地方,也是最容易被忽略的地方。测试边界情况不是为了追求完美,而是为了理解系统的真实边界。

API测试
Read More

BullMQ 队列

3 min read

队列不是技术选型,而是对时间的承认,对顺序的尊重,对不确定性的应对

系统设计异步处理
Read More

BullMQ Worker

2 min read

Worker 的本质是对时间的重新分配,是对主线的解放,也是对专注的追求

系统设计异步处理
Read More

配置不会自动同步

2 min read

视频生成任务永远pending,代码完美部署,队列正确配置。问题不在代码,在于配置的独立性被低估。静默失败比错误更危险。

部署配置管理
Read More

数据库参数国际化:从 13 个迁移学到的设计原则

3 min read

数据不该懂语言。当数据库参数嵌入中文标签时,系统的边界就被语言限制了。这篇文章从 13 个参数对齐迁移中提炼出设计原则——国际化不是功能,是系统设计的底层约束。

数据库国际化
Read More

Stripe Webhook中的防御性编程

2 min read

三个Bug揭示的真相:假设是代码中最危险的东西。API返回类型、环境配置、变量作用域——每个看似合理的假设都可能导致客户损失。

Web开发系统设计
Read More

让文档跟着代码走

2 min read

文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。

文档软件工程
Read More

双重验证:Stripe生产模式的防御性切换

7 min read

从测试到生产不是更换API keys,而是建立一套双重验证系统。每一步都在两个环境中验证,确保真实支付不会因假设而失败。

系统设计Stripe
Read More

在运行的系统上生长新功能

3 min read

扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。

系统设计架构
Read More

缺失值的级联效应

3 min read

一个NULL值如何在调用链中传播,最终导致错误的错误消息。理解防御层的设计,在失败传播前拦截。

系统设计防御性编程
Read More

Props Drilling

3 min read

数据在组件树中层层传递,每一层都不需要它,却必须经手。这不是技术债,是架构对真实需求的误解。

React组件设计
Read More