本文目录导读:

针对 Java 接口的幂等性(Idempotence)流程进行“规整”,核心目标是防止重复请求对系统状态产生副作用,规整意味着要建立一个清晰、可复用、易理解的框架,而非零散地在每个接口中写重复的检查逻辑。
以下是一套从原理到实现的规整流程及最佳实践。
核心核心核心:幂等性的本质与策略选择
- 本质:客户端发起同一个请求(例如支付、下单)多次,服务器只处理一次,后续请求直接返回第一次的结果(或成功提示),不会产生额外影响。
- 常见策略:
- Token 机制:适用于“前端预防”,前端先请求一个全局唯一 Token,后端在关键操作前验证并删除 Token(需要严格保证删除的原子性)。
- 去重表(幂等表):最常用、最可靠,利用数据库唯一索引(或分布式锁 + 表记录)作为锁,适用所有写操作。
- 乐观锁:适用于“更新操作”,依靠版本号或状态机。
UPDATE ... WHERE version = old_version AND status = 'pending'。 - 状态机:适用于“流程管控”,业务状态只能沿预定方向流转,不允许回退或重复触发已结束的状态。
规整的第一步:根据业务场景选择一种或组合策略。 通常推荐 “去重表”作为通用兜底方案。
规整流程架构(以“去重表 + Token”组合为例)
以下是规整后的系统流程示意图,可以清晰地拆分职责。
[客户端请求]
|
v
[1. 幂等 Token/Key 生成] (若使用Token机制,在此步骤)
| <-- 客户端携带 Token 或 业务幂等ID (如订单号、支付流水号)
|
v
[2. 幂等拦截器 / Filter] (统一入口,非侵入)
| - 提取幂等 Key (Header中的幂等Token, 或RequestBody中的流水号)
| - 校验Key非空 (对缺失的请求可直接拒绝或标记为“无幂等要求”)
|
v
[3. 幂等核心逻辑引擎] (核心规整点)
| a. **全局锁 & 去重检查**:
| - 尝试获取分布式锁 (Redis锁, key = "IDEMPOTENT:" + 幂等Key, 过期时间 3-5 秒)
| - 查询幂等表 (数据库) where request_id = 幂等Key
| ↓
| ├─ **存在且已成功**: 直接返回缓存的结果 (Response from DB)
| | (注意: 这里需区分“成功”与“处理中”状态,如果是“处理中”则返回“正在处理”异常)
| |
| └─ **不存在**: 插入幂等记录 (状态 = PROCESSING)
| | (利用唯一索引保证幂等插入,若插入失败说明已经被并发重复请求插入,回退并返回“重复请求”)
| |
| v
| b. **执行业务逻辑** (原Controller Method)
| - 执行业务逻辑 (下单、支付、写库)
| - 若业务成功: 更新幂等表状态为 SUCCESS, 并记录返回结果
| - 若业务失败: 更新幂等表状态为 FAILED (或回滚记录,根据业务需求)
|
v
[4. 统一返回响应]
- 返回业务结果 (第一次请求) 或 返回缓存的结果 (重复请求)
- 注意异常处理: 如果幂等锁超时或数据库异常,需给出明确的错误码 (如 429 Too Many Requests 或 409 Conflict)
代码层面的“规整”实现步骤
为了做到真正的规整(而非在每个接口里写重复代码),需要封装一个幂等注解 + AOP 切面 + 幂等组件。
定义幂等注解(Annotation)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// 幂等Key的来源
// 可选: HEADER / PARAM / BODY (如JSON字段) / TOKEN (自动生成)
Source source() default Source.TOKEN;
// 若source为PARAM或BODY,指定字段名 (如 "orderId", "requestId")
String field() default "";
// 幂等超时时间 (去重表记录保留时间, 单位秒)
long timeout() default 86400;
// 是否在业务失败后允许同Key重试 (默认不允许)
boolean allowRetryOnFailure() default false;
}
幂等组件核心实现(AOP + 去重表)
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private StringRedisTemplate redisTemplate; // 用于分布式锁与缓存
@Autowired
private IdempotentRecordMapper mapper; // 数据库幂等记录表 Mapper
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
// 1. 解析幂等 Key
String idempotentKey = extractKey(joinPoint, idempotent);
if (StringUtils.isBlank(idempotentKey)) {
// 若无法获取key,可拒绝请求或仅作记录 (根据业务)
throw new IllegalArgumentException("幂等Key必传");
}
// 2. 获取分布式锁 (防止多个实例并发插入去重记录)
String lockKey = "IDEM:LOCK:" + idempotentKey;
Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
if (!lock) {
// 未获取到锁,表示正在被其他实例处理,直接返回“处理中”
// 注意:此处是快速失败,也可设重试机制
return createRetryResponse("请求正在处理中,请稍后重试");
}
try {
// 3. 查询幂等记录
IdempotentRecord record = mapper.selectByKey(idempotentKey);
if (record != null) {
// 3.1 记录存在
if ("SUCCESS".equals(record.getStatus())) {
// 返回历史成功结果 (从缓存或记录中获取)
return deserializeResult(record.getResponseData());
} else if ("PROCESSING".equals(record.getStatus())) {
// 正在处理,返回处理中状态 (或用轮询等待机制)
return createProcessingResponse();
} else if ("FAILED".equals(record.getStatus())) {
// 失败记录
if (idempotent.allowRetryOnFailure()) {
// 允许重试: 继续往下执行业务
// 注意: 需要先将状态改回PROCESSING,或删除旧记录再插入新记录
// 简单处理:改状态 + 清空结果
mapper.updateStatus(idempotentKey, "PROCESSING");
} else {
// 不允许重试: 直接返回失败结果
return createFailedResponse("该请求已失败,不可重复发送");
}
}
} else {
// 3.2 记录不存在: 插入“PROCESSING”状态记录 (利用唯一索引保证幂等)
int insertCount = mapper.insert(new IdempotentRecord(idempotentKey, "PROCESSING", null));
if (insertCount <= 0) {
// 插入失败(并发冲突),说明有其他线程已插入相同key
// 返回“正在处理”或“重复请求”
return createRetryResponse("重复请求,请稍后重试");
}
}
// 4. 执行目标方法 (业务逻辑)
Object result = joinPoint.proceed();
// 5. 更新幂等记录为成功 (或缓存结果)
mapper.updateStatusAndData(idempotentKey, "SUCCESS", serializeResult(result));
return result;
} catch (Exception e) {
// 6. 异常处理: 更新状态为失败
mapper.updateStatus(idempotentKey, "FAILED");
throw e;
} finally {
// 7. 释放分布式锁
redisTemplate.delete(lockKey);
}
}
}
数据库设计“去重表”(最关键)
CREATE TABLE `idempotent_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `idempotent_key` varchar(255) NOT NULL COMMENT '幂等Key (requestId/orderId/token)', `status` varchar(20) NOT NULL DEFAULT 'PROCESSING' COMMENT '状态: PROCESSING / SUCCESS / FAILED', `response_data` text DEFAULT NULL COMMENT '业务返回结果JSON', `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_idempotent_key` (`idempotent_key`) USING BTREE COMMENT '这是一切的基础', KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='幂等去重表';
规整的关键原则与避坑指南
-
Key 的选取必须是全局唯一 + 业务相关:
- 推荐:业务流水号(如
orderId,paymentId)或 UUID(需由前端提供并持久化)。不要只用时间戳或随机数,因为无法对应同一笔业务。 - Token 机制的规整:前端提交时需携带 Token,Token 在业务入库前必须保证原子性删除(建议使用 Redis Lua 脚本
DEL key配合NX模式)。
- 推荐:业务流水号(如
-
去重表是核心,分布式锁是保障:
- 唯一索引保证了并发写入时也能通过数据库层面发现冲突。
- 分布式锁是为了减少不必要的数据库 IO(比如锁等待期间,后续请求直接等待锁或返回“处理中”)。
- 不要完全依赖缓存(Redis)做幂等,数据库才是最终一致性的保障。
-
状态机是状态变动的规则:
- 订单支付”接口,订单状态由
待支付->支付中->已支付。 - 幂等检查:查询订单当前状态是否已为
已支付,若是则直接返回成功,否则继续处理。
- 订单支付”接口,订单状态由
-
响应数据必须可序列化:
- 幂等表需要存储
response_data(JSON),彻底保证“幂等获取结果一致”,缓存的返回不能有副作用。
- 幂等表需要存储
-
不要对“读”接口做幂等:
读接口天然幂等,无需套用此流程,只针对写(增、删、改)操作。
-
失败重试策略:
- 默认业务失败了,同 Key 的请求是不允许再执行的,除非明确配置
allowRetryOnFailure = true,且修改幂等状态回PROCESSING。
- 默认业务失败了,同 Key 的请求是不允许再执行的,除非明确配置
规整流程的标准化步骤
- 定义幂等 Key:所有写接口统一从 Header(开中台推荐)或 RequestBody 中提取
idempotent_key。 - 入口拦截:通过自定义注解 + AOP 统一拦截,而非修改 Controller 代码。
- 核心去重:数据库唯一索引 + 分布式锁(或原子操作)。
- 状态管理:
PROCESSING -> SUCCESS / FAILED。 - 异常兜底:超时、并发插入失败等异常时,返回明确的 HTTP 状态码(409 Conflict 或 200 + 业务错误码)。
- 结果缓存:成功后将序列化的结果存幂等表,重复请求返回缓存结果。
这套规整方案完成后,你只需在需要幂等的方法上加 @Idempotent 注解,即可享受完整的幂等安全防护。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播