当前位置: 首页 > news >正文

通用幂等与防重就该这么实现!SpringBoot + Redis 打造一个生产级中间件

GitHub:https://github.com/songrongzhen/OnceKit

技术栈:Spring Boot 3.0 + JDK 17 + Spring AOP + Redis + Lua +SpEL

目标:开箱即用、生产就绪、注解驱动、支持高并发防重场景

一、为什么要做这个中间件?

1.1 痛点场景
  • 用户点击“提交订单”按钮多次→ 生成多笔订单

  • 网络超时重试→ 后端重复处理支付回调

  • MQ 消息重复投递→ 账户余额被多次扣减

  • 考生重复提交报名信息→ 数据库出现多条相同身份证记录

这些都违反了 幂等性(Idempotency)原则:同一操作无论执行多少次,结果应一致。

1.2 现有方案的问题

方案

缺点

数据库唯一索引

仅适用于写入场景,无法防“并发穿透”

前端按钮禁用

不可靠(可绕过)

Token 机制

需前后端配合,增加复杂度

手动写 Redis

重复代码多,维护成本高

于是,我决定:用 AOP + 注解 + Redis,打造一个通用、轻量、高性能的幂等中间件。

二、整体架构设计

2.1 系统架构图

整个过程在毫秒级完成,且无数据库压力

2.2 核心组件

组件

职责

@Idempotent

自定义注解,声明幂等规则

IdempotentAspect

AOP 切面,拦截带注解的方法

SpelKeyGenerator

使用 Spring SpEL 动态生成唯一 Key

RedisIdempotentStore

基于 Redis 实现原子校验

IdempotentFailureHandler

自定义重复请求处理策略

三、核心代码实现

3.1 注解定义
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Idempotent { String key(); int expire() default 300; String value() default "1"; }
3.2 AOP 切面逻辑
@Aspect publicclass IdempotentAspect { privatefinal IdempotentService idempotentService; privatefinal ExpressionParser parser = new SpelExpressionParser(); privatefinal StandardReflectionParameterNameDiscoverer discoverer = new StandardReflectionParameterNameDiscoverer(); public IdempotentAspect(IdempotentService idempotentService) { this.idempotentService = idempotentService; } @Around("@annotation(idempotent)") public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String[] paramNames = discoverer.getParameterNames(signature.getMethod()); Object[] args = joinPoint.getArgs(); // 解析 SpEL key StandardEvaluationContext context = new StandardEvaluationContext(); for (int i = 0; i < args.length; i++) { context.setVariable(paramNames[i], args[i]); } String key = parser.parseExpression(idempotent.key()).getValue(context, String.class); if (!idempotentService.tryLock(key, idempotent.expire())) { if (idempotent.mode() == Idempotent.Mode.REJECT) { thrownew IllegalStateException("重复请求,请勿重复提交"); } // TODO: RETURN_CACHE 模式(需结果缓存) } return joinPoint.proceed(); } }
3.3 自定义失败处理器(可扩展)
public interface IdempotentFailureHandler { void handle(String key, Method method); } @Component public class DefaultIdempotentFailureHandler implements IdempotentFailureHandler { @Override public void handle(String key, Method method) { // 默认什么都不做,由 AOP 抛出异常 } }

四、使用案例

案例 1:下单(防重复下单)
@PostMapping("/order") @Idempotent(key = "'order:' + #userId + ':' + #goodsId", expire = 300) public Result<String> createOrder( @RequestParam String userId, @RequestParam String goodsId) { // 模拟下单逻辑 orderService.create(userId, goodsId); return Result.success("下单成功"); }

若同一用户对同一商品在 5 分钟内重复下单,后续请求将被拒绝。

案例 2:考生报名(防身份证重复)
@PostMapping("/enroll") @Idempotent(key = "'enroll:' + #candidate.idCard", expire = 300) public Result<Void> enroll(@RequestBody Candidate candidate) { // 防止同一身份证重复报名 enrollmentService.save(candidate); return Result.OK(); } // 简写一个dto类吧 publicclass Candidate { private String name; private String idCard; private String phone; }

key 为enroll:11010119900307XXXX,5分钟内无法重复提交。

案例 3:秒杀场景(用户 + 商品维度)
@PostMapping("/seckill") @Idempotent(key = "'seckill:' + #userId + ':' + #goodsId", expire = 60) public Result<String> seckill(@RequestParam String userId, @RequestParam Long goodsId) { return seckillService.execute(userId, goodsId); }

即使用户疯狂点击,1 分钟内只允许一次有效请求。

五、性能与可靠性

  • 性能:Redis SET NX EX 是原子操作,单节点 QPS > 5w+

  • 一致性:基于 Redis 分布式锁语义,天然支持集群

  • 安全性:Key 由业务生成,无注入风险(SpEL 在受控上下文中执行)

  • 资源:Key 自动过期,无内存泄漏风险

工具代码已经完整的放到GitHub上,使用超级简单,你的项目中引用依赖

<!-- https://mvnrepository.com/artifact/io.github.songrongzhen/once-kit-spring-boot-starter --> <dependency> <groupId>io.github.songrongzhen</groupId> <artifactId>once-kit-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>

然后在你的需要幂等和防止重复提交的接口上加上一行注解就OK

@Idempotent(key = "'order:' + #userId + ':' + #goodsId", expire = 300)
http://www.jsqmd.com/news/367429/

相关文章:

  • AI应用架构师进阶:容量规划中的GPU虚拟化技术与资源调度
  • Hadoop与社交网络:关系图谱挖掘技术
  • 数字人开发避坑指南:lite-avatar形象库常见问题解答
  • Http接口对接太繁琐?试试UniHttp框架吧!简简单单~
  • AI应用架构师的技术创新:企业AI平台架构设计的新动力
  • 大数据诊断性分析全攻略:工具、方法与最佳实践
  • Yi-Coder-1.5B应用:Ollama部署+52种编程语言支持
  • 为什么Java里面,Service 层不直接返回 Result 对象?
  • StructBERT中文情感分类:社交媒体情绪监控实战
  • 2026网络安全终局之战:AI失控、量子降维、监管围剿,企业只剩一条生路
  • Z-Score归一化
  • 还在手动搭Maven多模块?这款IDEA插件让我效率提升10倍(真实体验)
  • 从规则到智能:Web漏洞扫描技术的演进、范式革命与未来防御图景
  • 一套万能通用的异步处理方案
  • AT_agc074_b [AGC074B] Swap if Equal Length and Sum
  • Fish-Speech 1.5快速入门:从安装到生成第一段语音
  • 别再用旧版了!OpenClaw 2026.2.9 更新迁移避坑指南
  • 如何用 Skill Creator,把一个真实项目拆成一整套 Agent Skills 的(MVP 实战)
  • SpringBoot 实现动态切换数据源,这样做才更优雅!
  • 2026贵阳二手房急售方案大比拼:在贵阳如何能快速卖房 - 精选优质企业推荐榜
  • Python核心语法-Anconda和jupyter - 努力-
  • 如何在Nginx 中实现动态封禁IP
  • Shiro代码审计 - 絮行
  • AI大模型-机器学习 - 努力-
  • 字节一面:POST 为什么会发送两次请求?
  • 2026年2月淄博新员工拓展公司推荐,助力新人快速融入团队 - 品牌鉴赏师
  • Jimeng LoRA保姆级教程:文件夹自动扫描+safetensors识别+自然排序配置
  • 如果在main主分支更改了代码,但是有权限不能上传怎么办?
  • 一键部署Jimeng LoRA:轻量文生图测试系统实战
  • 什么是机器学习?—— 用 “买西瓜” 讲透核心逻辑