当前位置: 首页 > news >正文

系统设计中的幂等性

1. 基本概念

幂等性(Idempotence)是系统设计中经常提到的概念。

如果某个操作执行一次或多次都能产生相同的结果,那么它就是幂等的

2. 代码示例

下面这段代码是幂等的。无论你调用多少次,show_my_button 的最终状态都是False。

def hide_my_button(self):self.show_my_button = False

再看一个例子:

def toggle_my_button_visibility(self):self.show_my_button = not self.show_my_button

这个方法不是幂等的,因为每次调用都会翻转状态:第一次隐藏,第二次又显示。

3. 常见误区

3.1 幂等性与返回值无关

很多初学者会误解幂等性,认为“多次调用返回值相同”才叫幂等。这是错误的,来看一个例子:

def hide_my_button(self):has_something_changed = self.show_my_buttonself.show_my_button = Falsereturn has_something_changed

如果第一次调用时 show_my_button 是 True,返回值是 True;再次调用时返回值变成了 False。但这个方法依然是幂等的,因为无论调用多少次,show_my_button 的最终状态总是 False。幂等性关注的是操作的副作用或系统状态的最终结果,而不是方法的返回值。

3.2 幂等与纯函数

这两个概念也容易混淆,所以简单解释一下。
纯函数:给定相同的输入,总是返回相同的输出,且没有任何副作用。

# 这是一个纯函数。square(3) 将始终是相同的数字。
def square(my_number):return my_number ** 2# 这不是纯函数。square(3) 几乎不会产生相同的结果
def square_with_randomness(my_number):return (my_number ** 2) * random.uniform(0, 1)

幂等函数不一定是纯函数。幂等函数可以有副作用:

# 如果每次保存相同的 name,最终数据库的状态保持一致 → 幂等
def save_name(name):my_database.save(name) # 写入数据库, 有副作用return name

4. 幂等性的问题

幂等设计也可能引入问题:如果某条“毒消息”导致消费者每次崩溃,那么该消息永远留在队列中。于是服务不断重启、崩溃、重试,形成死循环。
解决办法:引入 死信队列(DLQ),将处理失败多次的消息转移到 DLQ,以便后续人工排查。

5. 系统设计中的幂等性

在分布式系统中,网络是不可靠的,服务会失败,消息可能重复投递,所以幂等性是保证数据一致性和系统健壮性的关键。以下场景都依赖幂等性:

  • 消息队列的重复消费
  • RESTful API 请求重试
  • 数据库写入与 UPSERT
  • 分布式系统故障恢复
5.1 消息处理

假设我们有一个事件驱动系统:

  • Service A 往消息队列里推送事件
  • Service B 消费这些事件(假设会执行一些计算操作)并写入数据库

如果Service B在计算过程中崩溃,或者Service B与数据库之间存在网络分区,或其他情况发生,那消息和事件将永远丢失。

解决方案:不立即从队列中删除消息,而是等待Service B完成(包括写入数据库),然后再删除消息。

但这带来了一个新问题:同一条消息可能被读取两次。Service B执行计算并写入数据库,但随后发生了一些情况(比如崩溃)。在消息从队列中删除之前,服务已经崩溃。会发生什么?当 Service B 重启后,将继续从最后那条消息开始消费,因此该消息被消费了两次!但这并不成问题,如果 Service B 的处理逻辑是幂等的,那么即使消息被重复消费,最终结果也是一致的。

缺点是需要一些额外的复杂性(需要保证操作幂等)和一些计算资源(可能不必要地多次执行相同操作)。但这些缺点和丢失消息比起来不值一提。

5.2 API

如果你正在构建REST API,其实已经在处理幂等性了。HTTP协议实际上定义了哪些方法应该是幂等的:

HTTP 方法幂等性说明
GET查询资源,多次调用结果相同
PUT完全替换资源,重复 PUT 没有副作用
DELETE删除资源,删除已不存在的资源结果也相同
POST通常用于创建资源,天然非幂等

POST请求:通常在设计上不是幂等的。每个POST通常创建某些内容。但你可以使用幂等键使它们幂等。其工作原理如下:随请求发送一个唯一ID(通常在头中),服务器记住"已经处理过这个ID,因此将返回相同的结果,而不是再次执行工作"。

