
1. 为什么接口需要幂等性从一个真实的线上事故说起先说一个我亲身经历的事故。几年前我做电商支付系统的时候有一次大促期间突然接到报警订单库里出现了好几条一模一样的重复订单。查了半天原因其实特别简单用户在下单页面手一抖连续点了三次“确认支付”按钮。前端没有做防重复提交后端接口也没做幂等处理结果请求一抖动网络层重试、网关超时重试最终三笔请求全部落库成功。用户被扣了三次钱我们被投诉砸了电话。这个场景其实就是接口幂等性要解决的核心问题同样一个请求无论你送进来一次、两次还是十次系统最终产生的结果必须和只送一次完全一致。换句话说幂等性保证的是多次请求不会产生副作用累积不会出现重复下单、重复扣款、重复发送短信这种灾难。我后来在团队内部经常用一句话给新人解释这件事幂等不是防止请求重复进来而是要保证请求重复进来的时候系统能认出来“这就是上一次那个请求”然后直接把这它丢掉或者把上一次的结果原样返回。这套能力在现在的分布式系统架构里几乎是刚需。因为在这个架构下幂等性差的不是某一个环节而是整个链路里的每一个环节都可能重复——浏览器重试、Nginx重试、网关重试、微服务间的RPC重试、消息队列的重投、定时任务的重复执行……任何一个环节多触发一次请求都会多跑一遍。所以我在做技术方案的时候从来不会只盯着一个接口去谈幂等而是把眼光拉高到全链路去考虑。这也是我这篇文章想和你分享的核心思路怎么从理论到落地把幂等性真正做成一套端到端的防御体系而不是每个接口各搞各的、东一个注解西一个拦截器。这篇文章适合谁我觉得只要你写过接口、做过业务系统尤其是跟订单、支付、库存、账务这类强一致场景打过交道的开发都值得读一遍。就算你目前只负责某个CRUD模块理解幂等性的设计思路也能帮你在做接口设计的时候少走很多弯路。2. 先把理论基础吃透幂等性的分类与本质2.1 天然幂等与非天然幂等的接口先说一个基本判断不是所有接口都需要你做额外的幂等处理。有些接口从语义上就是天然幂等的比如查询类接口。GET /user/1这种请求查一万次结果还是一样的它天然满足幂等。再比如DELETE /user/1如果用户已经删了再删一次结果依然是“已删除”从业务结果上看也是一致的通常也可以算作幂等。真正需要花心思的是那些会产生状态变更的接口典型的是POST类请求——创建订单、创建支付单、扣减库存、增加余额。这类请求每执行一次系统状态就变一次如果不做任何约束重复执行必然产生重复数据。这里有个常见的误区我要特别说一下很多人认为只要把POST改成PUT接口就幂等了。但实际上HTTP方法本身不保证幂等性PUT只是语义上应该幂等你如果实现得不好照样能跑出重复数据。相反POST也可以做到幂等关键看你怎么设计。HTTP语义只是一个约定落地要靠业务代码去保障。2.2 从业务视角看幂等性的四个层级我在工作里把幂等性分成了四个层级方便跟产品和测试对齐第一层是结果幂等意思是一个请求被执行多次最终在数据库里只有一条有效记录或者最终状态一致。这是最基础的保障大部分项目做到这一层就算合格。第二层是行为幂等意思是多次请求不能触发多次副作用。比如发送短信验证码的接口你要保证用户请求十次运营商那边最多只能收到一次下发指令因为下发短信是要花钱的这就是行为幂等。第三层是补偿幂等指的是当主流程失败、走了补偿流程之后整个系统的最终状态依然是正确的。分布式系统里特别常见比如支付超时之后转异步对账如果对账逻辑没做幂等账目对一次就多一笔对十次账户余额就乱了。第四层是全链路幂等也就是我今天重点讲的。它不仅要求单个接口幂等还要求从客户端到网关、到业务服务、到数据库、到消息队列整条链路在任何一点出现重复最终都能收敛到同一个正确结果。2.3 幂等性的核心机制是什么搞清楚了什么需要幂等下一步要知道幂等是怎么实现的。其实全世界的幂等方案底层核心机制就那么几类。最简单也是最常见的手段是唯一约束。数据库表上建一个唯一索引重复插入时直接报错由代码捕获异常并返回上一次的结果。这个方法实现成本最低但是有个问题唯一索引冲突报错比较“粗暴”对用户体验不友好而且它只保证了结果幂等行为上可能还是多重试了。更进一步的手段是分布式锁。用Redis或者ZooKeeper做一把锁同一个业务ID同时只能有一个请求在处理后面的请求等锁或者直接返回。这把锁能拦住并发重复请求但要注意锁的粒度、超时时间、释放时机用不好容易引入死锁或者锁失效的问题。最优雅的手段是状态机。如果业务本身有明确的状态流转比如订单从“待支付”到“已支付”到“已发货”那你可以用状态迁移做幂等只有在“待支付”状态才能执行支付动作支付成功后状态切到“已支付”再收到重复请求时发现状态已经是“已支付”直接校验通过并返回成功。状态机天然支持幂等而且业务语义清晰我强烈建议在有状态场景里优先考虑。还有一种手段是Token令牌机制通常用在需要主动控制的场景。服务端先生成一个唯一Token下发给客户端客户端提交请求时必须带上这个Token服务端处理请求时校验并销毁Token。Token一次性使用用完就无效重复请求拿不到有效Token自然就被拦住了。这个方案我对它的定位是“前置拦截”它能挡掉一部分重复但不够全面后面会细讲。3. 全链路幂等方案的核心设计思路3.1 为什么单节点幂等不够用了我刚入行那会儿大家讲的幂等方案基本都是单机思维数据库唯一索引加锁搞定。但在现在的微服务体系里单单一个唯一索引完全不够。原因很简单——你的服务往往是多副本部署的同一个请求被负载均衡分发到两个不同的实例上每个实例都在处理而且每个实例都有一份本地锁互相之间根本不知道对方在处理同一个请求。再往上走请求还会经过多层网络转发。前端重试、网关超时重试、服务间RPC框架的重试机制任何一个节点都可能把一个请求发出多份。消息队列那边也是消费者处理完消息之后还没来得及提交offset进程挂了消息会被重新投递一次业务逻辑就被再次执行。这些场景单点层面的幂等设计根本管不过来。所以我的结论很明确只要你的系统是分布式部署幂等就必须从头到尾按链路设计不是给某个接口加个注解就完事。3.2 全链路幂等的分层模型我设计一套全链路幂等方案时会按下面这个分层模型来思考最前端是接入层负责拦截最明显的重复请求。这一层主要做Token校验、去重表、短时间窗口内的请求指纹去重。它解决的是“人傻手快”的问题比如用户快速双击按钮。中间是服务层负责处理业务逻辑的幂等。这一层用业务维度做判断比如订单号、支付流水号、业务单据号。核心做法是幂等表 状态机保证同一个业务ID只处理一次业务操作。后端是数据层负责兜底用唯一约束、数据库事务保证数据层面的唯一性。这一层是最后一道防线即使前面的逻辑全部失效数据库层面也不能出现重复数据。再往旁边是异步链路负责消息和补偿任务的幂等。消费MQ的时候要做消息幂等定时任务跑批的时候也要做批次幂等防止重复执行。这四个层面互相配合每一层都挡一部分重复流量层层收敛最终保证全链路不重复。我管这个思路叫“层层削峰”每一层都做自己能做的不把压力全压到最后一道。3.3 链路中每个环节的重复来源分析要设计好全链路幂等首先得清楚每一层的重复到底从哪来。我梳理了一张自己常用的检查清单客户端这一层重复来源于用户反复点击按钮、页面刷新导致表单重复提交、移动端网络切换导致请求重发。处理手段是前端置灰按钮、防抖但这些只是缓解不能作为唯一防线因为客户端是不可信的。网关层重复来源于网关的超时重试策略。很多网关配置了“读超时后自动重试一次”这个本意是为了提高可用性但如果后端是个扣款接口重试一次就是多扣一次钱。所以网关层的重试必须和后端接口的幂等能力配套否则就是事故源。服务间调用层重复来源于RPC框架自身的重试机制。比如Dubbo默认在超时后会重试两次如果你调的下游接口不幂等超时重试就是灾难。我的建议是对于非幂等的敏感接口RPC重试次数设置为0宁可报错让上层决策也不要盲目重试。数据库层重复来源于应用层的死信重放、定时任务补单、又或者是人工手动修复数据时误操作。这一层主要靠唯一索引兜底。消息队列层重复来源于消费者拉取消息后未提交offset就发生宕机、重平衡、又或者是消息重复投递和延迟队列的重新入库。消费端必须自己实现幂等不能依赖MQ的“恰好一次”语义——事实上大部分主流的MQ都只保证at-least-once。每一层来源不同解法也不一样如果这些环节你都能提前识别并且分别做应对全链路幂等基本就稳了。4. 全链路幂等的落地方案从方案选型到代码实现4.1 方案选型什么场景用什么方案落到具体设计的时候第一个要决策的问题是要不要引入独立的幂等框架还是自研一套市面上开源的幂等框架有不少比如某些公司开源的基于注解的幂等组件原理上是AOP拦截你的方法在方法执行前生成唯一key检查Redis再执行业务方法最后释放锁。这类组件的优点是接入快、开发量小一个注解就搞定。缺点是灵活性受限很多业务场景里幂等key的生成逻辑是动态的不太适用于复杂场景。我个人的习惯是核心交易场景自己实现一套轻量的幂等组件框架通用需求用开源的方案。为什么因为核心交易的幂等跟业务强耦合需要自己控制幂等表的SQL、锁的粒度、状态机的流转用框架反而要不断去适配它的约定。自己写一套也没多复杂核心代码其实很少主要是思路清晰。下面我把两种方案都展开讲一下你可以根据自己的项目情况选。4.2 基于Token机制的接入层实现先看最简单的Token方案。这个方案很适合表单提交、用户主动发起型的接口。我经常用在注册、领取优惠券这类场景。整体思路是前端先向后端申请一个唯一Token后端生成后存在Redis里并返回前端。前端提交业务请求时把这个Token放在Header里后端在执行业务前先做Token校验校验通过的Token会被立即删除这样后面同样的请求再进来就查不到Token了直接拦截。具体代码逻辑大概是这个样子的// Token生成接口 PostMapping(/token) public ResultString generateToken() { String token UUID.randomUUID().toString().replace(-, ); stringRedisTemplate.opsForValue().set(TOKEN_KEY_PREFIX token, 1, 30, TimeUnit.MINUTES); return Result.success(token); } // 业务接口中的幂等校验 public boolean checkAndConsumeToken(String token) { // 这里必须用Lua脚本保证检查和删除是原子的 String script if redis.call(exists, KEYS[1]) 1 then redis.call(del, KEYS[1]); return 1; else return 0; end; Long result stringRedisTemplate.execute( new DefaultRedisScript(script, Long.class), Arrays.asList(TOKEN_KEY_PREFIX token)); return result ! null result 1; }注意检查和删除必须要做成原子的否则并发请求同时到达时两个请求都能查到Token就会都校验成功幂等就失效了。这里用Lua脚本就是为了避免并发检查时的竞态条件。Token方案有个明显的局限它是“事前”拦截只能拦住那些走了正确流程、拿到Token并提交的请求。如果请求里压根没带Token比如有人直接构造请求Token方案就拦不住了。所以它适合做第一道防线不能当全部依赖。4.3 基于唯一键和状态机的服务层实现再来看服务层的核心方案。我比较推荐的做法是“业务唯一键 幂等表 状态机”三者配合。先说业务唯一键。不是所有的请求都有自己的业务ID有些幂等场景需要我们自己生成请求唯一标识。比如客户端在发起请求时生成一个UUID传给服务端。这里有个原则幂等key必须是在客户端生成的而且是业务层面的唯一维度。对于订单来说就是订单号对于支付来说就是支付流水号对于退款来说就是退款请求号。总之每个业务请求都应该有一个与业务绑定的唯一ID。然后要建一张幂等记录表把处理过的请求记录下来。我常用的表结构是下面这样的CREATE TABLE idempotent_record ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, biz_type varchar(32) NOT NULL COMMENT 业务类型, biz_id varchar(64) NOT NULL COMMENT 业务唯一ID如订单号, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 处理状态 0处理中 1成功 2失败, result_data text DEFAULT NULL COMMENT 处理结果快照, request_body text DEFAULT NULL COMMENT 请求参数快照, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_biz_type_biz_id (biz_type, biz_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT幂等处理记录表;这张表的唯一索引uk_biz_type_biz_id就是整个幂等方案里最关键的一道防线。它的作用是无论你有多少个请求同时进来数据库层面只会允许一条记录插入成功其余的全部会因唯一键冲突而失败。接着是处理逻辑。我把核心流程写成了下面这个步骤请求进来后先根据业务类型和业务唯一ID查幂等表。如果查到状态为“成功”的记录直接返回上次的结果快照结果快照在幂等表里存一份不再执行业务逻辑。如果查到状态为“处理中”说明上一个请求还在处理中这时有两种做法一种是等待后重新查询另一种是直接返回“请勿重复提交”。如果没查到记录则尝试插入一条状态为“处理中”的记录。插入成功说明当前请求拿到了处理权继续执行真正的业务逻辑。业务执行成功把幂等表记录的状态更新为“成功”同时保存结果快照。业务执行失败把状态更新为“失败”。这里有个细节很关键先插入“处理中”记录而不是先执行业务逻辑这和“先写日志再干活”是一个道理是为了保证同一时刻只有一个请求能进入业务处理其他的请求在入口处就被挡住了。如果先执行业务再写幂等记录并发请求会全部涌入业务逻辑那幂等表就起不到拦截作用了。再配合状态机以支付场景为例订单状态有机会从“待支付”走到“已支付”。代码执行时先判断当前状态是否为“待支付”是才允许操作执行完改成“已支付”。假如订单已经是“已支付”状态再来一笔请求状态机直接返回“重复支付”并给出原支付结果完全不需要再走一遍支付流程。前一版我用数据库状态更新语句配合条件判断来实现UPDATE orders SET status PAID WHERE order_id #{orderId} AND status UNPAID如果这个SQL的影响行数是0说明订单状态不是“待支付”很可能已经被支付过了此时直接返回上次结果。这种写法既是幂等校验又是并发控制比先查后改要安全得多。4.4 数据库层的唯一约束兜底前面说了服务层逻辑再完美也有出bug的时候。所以数据库层必须有一个不能突破的底线唯一约束。比如支付流水表支付流水号必须加唯一索引。订单表里订单号肯定也是唯一的。这些索引的约束代码上要配合异常处理把唯一索引冲突的异常捕获下来翻译成对用户友好的返回信息不能让用户看到一个“Duplicate entry”的报错。我在代码里是这么处理的try { paymentMapper.insert(paymentRecord); } catch (DuplicateKeyException e) { // 唯一索引冲突说明已经存在同笔支付直接查老记录返回 PaymentRecord exist paymentMapper.selectByPaymentNo(paymentRecord.getPaymentNo()); return Result.success(exist); }这套逻辑虽然简单但特别实用。正因为有了这层兜底前面哪怕代码有并发bug没拦住数据库也会帮我拦住第二份数据的写入。所以我在团队里一直在强调关键表一定有一组“业务唯一键”的索引这不只是为了查询性能更是数据幂等的最后防线。4.5 异步链路的消息幂等处理消息队列这块要单独说因为很多同学在设计时容易漏掉。消费者从MQ里拿到的消息并不能保证只被消费一次。一般的MQ都支持至少一次投递重复消息是家常便饭。消息幂等的基本做法消息里面带上一个全局唯一的messageId消费端先尝试往消息去重表插入这个messageId。INSERT INTO mq_consume_record(message_id, status) VALUES(#{messageId}, PROCESSING)这条语句同样利用唯一索引去重。插入失败说明这个消息已经消费过直接跳过插入成功才执行真正的业务逻辑。这样即使MQ重复投递消息去重表也能挡住。但是这里有个更隐蔽的问题消息消费的业务处理完了消息去重表也标记完成了但如果在标记完成之前发生了宕机重启那么这条消息会被重投。重投的时候去重表里查不到“完成”状态于是又重新执行了一遍业务。所以纯粹靠消息去重表还是会漏。我在实践中加了两个补丁。第一个是状态机双保险消息消费的业务本身也带上业务ID做幂等消息来了先查业务单是否已存在存在就直接返回成功不重复创建。第二个是补偿机制定时任务定期扫描消费记录表和业务单据表发现两者状态对不上就触发补偿处理。这样一来即使单点失效最终也能收敛到一致状态。5. 实战中的关键细节与高频踩坑点5.1 幂等key的设计是成败关键我在评审别人的方案时第一眼永远是看他的幂等key设计得对不对。这是整个方案里最容易被搞错的地方。坏的设计就是把不稳定的东西当key用典型的就是拿时间戳当幂等key。同一个请求重试两次时间戳肯定不一样两次都能通过校验幂等直接失效。还有就是拿业务语义中会发生变化的字段当key比如用订单金额当key如果用户调价了这个key就变了同一个订单会生成两笔记录。好的设计原则是选择业务里不可变且全局唯一的字段。订单号、支付流水号、退款请求号这类就是标准选择。如果需要自己生成请求ID那就由客户端在发起请求时生成一个UUID服务端只认这个UUID。要注意的是服务端生成的ID往往不能用作请求幂等key因为重试发生时客户端还是拿同一个服务端ID不一定客户端可能在拿到服务端ID之前就发起了重试这时候客户端是拿不到同一个ID的只有客户端自己能保证每次重试都复用同一个ID。5.2 并发场景下必须加锁吗很多人问幂等校验和业务执行之间到底要不要加分布式锁。我的答案是看你的幂等表设计。如果你用的是“先插入处理中记录 唯一索引”的模式其实不需要额外加锁数据库的唯一索引本身就是一个分布式锁。并发请求全部去插桶只有一个能成功插入插入成功的那个就拿到了执行权。这种方案天然避免了对Redis锁的依赖也就少了一整套Redis锁的复杂性和故障风险。如果你用的模式是“先查后写”比如先查幂等表没记录然后执行业务逻辑最后再写记录那必须加锁否则并发下两个请求都查到“没有记录”都往里跑就重复了。所以“查了再写”的方案一定要配分布式锁不然就是给自己埋雷。我自己的倾向是尽量用“插入即锁”的模型少用“先查后写”。前者代码看起来更直接也更稳。5.3 超时、重试和回滚的配合全链路幂等还牵扯一个老生常谈的问题超时重试和事务回滚怎么配合。一个典型的悲剧场景支付接口接收请求开始处理支付处理超时了上游触发重试。第一次请求其实已经在数据库里写了一半只是响应超时客户端没收到成功消息。此时第二次请求进来如果幂等处理得好它应该能识别出第一次处理一半的状态然后继续等待、查询结果而不是重新再扣一次款。这里的关键是幂等表中“处理中”状态下的超时处理。我在设计时通常会为“处理中”状态的记录设置一个超时阈值比如30秒。如果30秒后这条记录还停留在“处理中”说明第一次请求可能已经异常此时允许后续请求接管处理。这种接管逻辑要谨慎设计仅仅靠更新时间去判断还不太够最好配合一个业务单状态确认确认业务单确实还处于初始状态才能接管。回滚方面最怕的是把幂等记录和业务记录放在不同的事务里导致业务回滚了、幂等记录却留下状态为“成功”那后续重试就永远被幂等表挡住。我的建议是幂等记录的状态更新和业务更新尽量放在同一个本地事务中或者明确约定状态的对账补偿机制不要让两边的状态漂移。5.4 不同接口形态的幂等策略对照不同形态的接口幂等策略不太一样我整理了一个对照表方便参考接口形态典型场景推荐幂等方案说明同步查询查询订单详情无需处理天然幂等同步提交创建订单、提交表单Token 幂等表Token做前置拦截幂等表做最终兜底同步状态变更支付、确认收货、退款状态机 幂等表状态流转是天然的幂等屏障异步回调支付回调、第三方通知消息去重 业务状态校验回调消息可能重投必须按消息ID去重定时任务批量跑批、补单批次号 去重表每次跑批生成唯一的批次号防止重复执行MQ消费订单同步、消息通知消息ID去重 业务幂等消费端不能依赖MQ只投一次这张表是我在做设计评审时经常用来对照的你可以直接拿去当团队的checklist用。5.5 我踩过的三个比较隐蔽的坑第一个坑是锁没覆盖幂等校验这段逻辑。我最初做Token方案时只在业务逻辑上加了Redis锁但Token校验在锁外面结果并发请求先同时过校验然后又同时进业务逻辑Redis锁根本拦不住已经校验通过的请求。后来把校验和业务处理放到同一个临界区里才解决。核心教训是校验和执行的边界必须跟锁的边界对齐。第二个坑是幂等表数据膨胀。有一段时间我们的幂等表里塞了海量的数据因为每个请求都记录request_body积少成多半年下来表已经几个G了。后来优化策略是request_body只保留精简版本或者超过一定大小就截断。幂等表的记录还可以根据业务周期归档保留最近三个月老数据定期清理。第三个坑是把幂等和防重混淆了。防重是防止用户在短时间内疯狂点击幂等是保证重复请求不产生副作用。这两个概念经常被混在一起讨论但实现方式差别很大防重可以用前端按钮置灰、限流、滑动窗口幂等要靠唯一键、状态机、去重表。如果只做了防重没做幂等用户绕过前端直接调接口问题还是会暴露如果只做幂等没做防重用户的体验会受影响而且无谓地消耗系统资源。两者要配合使用不能偏废。6. 一套可落地的全链路幂等组件设计参考6.1 组件整体架构前面讲的都是思路和单点方案最后我给出一套我实际在项目里用过的、相对完整的全链路幂等组件设计你可以直接参考它的模块划分。组件分四个核心模块KeyGenerator幂等key生成器。根据业务类型和业务上下文动态生成幂等key核心是支持SPI扩展让每个业务自己定义key的生成规则。IdempotentAspectAOP拦截器。用注解标注哪些方法需要幂等拦截方法执行在方法前后调用幂等框架。Storage存储抽象层。支持基于Redis的快速幂等也支持基于数据库的严格幂等业务可以根据场景选择。Recovery补偿模块。负责超时“处理中”记录的扫描和接管以及最终一致性的对账。整体的执行流程是请求进入AOP切面切面先调用KeyGenerator生成幂等key然后调用Storage检查是否存在成功记录存在就直接返回缓存的结果不存在则尝试写入“处理中”状态写入成功才放行进入业务方法业务方法执行完成后更新幂等状态为“成功”并把结果快照写入存储。6.2 注解设计与使用方式我对业务开发暴露出一个简单的注解使用方式如下Idempotent( type IdempotentType.ORDER, keyExpression #orderNo, storage IdempotentStorage.REDIS, timeout 30 ) public Order payOrder(String orderNo, Integer amount) { // 业务逻辑 }type表示业务类型keyExpression是SpEL表达式用来从方法参数里提取幂等keystorage选择存储介质timeout是“处理中”状态的超时时间。业务方只需要加这个注解不需要关心背后的实现细节。值得注意的是注解的拦截范围要清晰。我建议只对“写操作”且“可能产生重复副作用”的方法使用不要一刀切给所有接口都加上因为幂等也有性能开销加上Redis交互和数据库写操作高频查询接口如果也做幂等反而会拖慢响应。6.3 组件中的关键实现要点实现这套组件时有几个性能细节值得注意。第一个是存储层的双写策略Redis做前置拦截数据库做最终兜底。请求先查RedisRedis命中直接返回不碰数据库Redis未命中再走数据库的唯一索引判断。这样做的好处是大部分重复流量在Redis这一层就被拦截了数据库压力和日志量都会小很多。第二个是结果快照的取舍幂等返回的是“上一次请求的结果”所以框架必须把第一次请求的响应结果序列化后缓存起来。缓存快照不宜过大一般控制响应体在几十KB以内如果响应体特别大可以只缓存在Map中做短时缓存或者存摘要标识前端看到返回状态为“重复提交”甚至可以直接去查询业务状态未必每次都拿完整快照。第三个是异常场景下的释放策略如果业务方法执行抛出了异常幂等记录的状态要置为“失败”而不是保留“处理中”。因为“失败”意味着请求是可以重试的而“处理中”会拦住后续所有请求。这里要区分“业务失败”和“系统异常”业务规则校验失败属于正常返回幂等记录保持成功因为业务确实是处理过且被拒绝了系统异常属于需要重试的情况状态置为“失败”。6.4 如何把组件接入现有项目组件接入现有项目大概分三步。第一步在项目里引入依赖配置好Redis连接和幂等表的数据库脚本。第二步在需要幂等的方法上加上注解按照业务情况写好keyExpression。第三步上线前做一轮幂等测试脚本模拟并发重复请求看最终数据是否只有一条。我建议团队在测试环境专门写一个幂等测试的用例集覆盖这几个场景同一个请求连续两次提交、同一个请求并发十次提交、同一个请求被网关重试、同一个请求跨节点重试、消息队列重复投递。这些用例可以做成自动化脚本每次发版都跑一遍防止回归。7. 最后分享一点我的实际使用体会写到这里整套全链路幂等的思路和落地方式基本讲完了。最后我只想强调一点幂等性的建设不是一次性的不是上线了就一劳永逸。因为业务在变、接口在变、调用链在变新增一个接口很容易忘记加幂等校验改一个状态机也可能破坏原来的幂等逻辑。在我现在的团队里大家已经把幂等性列入了接口设计的必查项每次设计接口必须回答“这个接口如果重试三次是否会产生三条数据”如果会就必须给出幂等方案。这个问题已经成了日常评审时的口头禅。如果你正在做一个核心交易系统的接口设计我强烈建议你先把幂等表和业务唯一键梳理清楚再逐个把Token、状态机、消息去重加上去。花两三个小时把地基打好总比半夜被线上重复数据的告警叫起来强得多。这套方法我用了很多年虽然不花哨但真的很稳。希望这篇文章能帮你少踩几个我当年踩过的坑。