商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 如何利用 Redis 的 Lua 脚本配合 Java 业务逻辑实现强一致性的分布式限流

如何利用 Redis 的 Lua 脚本配合 Java 业务逻辑实现强一致性的分布式限流

  发布于2026-07-11 阅读(0)

扫一扫,手机访问

分布式限流这件事,技术方案看起来不少,但要真正实现“强一致性”,路其实挺窄的。Redis 配合 Lua 脚本是目前比较成熟的解法,但这里面有个关键前提——限流的全部判断逻辑,必须老老实实缩在 Lua 脚本里完成,Ja va 层只负责“安全调用”,不要掺和任何状态判断。

问题的核心就几句话:Redis + Lua 脚本本身具备原子性,能保证强一致;而 Ja va 嘛,离了脚本,任何“先查后判再写”的操作都会破坏原子性,漏放或误拒只是时间问题。

为什么不能在 Ja va 里做 if-else 判断是否超限

实践中经常能看到这样的场景:Ja va 先 stringRedisTemplate.opsForValue().get() 拿到当前计数,接着 if (count >= max) throw new RuntimeException() 做个判断,最后才 incr() 加一。这三步之间,竞态窗口敞开着,多个请求可能同时读到旧值、同时判定未超限、同时写入,结果就是实际 QPS 轻松翻倍。

差别在哪里?

  • Redis 单线程执行 Lua,一个脚本里所有 redis.call() 天然是原子的,不会被打断。
  • Ja va 层的网络往返、本地计算、条件分支全在 Redis 外部,天然非原子。
  • 哪怕是 pipeline 或者事务(multi/exec),也只保证命令序列不被穿插,却无法保证“读-算-写”这个逻辑在并发下不被干扰。

简单说,Lua 才是集装箱,Ja va 只该是运货的卡车司机,不用替集装箱验货。

EVALEVALSHA 的选择要点

到了生产环境,关于脚本调用方式,有个很实际的建议:用 EVALSHA,别每次都传完整 Lua 长串。

  • EVAL 每次发送整段脚本,网络包体积不小,尤其脚本里塞了注释和空格的时候更加明显。
  • Redis 会缓存已经执行过的 Lua 脚本 SHA1 值,EVALSHA 只传 40 个字符的哈希,带宽和解析开销都降下来了。
  • 不过第一次调用前,需要先 SCRIPT LOAD 注册一下脚本,否则 EVALSHA 返回 NOSCRIPT 错误——这个错误容易被忽略,可一旦忽略,限流就形同虚设。
  • Spring Data Redis 的 RedisTemplate.execute(RedisScript, …) 默认走 EVALSHA,但它要求脚本对象已经预热,也就是要提前执行一次 loadScript()

总结下来就是:EVALSHA 省带宽、省解析,但预热这件事千万别省。

令牌桶 Lua 脚本中几个关键参数含义

拿最常见的令牌桶限流来说,KEYS[1] 自然就是限流 key,比如 "rate:uid:123"。而 ARGV 通常按这样几个参数传进去:

  • ARGV[1]:当前毫秒时间戳,用 System.currentTimeMillis() 传进去,用来计算这段时间内生成了多少令牌。
  • ARGV[2]:桶的容量 max_permits,比如 100。
  • ARGV[3]:令牌生成速率 rate,单位是“个/秒”,比如 50 就表示每 20 毫秒补一个。
  • ARGV[4]:本次申请的令牌数,通常为 1。虽然支持批量获取,但大多数场景固定传 1 就够。

这里有个容易被忽略的细节:有人喜欢用 redis.call('TIME') 在 Redis 内部获取时间,但返回的是秒 + 微秒。且不说时区、精度的问题,它和业务系统的时钟很难对齐,时间漂移会直接干扰限流的准确度。所以更推荐的做法是,让客户端(Ja va 端)把时间戳作为参数传进去。

Ja va 调用时最容易被忽略的细节

脚本写对了,这还只是第一步。Ja va 层配置上稍有不慎,强一致性照样会崩溃。

  • StringRedisTemplate 的序列化器必须明确设为 StringRedisTemplate.setDefaultSerializer(new StringRedisSerializer()),不然传进去的 KEYSARGV 可能变成乱码,Lua 里 tonumber(ARGV[1]) 返回 nil,限流直接就垮了。
  • Lua 脚本的返回值通常是 Long 类型,比如 1 表示通过,0 表示拒绝。Ja va 接收时泛型必须用 Long.class,要是图省事用了 Integer.classClassCastException 就会找上门。
  • Redis 连接池的配置也至关重要。max-active 设得太小,会导致 EVAL 请求排队,表面上看起来限流在生效,其实是连接池成了新瓶颈。timeout 设得太短,正常脚本执行到一半就被中断,直接抛 RedisCommandTimeoutException
  • 另外,不要在 Lua 脚本里写 redis.log()。虽然它不阻塞,但高频打日志会严重拖慢 Redis 主线程,而且日志内容也传不回 Ja va,调试时反而帮倒忙。

说来也怪,写出正确的 redis.call('zadd', ...) 并不难,真正的难点在于让整个链路——从 Ja va 时钟、序列化、连接池,到 Redis 的 script cache 和 clock drift——全都对齐在同一个语义下。稍微偏差一点,“强一致”就真的只剩一个名字了。

本文转载于:https://www.php.cn/faq/2386099.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注