我在开源项目重构了分布式幂等组件:支持三种策略、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_REQUIRED | Token验证优先 | 前端防重复提交、防重放攻击 |
2. 幂等结果缓存
当重复请求发生时,可以返回上次成功执行的缓存结果,无需重复执行业务逻辑:
- 提升用户体验(用户无需重新提交)
- 减少系统负载(避免重复计算)
- 缓存有效期可配置
3. Token机制防重放
提供完整的Token生成、验证、消费机制:
- Token单次消费,使用后即失效
- Token绑定用户ID,防止跨用户盗用
- 短TTL,减少泄露风险
- 提供REST API供前端调用
4. Redisson分布式锁
基于Redisson实现增强型分布式锁:
- 支持看门狗自动续期,长时间业务执行不会提前过期
- 可配置锁等待时间和租约时间
- 公平锁支持,避免饥饿
5. Prometheus监控集成
内置完整的监控指标:
| 指标 | 类型 | 说明 |
|---|---|---|
idempotent.requests.total | Counter | 请求总数 |
idempotent.requests.success | Counter | 成功次数 |
idempotent.requests.duplicate | Counter | 重复次数 |
idempotent.cache.returned | Counter | 缓存返回次数 |
idempotent.cache.hit.rate | Gauge | 缓存命中率 |
idempotent.execution.time | Timer | 执行耗时分布 |
快速开始
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:SpEL表达式解析没有异常处理
一开始直接解析,不捕获异常,如果用户写错了表达式,整个接口直接500。现在改成:解析失败回退到参数哈希方案,保证接口可用。
坑2:参数名获取方式兼容性问题
一开始用 StandardReflectionParameterNameDiscoverer,如果编译没有开 -parameters 参数,就获取不到参数名,SpEL中的 #参数名 就失效了。现在改成 DefaultParameterNameDiscoverer,它会自动尝试多种方式,兼容性更好。
坑3:全局开关动态不生效
一开始只在自动配置上加了 @ConditionalOnProperty,如果运行时通过配置中心关闭,切面还是会执行。现在在切面切入点再次检查配置开关,保证即时生效。
项目地址
完整代码已经开源在 Forge Admin:
欢迎 Star 关注,如果你对幂等组件有更好的想法,欢迎提 Issue 交流。
总结
这次重构让幂等组件从"能用"变成"好用",主要提升:
✅ 功能更完整:三种策略、Token防重放、结果缓存✅ 可靠性更高:Redisson分布式锁 + 看门狗续期✅ 可观测性:Prometheus监控,一切指标可查✅ 体验更好:重复请求返回缓存,用户无需等待
如果你也在找一个开箱即用的分布式幂等解决方案,不妨试试这个组件。
#Java #SpringBoot #分布式 #幂等 #开源 #ForgeAdmin #架构重构