3. 幂等性设计核心原则
在任务调度平台中,网络抖动、客户端重试、消息重复投递是常态。若接口不具备幂等性,一次任务创建请求可能产生多个重复实例,导致资源浪费、数据不一致甚至业务逻辑错误。本章围绕五项核心原则,构建一套可落地的幂等性设计体系。
3.1 唯一标识:幂等性的第一道防线
唯一标识是幂等性设计的基石。每个请求在到达服务端时,必须携带一个全局唯一的标识符,服务端据此判断该请求是否已被处理。
| 标识类型 | 生成方式 | 适用场景 | 注意事项 |
|---|---|---|---|
| UUID v4 | 随机生成,128位 | 客户端生成,通用场景 | 需考虑碰撞概率,但极低 |
| 雪花算法 ID | 时间戳+机器ID+序列号 | 分布式系统,服务端生成 | 依赖时钟同步,需处理回拨 |
| 业务流水号 | 业务前缀+日期+序列 | 可读性要求高的场景 | 需保证序列号唯一性 |
| 组合键 | 多个业务字段拼接+哈希 | 无单独ID字段的遗留系统 | 字段变更可能导致重复 |
实践建议:任务调度平台推荐使用雪花算法 ID 作为幂等标识。它既能保证全局唯一,又能通过时间戳字段快速判断请求时序,便于后续的过期清理。
3.2 状态机:约束请求的合法流转
任务实例具有明确的生命周期:待调度 → 运行中 → 成功/失败/超时。状态机通过定义合法状态转换路径,天然防止重复操作导致的异常状态。
核心规则:状态机只允许单向流转,禁止回退。当收到重复请求时,若当前状态已是“成功”,则直接返回成功结果,不再执行任务逻辑。
3.3 去重表:持久化幂等记录
去重表是幂等性最直接的实现方式。在数据库中建立一张幂等记录表,以唯一标识为主键,在请求处理前尝试插入,插入成功则继续处理,插入失败则说明请求重复。
-- 幂等去重表结构示例
CREATE TABLE idempotent_record (
idempotent_key VARCHAR(64) NOT NULL PRIMARY KEY, -- 幂等键
request_body TEXT, -- 请求体(可选)
response_body TEXT, -- 响应体
status TINYINT NOT NULL DEFAULT 0, -- 0:处理中 1:已完成
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME,
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意:去重表需要定期清理过期记录。建议设置 TTL 为 24 小时,使用定时任务删除 completed_at 超过 24 小时的记录,避免表数据膨胀。
3.4 乐观锁:并发场景下的幂等控制
当多个请求同时操作同一资源时,乐观锁通过版本号机制保证数据一致性。每次更新时检查版本号是否匹配,匹配则更新并递增版本号,不匹配则说明数据已被其他请求修改。
-- 乐观锁更新示例(任务状态更新)
UPDATE task_instance
SET status = 'RUNNING',
version = version + 1,
updated_at = NOW()
WHERE id = #{taskId}
AND version = #{expectedVersion}; -- 关键:版本号匹配检查
-- 影响行数为 0 表示版本冲突,需重试或返回冲突错误
| 对比维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 锁粒度 | 行级锁,阻塞其他事务 | 无锁,通过版本号控制 |
| 适用场景 | 写冲突频繁 | 读多写少,冲突概率低 |
| 性能影响 | 高并发下锁竞争严重 | 无锁等待,性能更优 |
| 幂等性支持 | 需配合唯一索引 | 天然支持幂等更新 |
3.5 幂等键(Idempotency-Key)机制:统一入口规范
幂等键机制将上述原则整合为统一的 API 规范。客户端在 HTTP 头部携带 Idempotency-Key,服务端拦截器统一处理幂等逻辑,对业务代码透明。
// 幂等键拦截器核心逻辑(伪代码)
public Response handleRequest(Request request, String idempotentKey) {
// 1. 查询去重表
IdempotentRecord record = idempotentDAO.selectByKey(idempotentKey);
if (record != null) {
// 2. 已存在记录:直接返回缓存结果
if (record.getStatus() == 1) {
return Response.fromJson(record.getResponseBody());
}
// 3. 处理中:等待或返回冲突
throw new ConflictException("请求正在处理中");
}
// 4. 新请求:插入记录(利用唯一索引防重复)
idempotentDAO.insert(new IdempotentRecord(idempotentKey, "PROCESSING"));
try {
// 5. 执行业务逻辑
Response response = businessService.process(request);
// 6. 更新记录为已完成
idempotentDAO.updateStatus(idempotentKey, "COMPLETED", response.toJson());
return response;
} catch (Exception e) {
// 7. 失败处理:可标记为失败或删除记录
idempotentDAO.updateStatus(idempotentKey, "FAILED");
throw e;
}
}
最佳实践总结:
- 唯一标识:使用雪花算法 ID 作为幂等键,兼顾唯一性和时序性。
- 状态机:定义任务实例的合法状态转换,拒绝非法重复操作。
- 去重表:以幂等键为主键,插入成功则处理,失败则返回缓存结果。
- 乐观锁:在状态更新时使用版本号,避免并发覆盖。
- 幂等键机制:通过拦截器统一处理,对业务代码无侵入。