微服务幂等性设计从原理到落地:接口防重实现方案对比

微服务架构里,网络重试、消息重复投递、用户双击提交都会让同一个请求被执行多次。如果接口不保证幂等,就会产生重复订单、重复扣款、重复入账一类问题。本文从幂等的定义讲起,给出几个可落地的接口幂等实现方案,并给出在Spring Boot环境下的实现示例与注意点。

先明确:什么样的接口算幂等

幂等是指”同一个请求无论执行一次还是多次,产生的业务结果一致”。GET天然幂等,POST天然不幂等,PUT/PATCH部分幂等。开发时先区分三类接口:查询类(天然幂等)、写入类(需要用业务标识去重)、计数类(扣减库存、扣款,需要原子操作+幂等令牌)。

方案一:唯一键约束(数据库层幂等)

最简单可靠的方式,在业务表上建唯一索引,用业务主键保证同一条记录只能插入一次:

-- 订单表:业务单号+用户ID唯一
CREATE TABLE t_order (
  id BIGINT PRIMARY KEY,
  order_no VARCHAR(32) NOT NULL,
  user_id BIGINT NOT NULL,
  status TINYINT,
  UNIQUE KEY uk_order_no (order_no)
);
// 捕获重复插入异常
try {
  orderMapper.insert(order);
} catch (DuplicateKeyException e) {
  // 重复请求,返回已存在的订单
  return getByOrderNo(order.getOrderNo());
}

这个方案零额外存储成本,但依赖数据库唯一索引,对”同一订单多次修改状态”这类场景不适用,只能防重复插入。

方案二:Redis幂等令牌(分布式环境下更通用)

// 前端提交前先获取令牌,随请求携带
String token = UUID.randomUUID().toString();
redis.set(token, "1", 5, TimeUnit.MINUTES);

// 处理接口时先删除令牌(原子操作)
// 能删成功 -> 第一次请求,放行
// 删除失败 -> 重复请求,直接拒绝
boolean ok = redis.eval("if redis.call('get', KEYS[1]) then return redis.call('del', KEYS[1]) else return 0 end", ...);

关键点是用Lua脚本保证”校验+删除”的原子性,避免并发下两个请求同时通过校验。令牌过期时间不能太短,要覆盖用户提交到服务端确认之间的网络延迟。这套方案的缺点是接口需要配合改造,且Redis不可用时需要降级策略。

方案三:消息消费幂等(状态机+去重表)

MQ消费场景下,同一消息可能被多个消费者消费,幂等不能靠接口层保证,要在消费端做:

// 消费前先查去重表
if (dedupMapper.exists(requestId)) {
    return; // 已处理过,直接ack
}
try {
    processBiz(bizData);
    dedupMapper.insert(requestId); // 处理成功后落去重记录
} catch (Exception e) {
    // 失败不落去重记录,允许重试
}

这里务必要先处理业务再插入去重记录;反过来的话,业务失败但去重表已插入,后续重试会被跳过。去重表建议单独建,主键用request_id,定期清理三个月前的数据。

方案对比与选用原则

方案 实现成本 适用范围 限制
数据库唯一约束 插入类接口 无法处理状态更新
Redis令牌 写接口全场景 依赖Redis高可用
去重表 消息消费、异步任务 需要定期清理

选用原则:能靠业务唯一键解决的(订单号、requestId)优先用数据库约束;需要跨服务幂等的用Redis令牌;消息场景用去重表。幂等是业务语义,必须让开发者在接口文档中声明”是否幂等、幂等键是什么”,这是工程规范里比代码更重要的部分。

幂等的验证与回归:并发压测

实现完用并发工具对同一请求压测:100并发打同一个幂等键,断言最终只产生一条业务记录。注意压测要覆盖”同一key不同请求体”和”不同key相同业务”两类异常情况,这两类最容易漏。幂等改造上线前,建议把”重试按钮连点”和”消息重复投递”两个场景写进回归用例。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/wei-fu-wu-mi-deng-xing-she-ji-cong-yuan-li-dao-luo-di-jie/

(0)
小编小编
上一篇 1小时前
下一篇 1小时前

相关推荐