3. 幂等性设计核心原则:唯一标识、状态机、去重表、乐观锁、幂等键(Idempotency-Key)机制

在任务调度平台中,网络抖动、客户端重试、消息重复投递是常态。若接口不具备幂等性,一次任务创建请求可能产生多个重复实例,导致资源浪费、数据不一致甚至业务逻辑错误。本章围绕五项核心原则,构建一套可落地的幂等性设计体系。

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,服务端拦截器统一处理幂等逻辑,对业务代码透明。

幂等键机制处理流程 客户端 幂等拦截器 去重表 业务服务 请求+Key 查询Key 新请求 返回结果 重复请求:直接返回缓存结果 幂等键机制核心逻辑 ① 拦截器提取 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 作为幂等键,兼顾唯一性和时序性。
  • 状态机:定义任务实例的合法状态转换,拒绝非法重复操作。
  • 去重表:以幂等键为主键,插入成功则处理,失败则返回缓存结果。
  • 乐观锁:在状态更新时使用版本号,避免并发覆盖。
  • 幂等键机制:通过拦截器统一处理,对业务代码无侵入。