首页 > 应用开发 > Java > 接口幂等设计:从重试到数据一致性
在日常后端开发中,我们经常会遇到这样的问题:用户只点击了一次按钮,但前端因为网络超时发起了重试;消息队列只希望消费一次,但实际投递了两次;第三方支付平台回调通知多次到达。如果服务端没有做好幂等控制,一次正常请求就可能变成重复创建订单、重复扣减库存、重复发放权益。
所谓接口幂等,简单来说就是:同一个业务请求执行一次和执行多次,最终结果应该一致。它不是为了阻止所有重复请求,而是让重复请求不会造成重复副作用。
并不是所有接口都必须做复杂的幂等设计。一般来说,读接口天然幂等,例如查询订单详情、查询用户信息。真正需要重点关注的是会修改数据的接口,例如:
这些场景都有一个共同点:一旦重复执行,业务结果可能发生变化。因此,设计接口时不能只考虑“正常请求”,还要考虑“同一个请求重复到达”。
最直接的方式是使用业务唯一键。例如创建订单时,可以由前端或服务端生成一个
requestId,同一次提交始终携带同一个值。服务端把这个值和业务数据一起落库,并建立唯一索引。
CREATE TABLE order_request ( id BIGINT PRIMARY KEY AUTO_INCREMENT, request_id VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, order_id BIGINT NOT NULL, created_at DATETIME NOT NULL, UNIQUE KEY uk_request_id (request_id) );
当重复请求到达时,数据库唯一索引会拦截重复写入。服务端捕获唯一键冲突后,可以查询之前生成的订单并返回相同结果。
这种方式简单可靠,特别适合创建类接口。需要注意的是,
requestId 必须和具体业务绑定,不能被不同业务场景复用。
有些业务不是简单的新增数据,而是状态流转,例如订单支付:
待支付 -> 支付中 -> 已支付 -> 已完成
这类场景可以通过状态机保证幂等。例如支付回调到达时,只允许
待支付 或
支付中 的订单更新为
已支付,如果订单已经是
已支付,则直接返回成功。
UPDATE orders
SET status = 'PAID', paid_at = NOW()
WHERE order_no = ?
AND status IN ('WAIT_PAY', 'PAYING');
执行后判断影响行数。如果影响行数为 1,说明本次完成了状态变更;如果影响行数为 0,则说明订单可能已经处理过,再查询订单状态确认即可。
这种方案的优点是逻辑清晰,尤其适合支付、审核、发货、退款等有明确状态流转的业务。
对于消息消费或第三方回调,也可以单独维护一张幂等记录表。每次处理前先写入一条处理记录,处理完成后更新状态。
CREATE TABLE idempotent_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_type VARCHAR(32) NOT NULL, biz_key VARCHAR(128) NOT NULL, status VARCHAR(16) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE KEY uk_biz (biz_type, biz_key) );
其中
biz_type 表示业务类型,例如
PAY_CALLBACK;
biz_key 表示唯一业务编号,例如支付平台流水号。这样即使不同业务使用相同编号,也不会互相影响。
处理流程可以设计为:
PROCESSINGSUCCESS这种方案比单纯依赖业务表更通用,适合复杂业务流程。
第一,不要只依赖前端按钮置灰。前端体验优化很有必要,但它不能替代服务端幂等。用户刷新页面、网络重试、接口被重复调用,服务端都必须能兜住。
第二,不要把幂等键设计得太宽泛。例如只用
user_id 作为唯一键,会导致同一个用户无法提交第二笔正常订单。幂等键应该标识“同一次业务请求”,而不是标识用户本身。
第三,不要忽略并发请求。两个相同请求可能几乎同时到达,如果只是在代码里先查询再插入,就会出现并发漏洞。更稳妥的方式是配合数据库唯一索引、事务或原子更新。
第四,幂等记录需要设置合理的保存周期。并不是所有记录都要保存,可以根据业务特点保留 7 天、30 天或更长时间。但支付、账务类数据通常需要更谨慎。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70050981/viewspace-3177208/ ,如需转载,请注明出处,否则将追究法律责任。