Java接口幂等流程如何规整

wen java案例 35

本文目录导读:

Java接口幂等流程如何规整

  1. 核心核心核心:幂等性的本质与策略选择
  2. 规整流程架构(以“去重表 + Token”组合为例)
  3. 代码层面的“规整”实现步骤
  4. 规整的关键原则与避坑指南
  5. 规整流程的标准化步骤

针对 Java 接口的幂等性(Idempotence)流程进行“规整”,核心目标是防止重复请求对系统状态产生副作用,规整意味着要建立一个清晰、可复用、易理解的框架,而非零散地在每个接口中写重复的检查逻辑。

以下是一套从原理到实现的规整流程及最佳实践。

核心核心核心:幂等性的本质与策略选择

  1. 本质:客户端发起同一个请求(例如支付、下单)多次,服务器只处理一次,后续请求直接返回第一次的结果(或成功提示),不会产生额外影响。
  2. 常见策略
    • 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='幂等去重表';

规整的关键原则与避坑指南

  1. Key 的选取必须是全局唯一 + 业务相关

    • 推荐:业务流水号(如 orderId, paymentId)或 UUID(需由前端提供并持久化)。不要只用时间戳或随机数,因为无法对应同一笔业务。
    • Token 机制的规整:前端提交时需携带 Token,Token 在业务入库前必须保证原子性删除(建议使用 Redis Lua 脚本 DEL key 配合 NX 模式)。
  2. 去重表是核心,分布式锁是保障

    • 唯一索引保证了并发写入时也能通过数据库层面发现冲突。
    • 分布式锁是为了减少不必要的数据库 IO(比如锁等待期间,后续请求直接等待锁或返回“处理中”)。
    • 不要完全依赖缓存(Redis)做幂等,数据库才是最终一致性的保障。
  3. 状态机是状态变动的规则

    • 订单支付”接口,订单状态由 待支付 -> 支付中 -> 已支付
    • 幂等检查:查询订单当前状态是否已为 已支付,若是则直接返回成功,否则继续处理。
  4. 响应数据必须可序列化

    • 幂等表需要存储 response_data(JSON),彻底保证“幂等获取结果一致”,缓存的返回不能有副作用。
  5. 不要对“读”接口做幂等

    读接口天然幂等,无需套用此流程,只针对写(增、删、改)操作。

  6. 失败重试策略

    • 默认业务失败了,同 Key 的请求是不允许再执行的,除非明确配置 allowRetryOnFailure = true,且修改幂等状态回 PROCESSING

规整流程的标准化步骤

  1. 定义幂等 Key:所有写接口统一从 Header(开中台推荐)或 RequestBody 中提取 idempotent_key
  2. 入口拦截:通过自定义注解 + AOP 统一拦截,而非修改 Controller 代码。
  3. 核心去重:数据库唯一索引 + 分布式锁(或原子操作)。
  4. 状态管理PROCESSING -> SUCCESS / FAILED
  5. 异常兜底:超时、并发插入失败等异常时,返回明确的 HTTP 状态码(409 Conflict 或 200 + 业务错误码)。
  6. 结果缓存:成功后将序列化的结果存幂等表,重复请求返回缓存结果。

这套规整方案完成后,你只需在需要幂等的方法上加 @Idempotent 注解,即可享受完整的幂等安全防护。

抱歉,评论功能暂时关闭!