def create_user(request):idempotency_key = request.headers.get('Idempotency-Key')# 是否已经处理过这个确切的请求?if idempotency_key and already_processed(idempotency_key):return get_cached_response(idempotency_key)# 没有,创建用户user = User.create(request.data)# 缓存响应以备下次使用if idempotency_key:cache_response(idempotency_key, user)return user
5.3 数据库

在数据库操作中,常用的幂等性设计包括:

UPSERT操作(如果存在则INSERT或UPDATE)自然是幂等的。使用相同数据运行update 10次,每次都会得到相同的结果。记录要么创建一次,要么多次更新为相同的值。

5.4 分布式系统

在分布式系统中,幂等性是重试机制的安全基础。

  • 网络调用失败可能导致请求被重发
  • 微服务之间的链路存在超时重试
  • 跨机房、跨服务调用可能会收到重复事件

如果操作不是幂等的,每次重试可能引入数据污染和状态异常。

6. 完整示例:订单处理系统

下面通过一个具体示例将所有内容整合起来。有一个简单的订单处理流水线:订单来自Web应用程序,由订单服务验证,进入队列,然后由订单处理器服务处理并写入数据库。

  • Web App → 提交订单请求
  • API Gateway → 路由、鉴权、限流
  • Order Service → 校验订单并发送消息到 MQ
  • MQ → 存储订单消息
  • Order Processor Service → 消费消息并写入数据库
  • Orders DB → 持久化订单数据
  • Dead-Letter Queue → 收集失败的消息
  • Notification Service → 给用户发送通知

这里的关键是 Order Service 要是幂等的。怎么使Order Service幂等?

Order Service从 MQ 消费消息并执行实际的业务逻辑。当处理消息(即订单事件)时,我们希望:

  1. 检查它是否已被处理
  2. 如果没有,将其插入Orders DB,告知Notification Service发送通知

这是幂等的,因为已经检查它是否已被处理。可以通过在订单表中执行SELECT操作,仅当不存在时才插入。可以对通知服务执行类似操作,或在通知服务内部使用其自己的数据库执行。

注意需要处理并发问题(有两个不同的订单处理服务实例处理同一条消息,并且它们同时执行SELECT),处理消息两次不是一个合理的做法,更好的方案是将此逻辑包装到事务中。

http://www.xdnf.cn/news/1369045.html

相关文章:

  • 【LeetCode 热题 100】31. 下一个排列
  • 进入docker中mysql容器的方法
  • Linux(二十二)——服务器初始化指南
  • 把 shell 脚本里的「后台接收」-- 以 UART/CAN 双总线监听为例
  • 影响服务器托管费用的因素​
  • 论文阅读-CompletionFormer
  • 山中游玩播报
  • 简单聊聊光栅化技术
  • 虚拟机中kubeadim部署的k8s集群,虚拟机关机了,重新开机后集群状态能否正常恢复的两种可能(详解)
  • vue2 创建threejs场景
  • ubuntu20.04 终端安装claude
  • 事件驱动架构详解
  • .gitignore 文件相关使用配置
  • 服务器数据恢复—热备盘上线失败如何恢复数据?
  • Ansible 自动化运维工具:介绍与完整部署(RHEL 9)
  • 如何基于阿里云OpenSearch LLM搭建智能客服平台
  • 亚马逊类目合规风暴:高压清洗机品类整顿背后的运营重构与风险防御
  • 零基础构建MCP服务器TypeScriptPython双语言实战指南
  • 零基础也能照做的WordPress网站安全漏洞修复 + 高级优化保姆级教程。
  • 【JavaEE】了解volatile和wait、notify(三)
  • 算法题打卡力扣第209题:长度最小的子数组(mid)
  • 【强化学习】区分理解: 时序差分(TD)、蒙特卡洛(MC)、动态规划(DP)
  • THM El Bandito
  • 使用C++与Qt6,在windows上打造MacOS风格桌面应用窗口
  • SELinux
  • Mac测试端口连接的几种方式
  • 【制作100个Unity游戏】从零开始构建类《月圆之夜》《杀戮尖塔》的卡牌游戏(附带项目源码)
  • CSS 结构伪类选择器
  • C语言开发入门教程:从环境搭建到第一个程序
  • 【lucene】SpanNotQuery 存在的意义