JEECG-Boot 接口幂等性怎么落地:Token 机制与分布式锁两把钥匙,一次说清
【免费下载链接】jeecg-boot【低代码v2.0,一句话即可生成整个系统】企业级AI低代码平台,一键生成前后端代码甚至整个系统。 AI Skills 一句话画流程、设计表单、生成报表、大屏。内置 AI应用平台涵盖:AI聊天、知识库、流程编排、MCP插件等,兼容主流大模型。引领AI低代码「Skills 生成 → 在线配置 → 代码生成 → 手工合并->AI修改」开发模式,解决 Java 项目 90% 重复工作,提高效率又不失灵活。项目地址: https://gitcode.com/GitHub_Trending/je/jeecg-boot
先说个常见翻车现场:用户双击"提交订单",网络刚好慢一拍,同一个订单插了两次库;或者 MQ 把"扣款"消息重投了一次,钱扣了两遍。这就是接口幂等性要管的事——同一个请求打进来一次和打进来三次,产生的结果必须一样。JEECG-Boot 里针对这类问题,实际上准备了两种手段:认证侧的 Token 校验机制,和基于 Redisson 的分布式锁。这篇文章就带你把这两把钥匙分清楚,各自能解什么锁、怎么接进来。
一次双击和一次重投,是两件不同的事
新手最容易犯的错,是把"防重复"当成一个整体方案去找,其实它拆开后是两个问题:
问题一:同一个客户端把同一个动作发了多次。双击、表单连点、超时重试,来源都是"人"或"端"。你希望的是第二个请求根本别进来。
问题二:多个请求并发地改同一份数据。两个服务实例同时给库存 -1,谁先谁后没人管,数据就脏了。你希望的是同一时刻只有一个请求能碰它。
这两种问题混在一起谈,就会出现"我上了锁怎么还重复提交"这种扯不清的锅。下面分开看。
Token 机制:JEECG-Boot 里的 Token 到底管到哪一步
先泼一盆冷水:JEECG-Boot 的 Token 机制是登录态(JWT)的鉴权校验,不是业务级的"防重放 Token"。它的入口在 TokenUtils.verifyToken,核心逻辑就这几步:
public static boolean verifyToken(String token, CommonAPI commonApi, RedisUtil redisUtil) { if (StringUtils.isBlank(token)) { throw new JeecgBoot401Exception("token不能为空!"); } String username = JwtUtil.getUsername(token); if (username == null) { throw new JeecgBoot401Exception("token非法无效!"); } // ... 查用户、校验状态,再走 jwtTokenRefresh 校验/续期 return true; }注意jwtTokenRefresh这一步:Token 会存在 Redis 里,校验时如果快过期就续期,目的是让用户操作时不掉线。所以它的角色很明确——确认"你是谁",而不是确认"这个动作能不能再做一次"。
那"防重复提交"谁来兜底?老实说,前端按钮防抖 + 后端分布式锁才是 JEECG-Boot 生态里实际扛住重复提交的主力。如果你需要"一次性凭证"(先领 Token、提交即作废)那种严格幂等设计,需要自己在业务层加一层,平台没有现成注解。这点心里有数,省得照着网上文章找了一个不存在的 API。
分布式锁接入三步走:JEECG-Boot 的 Redisson 锁
后端并发这层,JEECG-Boot 给的是jeecg-boot-starter-lock(基于 Redisson),用法有注解式和编程式两种。仓库里现成的示例在 DemoLockTest.java,三步接入:
第一步,加依赖。在你的业务模块 pom 里引入:
<dependency> <groupId>org.jeecgframework.boot</groupId> <artifactId>jeecg-boot-starter-lock</artifactId> </dependency>第二步,配 Redis。在application-dev.yml里已有现成的一段:
jeecg: lock: #分布式锁配置 redisson: address: 127.0.0.1:6379 password: type: STANDALONE enabled: true第三步,用锁。注解式最简单,适合定时任务这类"同一时间只能跑一份"的场景:
@Scheduled(cron = "0/5 * * * * ?") @JLock(lockKey = CloudConstant.REDISSON_DEMO_LOCK_KEY1) public void execute() throws InterruptedException { // 每 5 秒触发一次,但同一时刻全集群只有一个实例在跑 }需要更细控制(比如抢不到锁就跳过,而不是排队)就用编程式,注入RedissonLockClient后:
if (redissonLock.tryLock(key, -1, expireSeconds)) { try { // 执行业务逻辑 } finally { redissonLock.unlock(key); } }这里tryLock第二个参数是等待时间(-1表示不等待),第三个是锁的持有时长。为什么必须带过期时间?因为持锁的进程可能崩掉,没有兜底的锁会把整个业务卡死。Redisson 内部有看门狗机制配合,但业务侧显式传expireSeconds仍然是好习惯。
两种方案怎么选:一张表看完
| 维度 | Token 机制(登录态校验) | 分布式锁(Redisson) |
|---|---|---|
| 解决的问题 | 请求是否来自合法在线用户 | 同一时刻谁允许碰这份数据 |
| 作用范围 | 全局鉴权,所有接口 | 锁粒度你定,按 key 隔离 |
| 挡得住"重复提交"吗 | 挡不住,它只管身份 | 能挡住并发重复执行 |
| 部署形态 | 单体、微服务通用 | 依赖 Redis,天然跨实例 |
| 典型失败姿势 | Token 过期用户被踢 | 锁没释放、锁粒度太粗 |
| 适用场景 | 所有接口的准入控制 | 库存扣减、订单号生成、定时任务互斥 |
一句话记忆:Token 管"谁能进",锁管"谁能先动"。防重复提交想彻底一点,还得在前端把按钮做防抖,后端用锁兜底,三层一起上。
容易踩的坑与落地建议
- 锁粒度别贪大。用全局一把大锁保护整个订单流程,等于把并发能力清零。key 里带上业务主键,比如
lock:order:{orderId},只锁该锁的那条数据。 - unlock 必须放进 finally。上面示例里那层
try/finally不是仪式感,业务抛异常时锁不释放,后续请求全部卡死。 - 等待时间和持锁时间是两回事。
tryLock(key, -1, expire)里第一个是"抢不到等多久",第二个是"抢到后最多拿多久"。新手经常把-1当成持锁时间,锁永远不自己放,直接生产事故。 - 别指望 Token 帮你幂等。再次强调,JEECG-Boot 的 Token 是登录态。如果你的场景是"消息重投要幂等",要么在消费端用锁 + 去重表,要么自己做一次性 Token,别把希望寄托在鉴权 Token 上。
- 先量再改。上锁前先确认瓶颈真的是并发冲突,而不是 SQL 慢。锁解决不了慢查询,反而把单线程慢放大成全员排队。
最后给个务实的落地顺序:前端防抖止血 → 后端@JLock或tryLock兜住并发 → 对账/去重表做最终一致性核对。三件套齐了,大部分"重复提交"和"重复扣款"的工单就再也不会来了。 🛠
【免费下载链接】jeecg-boot【低代码v2.0,一句话即可生成整个系统】企业级AI低代码平台,一键生成前后端代码甚至整个系统。 AI Skills 一句话画流程、设计表单、生成报表、大屏。内置 AI应用平台涵盖:AI聊天、知识库、流程编排、MCP插件等,兼容主流大模型。引领AI低代码「Skills 生成 → 在线配置 → 代码生成 → 手工合并->AI修改」开发模式,解决 Java 项目 90% 重复工作,提高效率又不失灵活。项目地址: https://gitcode.com/GitHub_Trending/je/jeecg-boot
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考