ForgeAdmin实战:开源项目分布式幂等组件 v2.0 升级

0 阅读6分钟

我在开源项目重构了分布式幂等组件:支持三种策略、Token防重放、结果缓存

为什么要重构幂等组件?

在企业级开发中,幂等性是保障数据一致性必不可少的能力。之前我在 Forge Admin 开源项目中实现了一个基础版本的幂等组件,但随着使用场景越来越多,发现了一些问题:

问题影响
无结果缓存重复请求只能拒绝,用户体验差
缺少Token机制无法防范CSRF和恶意重放攻击
没有监控统计无法评估幂等效果和性能
锁实现简单仅使用SETNX,长时间业务可能锁过期
策略单一只支持拒绝,无法满足不同场景需求

因此我参考了业内主流开源项目的设计思想,对幂等组件进行了全面重构,推出了 v2.0 版本。

重构后的整体架构

重构后的架构更加清晰,分层职责明确:

┌─────────────────────────────────────────────────────────┐
│                Idempotent Framework v2.0                 │
├─────────────────────────────────────────────────────────┤
│                                                           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  @Idempotent │  │  Idempotent │  │   Context   │  │
│  │  Annotation │  │    Aspect    │  │   Manager   │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
│                                                           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │   Token      │  │   Result     │  │    Lock      │  │
│  │   Service    │  │    Cache     │  │   Manager    │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
│                                                           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │   Monitor    │  │   Storage   │  │   Strategy   │  │
│  │   Metrics    │  │   (Redis)    │  │   Provider   │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
│                                                           │
└─────────────────────────────────────────────────────────┘

核心特性

1. 支持三种幂等策略

针对不同业务场景,提供三种不同的幂等处理策略:

策略处理逻辑适用场景
STRICT严格拒绝重复请求订单创建、支付处理等关键操作
RETURN_CACHE返回上次缓存结果查询类操作、可重复读取
TOKEN_REQUIREDToken验证优先前端防重复提交、防重放攻击

2. 幂等结果缓存

当重复请求发生时,可以返回上次成功执行的缓存结果,无需重复执行业务逻辑:

  • 提升用户体验(用户无需重新提交)
  • 减少系统负载(避免重复计算)
  • 缓存有效期可配置

3. Token机制防重放

提供完整的Token生成、验证、消费机制:

  • Token单次消费,使用后即失效
  • Token绑定用户ID,防止跨用户盗用
  • 短TTL,减少泄露风险
  • 提供REST API供前端调用

4. Redisson分布式锁

基于Redisson实现增强型分布式锁:

  • 支持看门狗自动续期,长时间业务执行不会提前过期
  • 可配置锁等待时间和租约时间
  • 公平锁支持,避免饥饿

5. Prometheus监控集成

内置完整的监控指标:

指标类型说明
idempotent.requests.totalCounter请求总数
idempotent.requests.successCounter成功次数
idempotent.requests.duplicateCounter重复次数
idempotent.cache.returnedCounter缓存返回次数
idempotent.cache.hit.rateGauge缓存命中率
idempotent.execution.timeTimer执行耗时分布

快速开始

1. 添加依赖

<dependency>
    <groupId>com.mdframe.forge</groupId>
    <artifactId>forge-starter-idempotent</artifactId>
</dependency>

2. 配置文件

forge:
  idempotent:
    enabled: true
    prefix: "idempotent:"
    expire: 600
    message: "请勿重复提交"
    
    cache:
      enabled: true
      expire: 3600
      
    token:
      enabled: true
      expire: 300
      header: "X-Idempotent-Token"
      
    lock:
      enabled: true
      wait-time: 3000
      lease-time: 5000

使用示例

示例1:严格模式(订单创建)

@PostMapping("/order/create")
@Idempotent(
    strategy = IdempotentStrategy.STRICT,
    prefix = "order:",
    key = "#orderRequest.orderId",
    message = "订单正在处理中,请勿重复提交"
)
public RespInfo<Order> createOrder(@RequestBody OrderRequest orderRequest) {
    return RespInfo.success(orderService.create(orderRequest));
}

行为:第一次请求正常执行,重复请求直接抛出异常拒绝。


示例2:缓存模式(订单查询)

