幂等性设计实战:Python通用方案解决重复执行难题

发布时间:2026/9/9 16:08:59
幂等性设计实战:Python通用方案解决重复执行难题 做后端开发这么多年我总结出一个规律线上事故里至少有一半跟“重复执行”有关——消息队列重投一次、定时任务多跑一次、用户手抖连点两下、RPC 超时后客户端自动重试。在“至少一次投递”这种消息语义下重复执行不是“万一发生”的小概率事件而是“早晚会发生”的确定性事件。幂等性就是把这件不可控的事情变成“可控变量”的核心手段。这篇文章想聊透一件事幂等性到底是什么、为什么它是分布式系统的必修课以及在 Python 里怎么用一套通用设计把它真正落地。适合正在做接口开发、消息消费、支付回调、定时任务的工程师也适合所有想把“重复执行”这个坑彻底填平的朋友。1. 先搞清楚“重复执行”到底哪里可怕1.1 一次支付回调事故的现场很多年前我维护过一个订单系统支付回调是个标准的“at-least-once”场景支付网关会反复重试回调重试间隔从几秒到几小时不等。最初代码写得很简单拿到回调就更新订单状态、发短信、加积分。结果某个晚上网关集中重投同一个订单被连续处理了两遍积分加了两份短信发了两条最麻烦的是财务对账怎么对都对不上。这个事故让我真正理解了“重复执行”的可怕之处它不是一个简单的“多跑一次”的问题而是所有副作用都会被叠加一次。发短信、加积分、改库存、记账这类操作重复执行一次业务数据就产生一次不可逆的偏差。如果刚好赶上并发重复两个线程同时执行同一段逻辑还会出现“检查时没问题、写入时互相覆盖”的诡异情况。1.2 幂等性的数学定义和工程定义先看严格的数学定义。一个操作是幂等的指的是执行一次和执行 N 次产生的结果完全一致写成公式就是 f(f(x)) f(x)。典型的例子是 SET 操作把变量设置为 1不管执行一次还是执行一百次变量的值都是 1。而像“计数器加一”“往列表追加数据”“发送一条短信”这类操作天然不是幂等的执行两次结果就变了。工程上的幂等没有数学上那么严格我更愿意把它理解为“业务结果一致”同样一个请求重复到达不管处理几次最终数据库里的状态、用户收到的通知、账户里的余额都应该跟只处理一次时一样。这种弱化版的幂等才是我们在实际系统里真正需要的。理解了这一点你就不会纠结于“到底要不要做到数学意义上的完全一致”而是会把重点放在“哪些副作用可以接受重复、哪些绝对不可以”。1.3 哪些场景必然遇到重复执行只要系统里出现下面任意一种情况你就默认已经处于“重复随时会发生”的状态消息队列采用 at-least-once 投递消费者处理完但 ack 超时、消费者宕机后分区重分配、生产者发送时重试都会产生重复消息。HTTP 客户端超时重试请求已经打到服务器业务也执行成功了但响应在网络中丢失客户端以为失败于是重新发起请求。定时任务重跑调度平台重复调度、手动补数据、上一次执行超时被判定失败后自动拉起都会把同一批数据再处理一遍。前端重复提交用户双击按钮、弱网卡顿后反复点击、浏览器重新提交表单这些都是最常见的重复来源。第三方回调重试支付成功回调、短信发送回执、开放平台的事件推送几乎都是“不确认就重试”的策略。有一个共性是这类场景的通用背景触发方没有办法感知接收方的真实处理结果只能靠重试来确保“消息不丢”。既然重试必然带来重复接收方就必须具备识别重复并正确处理重复的能力这就是幂等存在的原因。1.4 “可控变量”的三层含义“把重复执行变成可控变量”这句话不是一句玄学口号它有三层具体的含义第一层重复不再产生副作用。最理想的状态是业务逻辑本身做到了幂等重复请求进来后直接“短路”不会触发任何副作用。第二层重复是可以被观测的。系统需要有能力识别“这是一条重复请求”并且把识别结果记录到日志和监控里。这样当线上数据异常时你能够根据幂等键去反查“这条数据被哪个环节重复处理了”。第三层重复是可以被管理的。你可以决定重复请求是“静默忽略”还是“返回上次处理结果”还是“触发告警”。不同业务场景需要不同的处理方式而好的幂等设计会把这几种策略统一收口。2. 为什么说幂等是“至少一次投递”下的必修课2.1 at-least-once 到底保证什么消息队列里最常见的投递语义是 at-least-once也就是“至少一次”。它保证消息不丢失但允许消息重复。主流的消息中间件比如 Kafka、RocketMQ、RabbitMQ 在特定的确认模式下都遵循或者可以配置成这种语义。Kafka 的机制最典型消费者处理完一条消息后需要向 Broker 提交 offset。如果消费者在处理完成之后、提交 offset 之前崩溃了那么这条消息会被重新消费。哪怕消费者在业务代码里做了数据库事务提交只要 offset 没提交上去重启后还是会把这条消息再读一遍。这跟你业务逻辑写得好不好没关系是消息系统本身的语义决定的。所以“至少一次投递”本质上是在跟你做一笔交易把“消息一定不会丢”作为承诺代价是“你需要自己处理重复”。很多人刚接触消息队列时会忽略这后半句等到线上出现重复数据才反应过来——这个代价是躲不掉的。2.2 重复的三种形态在至少一次投递的体系里重复并不是只有一种形态我把它拆成三种第一种消费者处理成功但确认丢失。最典型的就是 Kafka 里 offset 提交失败或者 RabbitMQ 的 ack 在网络中丢失。这种情况下业务已经完整执行成功了但消息系统认为没成功于是重新投递。这是最无辜的重复因为业务代码没有任何问题纯粹是确认环节断了。第二种消费者处理过程中异常。消息消费到一半数据库连接断了或者代码抛异常了消息被重新派发。这种重复最危险因为第一次执行可能已经把一部分副作用写入进去了。比如订单状态已经改成了“处理中”短信已经发出了但后续步骤失败了重试时又从头走一遍流程。第三种生产者重试导致物理重复。生产者发送消息时网络超时后重新发送Broker 可能同时收到两条内容相同的消息。这种重复发生在源头消费者拿到的甚至可能是不同 message id 的“双胞胎消息”仅靠消息 id 去重是去不掉的。理解这三种形态很重要因为它们对幂等设计的要求不一样第一种只需要一个结果标记就能挡住第二种还需要考虑部分生效的恢复第三种则要求幂等键必须基于业务维度而不是消息维度。2.3 没有幂等重试机制就失去了意义很多团队早期不敢开重试或者把重试次数压得很低不是因为重试本身有什么问题而是因为系统不幂等。一旦开了重试重复副作用就像开了闸一样涌出来重复扣款、重复发货、重复发短信客诉和资损马上就来。这其实是把“可靠性手段”和“业务正确性”对立起来了。但正确的做法不是回避重试而是通过幂等把重试的代价降到零。一个幂等的接口无论被重试多少次业务结果都一样这时候你才敢放心大胆地把重试次数调大、把重试策略做成指数退避。可以说幂等性是重试机制能够真正发挥作用的前提。2.4 从“至少一次”到“恰好一次”的语义转变很多人会问既然 at-least-once 会产生重复那有没有 exactly-once 的语义直接一步到位严格来说在分布式环境下物理上做到“只执行一次”是不可能的。你没法保证系统在任意时刻崩溃后不留下任何中间状态也没法靠一个全局锁把所有并发的重复请求挡住。业内常说的 exactly-once本质上是“逻辑上的恰好一次”也就是通过幂等和去重让外部观察者看到的效果跟恰好执行一次完全一致。Kafka 的 exactly-once 就是这么实现的事务性生产者保证消息不重复写入消费者端靠幂等操作保证重复消息不会产生重复效果。真正的核心还是幂等没有幂等任何“恰好一次”都是口号。3. Python 幂等实战真正可落地的四种设计方案3.1 设计前先回答三个问题动手写代码之前我建议先回答三个问题想不清楚就直接写大概率会返工第一个问题靠什么识别同一条重复请求这就是“幂等键”。它可以是一个 request_id、一个 order_no、一个业务摘要但必须满足一个条件——同一次业务操作的所有重试都携带同一个键。第二个问题在哪个环节拦截重复是接口层统一拦还是业务层根据状态判断还是数据层靠唯一约束硬挡我的经验是三层各司其职但核心判断逻辑越靠近数据层越可靠因为只有数据层能够保证原子性。第三个问题重复请求到达时返回什么有些场景希望返回上次成功的结果有些场景只需要返回一个“成功”或“处理中”即可。不同的返回策略会影响缓存的设计建议在设计阶段就跟前端或上游对齐。3.2 方案一基于状态机的分支判断配合数据库行锁这是我最推荐的方案适合业务对象本身有明确状态流转的场景比如订单、任务、审批单。核心思路是用业务状态本身来标识“这个对象处理到了哪一步”状态一旦流转到终态后续的重复请求就会走“已处理”分支不再触发副作用。from enum import Enum class OrderStatus(str, Enum): PENDING_PAY pending_pay PAID paid REFUNDED refunded def process_payment_callback(order_id: str, user_id: str, amount: int): # 使用数据库事务并对订单行加锁防止并发重复同时读到旧状态 with db.transaction(): order db.query( SELECT * FROM orders WHERE order_id%s FOR UPDATE, order_id, ) if order.status OrderStatus.PAID: # 已经处理过直接返回成功不重复执行副作用 return {result: duplicated, status: order.status} if order.status OrderStatus.REFUNDED: # 已经退款还收到支付成功回调属于异常需要告警 alert(已退款订单收到支付回调: %s, order_id) return {result: abnormal, status: order.status} # 正常处理更新状态再做积分、短信等副作用 db.execute( UPDATE orders SET status%s WHERE order_id%s, OrderStatus.PAID, order_id, ) grant_points(user_id, amount) send_sms(user_id, 支付成功) return {result: success, status: OrderStatus.PAID}这里最关键的是SELECT ... FOR UPDATE行锁。为什么要加锁因为如果两个重复回调同时进来都先查询到订单处于PENDING_PAY状态两边都认为是第一次处理然后都会执行grant_points积分就加了两遍。加了行锁之后第二个事务必须等第一个事务提交才能读取到数据这时候订单状态已经是PAID走的就是duplicated分支。这个方案还有一个好处不需要额外的去重表也不需要 Redis纯粹靠业务状态本身就能完成幂等。缺点是要求业务对象必须有清晰的状态机如果你的业务是一堆无状态的中间操作这个方法就不太适用。3.3 方案二唯一键去重表有些场景没有状态机或者业务表本身就是日志流水比如用户签到、点击统计、内容曝光。这类场景适合单独建一张“幂等记录表”把业务唯一标识作为主键利用数据库唯一索引挡住重复插入。CREATE TABLE idempotent_records ( idempotent_key VARCHAR(128) PRIMARY KEY, biz_type VARCHAR(32) NOT NULL, request_body TEXT NOT NULL, response_body TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP );对应的 Python 处理逻辑import json import time import uuid from database import db, IntegrityError def process_with_dedup_table(biz_type: str, request_body: dict): # 生成稳定的业务幂等键同一次业务操作复用同一个键 idempotent_key make_biz_idempotent_key(biz_type, request_body) try: db.execute( INSERT INTO idempotent_records (idempotent_key, biz_type, request_body) VALUES (%s, %s, %s), idempotent_key, biz_type, json.dumps(request_body), ) except IntegrityError: # 唯一键冲突说明重复请求读上次结果返回 record db.query_one( SELECT response_body FROM idempotent_records WHERE idempotent_key%s, idempotent_key, ) if record and record.get(response_body): return json.loads(record[response_body]) return {result: processing, message: request is being processed} # 插入成功说明是第一次到达执行业务逻辑 result do_business(request_body) db.execute( UPDATE idempotent_records SET response_body%s WHERE idempotent_key%s, json.dumps(result), idempotent_key, ) return result这个方案里有一个非常重要、也最容易踩坑的细节去重记录的插入和业务操作必须在同一个数据库事务里。也就是说INSERT INTO idempotent_records和do_business(request_body)里的所有写操作要么一起提交要么一起回滚。如果不在同一个事务里会出现什么情况第一次请求插入去重记录成功了但业务逻辑执行了一半失败了事务回滚之后去重记录却还在。那么下一次重试时系统会误判为“重复请求”直接返回processing或者上次的结果但实际上业务根本没有成功。这会导致“永远无法真正执行成功”的尴尬状态比重复副作用更难排查。3.4 方案三Redis SETNX 幂等标记对于高频接口每次去数据库查去重表可能性能不够可以考虑用 Redis 的SETNX做一个轻量的幂等标记。它特别适合这种场景重复请求只需要拿到一个“成功”或“处理中”的信号不需要完整恢复上次结果。import redis import time r redis.Redis(host127.0.0.1, port6379, decode_responsesTrue) class IdempotentGuard: def __init__(self, redis_client, ttl3600): self.redis redis_client self.ttl ttl def try_acquire(self, idempotent_key: str) - bool: # SETNX 加过期时间原子操作不存在才设置成功 ok self.redis.set(idempotent_key, str(int(time.time())), nxTrue, exself.ttl) return bool(ok) def mark_done(self, idempotent_key: str, result: str): result_key f{idempotent_key}:result self.redis.set(result_key, result, exself.ttl) def get_result(self, idempotent_key: str): return self.redis.get(f{idempotent_key}:result) def pay_callback_with_redis(idempotent_key: str, payload: dict): guard IdempotentGuard(r) if not guard.try_acquire(idempotent_key): # key 已存在可能是重放也可能还在处理中 cached guard.get_result(idempotent_key) if cached: return json.loads(cached) return {result: processing} # 第一次到达执行业务 result do_pay_business(payload) guard.mark_done(idempotent_key, json.dumps(result)) return result这里我想特别提醒几个坑。第一个坑是 TTL 设置。Redis 的 key 如果设置了过期时间但业务处理时间超过了 TTLkey 就会提前消失。这时候另一个重复请求进来SETNX又能成功于是重复执行又发生了。所以 TTL 必须大于业务的最大处理时间最好留出充足余量。如果业务处理时间波动很大可以考虑用一个独立的 key 续期线程定期刷新过期时间但那样复杂度会高很多。第二个坑是 Redis 不是强一致存储。在极端情况下Redis 主从切换、持久化丢失都可能导致幂等标记丢失。如果业务对资损零容忍就不要单独依赖 Redis 做唯一的幂等保障而是让它作为第一道“快速拦截”的屏障数据层再用唯一约束托底。第三个坑是“业务失败时是否删除关键”。很多实现会在except Exception里把 key 删掉这样下一次重试可以重新执行。但这里要区分失败类型如果业务没产生任何副作用就失败了删掉是合理的如果业务已经部分生效删掉 key 就会导致第二次重试把副作用再做一遍反而更糟。后面我还会专门展开讲这个问题。3.5 方案四封装成通用幂等装饰器如果公司内部有很多接口都需要幂等支持可以考虑封装一个通用装饰器把“检查幂等键、占位、调用业务、缓存结果”的逻辑统一收口。这样业务代码不用每处都重复写一套去重逻辑。import functools import json import redis r redis.Redis(host127.0.0.1, port6379, decode_responsesTrue) def idempotent(redis_clientNone, key_prefixidem, ttl3600): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): idempotent_key kwargs.get(idempotent_key) or kwargs.get(request_id) if not idempotent_key: raise ValueError(缺少 idempotent_key 参数) full_key f{key_prefix}:{func.__name__}:{idempotent_key} placeholder_ok redis_client.set(full_key, 1, nxTrue, exttl) if not placeholder_ok: cached redis_client.get(f{full_key}:result) if cached: return json.loads(cached) return {code: PROCESSING, message: request is being processed} try: result func(*args, **kwargs) # 业务成功后再缓存结果 redis_client.set(f{full_key}:result, json.dumps(result), exttl) return result except Exception: # 只有确认业务没有副作用时才推荐删除占位 key redis_client.delete(full_key) raise return wrapper return decorator idempotent(redis_clientr) def create_order(idempotent_key: str, product_id: str, user_id: str): # 真实业务逻辑 return {order_id: 12345, status: created}装饰器写起来很爽但我个人的建议是在核心链路、涉及资金的场景尽量不要只依赖装饰器。原因是装饰器把幂等逻辑和业务逻辑放在同一个进程里一旦业务代码内部产生远程调用、开启子事务装饰器很难帮你管理“业务副作用是否已部分生效”这类细致状态。更稳妥的方式是把它当成一道“前置快速拦截层”真正的幂等兜底仍然放在数据库或消息内部的业务状态里。换句话说装饰器可以帮助你挡住 99% 的重复请求剩下 1% 的极致并发和异常情况必须靠更底层的机制来兜底。3.6 幂等键怎么生成才靠谱幂等键是整个幂等设计的地基地基要是歪了上面设计得再好都没用。我看到最常见的错误是在重试时重新生成一个 UUID 作为幂等键。这样做的结果是每次重试对于服务端来说都是一个新的请求幂等机制完全失效。正确的幂等键通常来自这几个方向调用方生成并透传客户端在一次业务操作开始时生成一个request_id后续所有重试、回调、消息都带着它。这是最常见的方案前提是上游愿意配合。服务端基于业务摘要生成对于创建类接口如果用户重复提交且没有传 id服务端可以用业务字段算一个摘要比如user_id product_id amount做哈希。但这个方案有个风险就是如果用户确实要下两笔完全一样的订单摘要会冲突。所以需要再加上一个时间窗口或者一个随机因子。消息队列场景不要只用 message id 做幂等键因为上面说过生产者重试可能产生两条物理消息message id 不同但业务内容相同。应该用业务唯一标识比如order_no event_type。Kafka 消息可以放到 key 字段RocketMQ 可以放到 keys 字段。另外要注意幂等键的编码。如果幂等键会作为 Redis key 或者数据库主键最好限制长度统一格式。我习惯用统一前缀加冒号分隔比如idem:order:create:20250101120000:order_no_123这样既方便排查也避免撞 key。4. 实战中容易翻车的细节从原理到排查4.1 幂等与并发先查再写为什么不行很多刚接触幂等的人会写出这样的代码先查一下记录存不存在不存在就执行业务。这在单线程场景下没有问题但一旦有两个请求同时进入两个线程都可能查不到记录然后同时执行业务。这就是典型的“检查与执行之间有时间窗”中间隔着一次网络或线程调度的间隙给了并发重复可乘之机。要解决这个问题必须保证“检查并占位”这个动作是原子的。数据库的FOR UPDATE行锁、唯一索引插入时触发的唯一约束冲突、Redis 的SETNX都是原子性的操作。它们本质上都是同一个思路让多个并发请求竞争同一个资源只有其中一个能赢其余全部被挡住。所以在设计阶段就要想清楚你的幂等判断到底是“先查再写”还是“先占位再写”。如果是前者就必须有一把锁或者一个唯一约束来防止并发穿透。这是我反复强调的关键很多线上故障都是因为第一步就写错了。4.2 业务失败后要不要删幂等标记这个问题我见过太多团队翻车。先说结论不能一刀切地删或者不删要看失败的类型。如果业务代码在进入核心副作用之前就抛异常了比如参数校验失败、上游返回业务错误、请求体解析失败这时候没有任何状态被修改删除幂等标记是安全的下一次重试还可以重新尝试。但如果失败发生在副作用已经部分生效之后情况就复杂了。比如你已经调用了支付接口、在本地标记了订单已支付但后面给用户发积分的动作抛异常了。此时如果把幂等标记删掉下一次重试会重新走一遍支付流程用户可能被重复扣款。正确的做法是保留这个幂等标记并抛出一个“可业务补偿”的错误码让上层知道这次请求需要走人工补偿或者自动对账而不是简单重试。实操中我会把异常分成两类一类是“可安全重试”的比如依赖服务网络超时、数据库连接抖动这类可以删标记或允许重试另一类是“可能产生部分副作用”的比如外部接口状态不确定、本地更新已提交但后续失败这类绝不删标记直接走人工或异步对账。4.3 幂等记录清理与 TTL 设置去重表和 Redis 标记如果永久保留数据会一直增长最终影响查询性能和存储成本。但你也不能随便清否则会破坏幂等窗口。幂等记录保留多久取决于“重复请求可能到达的时间范围”。支付回调的重试窗口通常不会超过 24 小时短信回执的重试窗口更短。一般保留 3 到 7 天是足够安全的。如果你做的是对账系统需要支持历史消息重放那么保留时间就要拉长甚至按全量归档处理。Redis 侧的 TTL 也要遵循同样的逻辑。不要为了省内存把 TTL 设得太短否则会出现“重复请求晚到一步幂等标记已经过期”的尴尬。一个基本的经验是TTL 必须大于最极端的重试间隔同时大于业务最大处理时间两者取较大值再额外加一个安全窗口。4.4 外部接口的幂等别只做内部防护幂等设计不能只盯着自己系统的内部逻辑还要考虑外部依赖。当你调用支付宝、微信支付、短信服务、物流接口时如果这些外部服务不幂等你的内部幂等做得再漂亮也没用。一个非常典型的例子支付网关支持同一个out_trade_no重复发起请求时返回同一笔结果这就是外部幂等。你在调用时把本地订单号传给对方对方就能在收到重复请求时识别并去重。相反如果某次调用超时了你自己已经生成了一个新请求号去重试就可能产生两笔支付单。所以每次接入外部接口我做的第一件事就是看文档里有没有“幂等键”、“商户订单号”、“out_trade_no”之类的参数。有的话想尽办法把它填上。没有的话就要评估是否需要在本地用状态机加人工确认。这看起来是小事但足以决定一次对接是否经得起故障考验。4.5 幂等设计自检清单检查项说明幂等键是否稳定同一次业务操作的所有重试是否复用了同一个 key检查与执行是否原子是否使用了行锁、唯一约束、SETNX 等原子手段而不是先查再写并发重复是否覆盖是否考虑了两个线程同时到达同一个请求的场景事务边界是否完整去重写入和业务异步副作用是否在同一事务或一致机制内失败时是否区分处理是否区分了“无副作用失败”和“部分副作用失败”外部依赖是否支持幂等是否传递了商户订单号、业务唯一标识给外部服务记录清理策略是否设置了合理的清理窗口不会过早清除也不会无限增长是否有监控告警幂等冲突、重复请求、状态异常是否能被观测并触发告警5. 线上问题排查怎么定位是不是幂等的锅5.1 排查步骤线上出现重复数据时先不要急着改业务逻辑按照下面的顺序排查通常能很快定位问题第一步在日志里用业务唯一标识比如 order_no、user_id 加时间范围把所有相关请求捞出来看有没有同一条请求被处理了多次。日志里如果找不到第二次到达的痕迹那问题可能出在数据链路更深处。第二步查幂等记录表或者 Redis 里的幂等标记。如果记录表里没有对应记录说明重复请求穿过了幂等层问题出在幂等判断本身。第三步看时间戳。如果两条重复请求的时间差只有几十毫秒大概率是并发穿透如果时间差在几分钟甚至几小时那就是消息重投或者回调重试。第四步查幂等标记有没有被误删。很多框架代码里有except Exception: delete key的逻辑如果业务曾经抛出过异常key 被删掉了后续重试就会重新执行。第五步检查 TTL 或记录过期时间。如果重复请求到达时幂等标记正好已经过期也会导致重复执行。5.2 一个真实的排查案例有段时间我们的积分系统总出现“用户积分重复增加”的投诉。排查时我先拉出了这个用户的所有积分变动记录发现同一条消息的message_id不同但业务字段完全一致说明是生产者重试导致了两条物理消息。这时候如果拿message_id做幂等键根本查不出重复。继续往下查发现幂等键用的确实是user_id action_type但同一天内用户做了两次完全相同的动作幂等键发生了碰撞第二次操作被误判为重复导致积分没加上。这里的问题不是幂等机制失效而是幂等键的粒度太粗同一个用户同一种动作在这个业务里是有合法重复场景的。最终的方案是把幂等键改成user_id action_type 操作对象唯一ID并且把业务上的“同一动作窗口”也纳入键的生成规则比如加上一个客户端生成的操作时间窗口问题才根治。这个案例充分说明幂等键的设计不是拍脑袋而是要先理解业务中“什么才算同一次操作”。6. 就到这里聊点经验我不打算做那种“通过本文我们了解了……”的总结就聊点我自己的工程习惯。早些年做系统我总觉得幂等是一个“要做但不用太急”的优化项直到连续踩了支付回调和消息重投的坑以后我把幂等当成编码前的必答题。现在每写一个接口、每写一个消息消费者、每写一个定时任务我都会先问自己一句如果这段代码被重复执行一次会发生什么多问几次很多隐患在写代码阶段就消灭了。另外一个特别实用的习惯是所有对外接口都要求请求方传一个request_id如果客户端没传服务端自动生成一个并透传。刚开始可能觉得这些 id 用不上但如果系统里每个请求都有统一的追踪标识到排查线上问题、做全链路日志关联的时候你会发现这笔“设计债”还得很值。如果你正准备做一个需要对外接回调、对接消息队列的新系统我强烈建议在数据库模型和接口契约阶段就把幂等方案放进去不要等到上线后打补丁。后期补幂等的成本通常是设计阶段就做的十倍以上。这不是夸张因为后期你还要面对存量数据的清洗、重复数据的修复、业务流程的兼容每一样都让人头大。最后分享一句我目前比较认可的组合拳思路接口层用 Redis 幂等标记挡住绝大多数重复请求数据层用唯一约束或状态机托底业务层对“可安全重试”的异常才放行重试。三层配合既保证了大部分请求的处理速度又保证极端情况下不会出现重复副作用。这套思路不一定要照搬但方向是通用性的。如果正在阅读这篇文章的你也为“重复执行”头疼过希望这些踩坑记录能帮你少走几步弯路。

相关新闻