最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
如何利用Redis Lua脚本防止接口幂等性冲突_实现原子化的Token检查与设置
时间:2026-07-10 10:41:57 编辑:袖梨 来源:一聚教程网
必须用Lua脚本原子执行“判断+删除”,因GET与DEL分离会导致高并发下两次校验都通过而重复提交;脚本需用redis.call("get")判空后del并return 1/0,Java调用须用DefaultRedisScript封装且resultType设为Long.class。
直接用 GET + DEL 两步校验 Token,高并发下必然失效;必须用 Lua 脚本把「存在判断 + 删除」锁死在一次 Redis 执行中,否则重复提交拦不住。
为什么不能分开执行 GET 和 DEL
两个并发请求同时执行 GET token:abc,都拿到非空结果;接着都执行 DEL token:abc,再都走业务逻辑——订单就生成了两次。这不是理论风险,是线上真实会炸的场景。
-
GET和DEL是两个独立命令,Redis 不保证它们之间不被其他客户端插入操作 - 哪怕加了
EXPIRE,也不能解决中间窗口期的竞态 - 用
EXISTS判断后再DEL同样无效,本质一样是非原子
Lua 脚本必须这样写:用 redis.call("get") + del 原子返回
核心是让整个判断和清理只返回一个确定值(1 或 0),业务层靠这个值决定是否放行。下面这个脚本是经过生产验证的最小可靠形态:
if redis.call("get", KEYS[1]) ~= false then redis.call("del", KEYS[1]) return 1else return 0end
- 必须用
redis.call("get", ...),不能用redis.call("exists", ...),因为exists返回整数,而get返回 nil 或字符串,Lua 中~= false才能准确捕获 key 不存在的情况 - 脚本里不能出现
TIME、RANDOMKEY等非确定性命令,否则 Redis 主从同步会拒绝加载 - 不要在脚本里做业务逻辑(比如拼接日志、调用外部接口),它只负责“钥匙是否有效+立刻作废”这件事
SpringBoot 中调用 EVAL 的关键配置点
Java 层不是简单拼字符串发过去,得用 DefaultRedisScript 封装并指定返回类型,否则返回值解析会出错:
-
redisScript.setScriptText(...)必须赋值为上面那段 Lua 字符串 -
redisScript.setResultType(Long.class)—— 因为脚本最后return 1是数字,不是字符串,不设类型会导致ClassCastException - 调用时传参:
redisTemplate.execute(redisScript, Collections.singletonList(token)),KEYS[1]对应的就是这个token字符串 - 返回值为
1L表示校验通过且已删除;0L表示 token 不存在或已被用过
Token 存储本身也要防覆盖,用 SET NX EX 更稳妥
生成 Token 时别用 set(token, "1"),而是用原子命令确保不会意外覆盖:
- 推荐写法:
stringRedisTemplate.opsForValue().set(token, "1", 5, TimeUnit.MINUTES, RedisSetOption.UPSET) - 更底层可直接用:
redis.call("set", KEYS[1], ARGV[1], "EX", ARGV[2], "NX"),其中"NX"是关键,避免并发生成时写入冲突 - 过期时间别设太长(比如 24 小时),5~10 分钟足够覆盖用户操作窗口,过长会堆积无效 key
真正容易被忽略的是:Lua 脚本的返回值类型和 Java 调用时的泛型必须严格一致,差一个 Long 和 Integer 就会静默失败;还有就是前端拿 token 后没及时带出,或者后端没校验 header/param 是否为空,这些地方一漏,原子性再强也白搭。
相关文章
- 诛仙世界云若·梦影游仙新时装怎么获得 07-29
- 检疫区最后一站灭鼠者成就如何完成 07-29
- 蚂蚁森林神奇海洋2026年1月26日答案 07-29
- 三角洲行动长弓溪谷2.2日密码是多少 07-29
- html-anything 怎么安装?Codex/Claude Code 本地 HTML 编辑器教程 07-29
- Gardenin新滤镜成就如何解锁 07-29