@GetMapping("/order/{orderId}")
@Idempotent(
    strategy = IdempotentStrategy.RETURN_CACHE,
    prefix = "order:query:",
    key = "#orderId",
    cacheExpire = 300
)
public RespInfo<Order> queryOrder(@PathVariable String orderId) {
    return RespInfo.success(orderService.getById(orderId));
}

行为:第一次请求执行查询并缓存结果,重复请求直接返回缓存结果,不访问数据库。


示例3:Token模式(支付处理)

第一步:前端获取Token

const res = await axios.post('/api/idempotent/token/generate', {
  prefix: 'payment'
});
const token = res.data.data.token;

第二步:携带Token请求

await axios.post('/api/payment/process', paymentData, {
  headers: {
    'X-Idempotent-Token': token
  }
});

第三步:后端验证

@PostMapping("/payment/process")
@Idempotent(
    strategy = IdempotentStrategy.TOKEN_REQUIRED,
    prefix = "payment:",
    key = "#paymentRequest.paymentId"
)
public RespInfo<PaymentResult> processPayment(@RequestBody PaymentRequest request) {
    return RespInfo.success(paymentService.process(request));
}

Token API 一览

获取Token

POST /api/idempotent/token/generate
{
  "prefix": "order"  // 可选
}{
  "code": 200,
  "data": {
    "token": "abc123...",
    "expireSeconds": 300,
    "createTime": 1678923456789
  }
}

批量获取Token

POST /api/idempotent/token/batch-generate
{
  "count": 10,
  "prefix": "order"
}

验证Token

POST /api/idempotent/token/validate
{
  "token": "abc123..."
}
→
{
  "code": 200,
  "data": true
}

Redis 存储结构

Key类型Key格式TTL
幂等标记idempotent:{prefix}:{businessKey}expire秒
结果缓存idempotent:cache:{prefix}:{businessKey}cacheExpire秒
Token存储idempotent:token:{prefix}:{tokenValue}tokenExpire秒
分布式锁idempotent:lock:{prefix}:{businessKey}Redisson管理

设计思路总结

为什么选择这三种策略?

在实际业务中,不同场景对幂等的需求是不一样的:

  • 关键写操作(如创建订单):必须严格防止重复,所以用 STRICT 模式
  • 读多写少的查询:重复查询结果一样,用 RETURN_CACHE 可以提升性能
  • 前端表单提交:用户可能重复点击,用 TOKEN_REQUIRED 配合Token可以有效防止重复提交

为什么选择Redisson而不是自己实现?

Redisson的分布式锁已经经过生产环境验证,特别是看门狗自动续期这个特性,自己实现很容易出问题,站在巨人肩膀上更好。

结果缓存会不会有一致性问题?

是的,如果业务数据更新了,缓存还没过期,会返回旧数据。所以建议:

  1. 缓存过期时间设置合理(一般几分钟)
  2. 更新数据时可以提供手动清理缓存的接口
  3. 不建议对一致性要求非常高的场景使用缓存模式

踩坑记录

坑1:SpEL表达式解析没有异常处理

一开始直接解析,不捕获异常,如果用户写错了表达式,整个接口直接500。现在改成:解析失败回退到参数哈希方案,保证接口可用。

坑2:参数名获取方式兼容性问题

一开始用 StandardReflectionParameterNameDiscoverer,如果编译没有开 -parameters 参数,就获取不到参数名,SpEL中的 #参数名 就失效了。现在改成 DefaultParameterNameDiscoverer,它会自动尝试多种方式,兼容性更好。

坑3:全局开关动态不生效

一开始只在自动配置上加了 @ConditionalOnProperty,如果运行时通过配置中心关闭,切面还是会执行。现在在切面切入点再次检查配置开关,保证即时生效。

项目地址

完整代码已经开源在 Forge Admin:

👉 gitee.com/ForgeLab/fo…

欢迎 Star 关注,如果你对幂等组件有更好的想法,欢迎提 Issue 交流。

总结

这次重构让幂等组件从"能用"变成"好用",主要提升:

功能更完整:三种策略、Token防重放、结果缓存✅ 可靠性更高:Redisson分布式锁 + 看门狗续期✅ 可观测性:Prometheus监控,一切指标可查✅ 体验更好:重复请求返回缓存,用户无需等待

如果你也在找一个开箱即用的分布式幂等解决方案,不妨试试这个组件。


#Java #SpringBoot #分布式 #幂等 #开源 #ForgeAdmin #架构重构