接口幂等性的边界与高并发场景
用户连续点两次下单按钮、支付回调重试、消息队列重复投递,同一请求被多次处理,业务结果必须保持与第一次一致。这就是接口幂等性,后端开发里最容易被低估又最容易出事故的设计点。高并发场景下问题还会放大:并发重试同时进入,去重逻辑本身要防并发竞争,不能简单查一次再插入。
方案一:数据库唯一键/幂等表
利用数据库唯一约束实现幂等,是最稳妥的方案。支付回调场景建一张幂等表:
CREATE TABLE idempotent_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64) NOT NULL,
request_hash VARCHAR(64) NOT NULL,
result TEXT,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_biz (biz_id)
) ENGINE=InnoDB;
处理请求前先INSERT幂等记录,biz_id为业务唯一标识(如订单号+操作类型)。插入成功说明是第一次,执行业务逻辑;插入冲突说明重复请求,直接返回已处理结果。注意要把执行业务和插入记录放到同一事务里,否则记录先落库业务失败,回滚后又可以重复执行。
方案二:Token令牌机制
前端提交前先向后端申请一个Token,后端把Token存在Redis(或内存)并设置过期时间;提交时携带Token,后端用DEL命令原子删除,删除成功才执行。利用Redis单线程特性,DEL成功只有一个请求,天然解决并发问题:
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("idem:" + token, "1", Duration.ofMinutes(10));
// 提交时
Boolean deleted = redisTemplate.delete("idem:" + token);
if (!Boolean.TRUE.equals(deleted)) {
throw new BusinessException("重复提交或请求已过期");
}
这个方案适合表单提交、下单确认等用户可见操作,缺点是需要前置接口,重试机制、网关层重放会自动再申请Token,可能失效。
方案三:Redis分布式锁
对纯接口做幂等,用分布式锁配合状态机检查:
String lockKey = "lock:order:" + orderId;
RLock lock = redissonClient.getLock(lockKey);
boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("操作进行中,请稍后重试");
}
try {
if (orderService.isProcessed(orderId)) {
return orderService.getResult(orderId);
}
orderService.process(orderId); // 业务方法里再配合数据库唯一约束
} finally {
lock.unlock();
}
分布式锁防的是并发同时进入,业务幂等还是要靠数据库约束兜底。锁的过期时间要大于业务执行时间,否则锁提前释放后其他请求重复执行。
方案四:状态机与版本号
订单状态推进类接口(待支付→已支付→已发货),用状态判断天然幂等:只有当前状态匹配才允许流转。版本号(乐观锁)适合更新类操作,UPDATE时带version条件,返回影响行数为0则视为重复。这两类接口用状态判断,比所有方案都轻。
各方案怎么选
原则:写入型操作优先用数据库唯一键兜底;用户提交类操作加Token;并发的重复请求再加分布式锁;状态变更类直接用状态机。多数业务是几种组合使用,比如下单同时用Token + 数据库唯一键 + 状态机,三层防护保证高并发下也不会产生脏数据。实现幂等之后,别忘记给幂等判断加监控指标,重复请求的数量本身就是排查线上问题的信号。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/gao-bing-fa-xia-dan-jie-kou-mi-deng-xing-she-ji-shi-zhan/