Redis Lua 原子限流:Lua 脚本、竞态条件与并发控制

一、Lua 脚本的基本概念

Lua 是一种轻量级、可嵌入的脚本语言,支持变量、条件判断、循环、函数和表(Table)等基本编程能力。Lua 的重要特点是运行环境较小 ,能够方便地嵌入其他程序,由宿主系统提供特定功能接口。

Redis 内置了 Lua 解释器,允许开发者将多条 Redis 命令组织为一段脚本,在 Redis 服务端执行。Redis 官方将这种能力称为Redis Scripting,主要用于在数据存储位置执行自定义逻辑,减少多次客户端通信,并保证相关操作连续执行。

Redis 当前使用 Lua 5.1 解释器。脚本可以通过 EVAL 执行,也可以通过 SCRIPT LOAD 加载后使用 EVALSHA 调用。Redis 7.0 起还提供了 Redis Functions,用于管理可复用的服务端函数。

1.1 Redis Lua 脚本如何执行

Lua 脚本在 Redis 中运行时,可以使用 redis.call() 调用 Redis 命令。脚本通过 KEYS 接收需要访问的键名,通过 ARGV 接收普通业务参数。

例如:

复制代码
-- 获取需要操作的 Redis Key
local key = KEYS[1]

-- 执行一次计数增加
local count = redis.call("INCR", key)

-- 将更新后的计数返回客户端
return count

其中,local 用于声明 Lua 局部变量,redis.call() 负责调用 Redis 命令,return 将执行结果返回客户端。

Lua 脚本执行期间,Redis 保证该脚本不会与其他客户端命令交错执行。这种执行方式使开发者能够将多个具有依赖关系的操作组织在一起。


二、竞态条件与原子操作

竞态条件(Race Condition)是指多个执行单元并发访问或修改共享状态时,最终结果受到执行先后顺序影响的情况。 如果程序没有对关键操作进行适当控制,就可能产生错误的业务结果。

在限流系统中,通常需要读取当前请求次数、判断是否达到阈值,再决定是否增加计数。假设当前计数为 99,限制为 100 次,两个请求同时读取到 99,都认为自己可以通过。随后两个请求分别增加计数,最终可能使通过数量超过限制。

问题在于,这三个步骤分别执行时,其他请求可能在步骤之间插入。

这里需要理解原子性(Atomicity) :一个操作在并发观察者看来具有不可分割的执行效果。Redis 的 INCR 本身具有原子性,但如果业务规则需要同时完成读取、判断、增加计数和设置过期时间,那么单条 INCR 的原子性无法自动覆盖完整业务判断。

Redis Lua 可以将这些步骤放入同一次脚本执行,使它们在同一个 Redis 执行环境中连续完成,从而消除这些步骤之间的命令交错风险。


三、使用 Lua 实现固定窗口限流

固定窗口限流(Fixed Window Rate Limiting)是指在一个固定计数周期内,限制某个用户或资源允许执行的请求数量。可以利用 Redis Key 保存当前窗口的请求次数,并通过 TTL 控制窗口持续时间。

下面是一个简化的 Lua 实现:

Lua 复制代码
-- KEYS[1]:限流计数键
-- ARGV[1]:最大请求次数
-- ARGV[2]:窗口持续秒数

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])

local count = tonumber(redis.call("GET", key)) or 0

-- 已达到限制,直接拒绝
if count >= limit then
    return 0
end

-- 增加请求次数
count = redis.call("INCR", key)

-- 首次请求时设置过期时间
if count == 1 then
    redis.call("EXPIRE", key, window)
end

return 1

这段脚本将读取、判断、计数更新和首次过期时间设置组织在一起。返回 1 表示允许当前请求,返回 0 表示拒绝。Java 应用可以通过 Spring Data Redis 的脚本执行接口调用这段 Lua,无需分别向 Redis 发送多条业务命令。

这个实现使用首次允许请求作为计数周期的开始,因此属于以首次请求为起点的固定时长窗口。它能够满足基础限流需求,但窗口边界附近仍可能出现短时间集中请求的问题。对流量平滑程度要求较高时,可以进一步考虑滑动窗口或令牌桶算法。


四、Lua 原子性的保证与局限

Redis Lua 的原子执行意味着其他客户端无法在脚本运行过程中插入普通命令,但它不等同于数据库事务的自动回滚机制 。如果脚本执行到一半发生错误,之前已经成功写入 Redis 的修改并不保证自动撤销。因此,应提前检查参数,并避免在脚本中放入复杂、容易失败的操作。

Lua 脚本还会占用 Redis 的执行资源。脚本运行时间过长可能阻塞其他请求,因此适合执行简短的状态读取、判断和更新逻辑。对于 Redis Cluster,脚本涉及的多个键还需要符合相应的哈希槽约束。

实际限流还涉及限流对象选择、客户端身份识别和 Redis 故障处理。如果 Redis 不可用,应用需要根据业务风险决定临时放行、拒绝请求或采用其他降级策略。


五、面试理解与表达

Lua 是一种轻量级脚本语言,Redis 支持在服务端执行 Lua 脚本。它的主要价值是将多条具有依赖关系的 Redis 操作组合成一次原子执行。

普通限流如果分别完成读取计数、判断阈值和增加计数,可能在并发请求下出现竞态条件。Lua 可以保证这些步骤在执行期间不会被其他客户端命令插入,从而提高限流判断的正确性。但还需要考虑窗口算法本身、Redis 故障及脚本执行时间等问题。

参考资料: Redis 官方《Scripting with Lua》《Redis Programmability》。

相关推荐
杨凯凡34 分钟前
MySQL 死锁怎么排查?InnoDB 锁等待与 Spring Boot 整事务重试实战
数据库·spring boot·mysql
Aloudata34 分钟前
Claude 做数据分析的这两年:一份复盘与发展线索
大数据·数据库·人工智能·数据挖掘·数据分析·dataagent
Decay__1 小时前
01_SQLServer 体系架构全解 —— 实例进程、关系引擎、SQLOS 与事务日志深入
运维·数据库·sql·mysql·架构
zhanghaha13141 小时前
C# 系列教程_12 字符串 string 保姆级系统教程(面向新手,大量示例 + 原理讲解)
开发语言·数据库·c#
韩曙亮1 小时前
【错误记录】安卓 adb 调试 5037 端口占用排查 ( adb 的三层结构 | 找出本机占用 5037 的进程 )
android·数据库·adb·安卓·错误记录
瑞雪兆丰年兮1 小时前
JavaWeb-day07-MySQL(DQL&多表设计)
java·开发语言·数据库·sql·mysql·javaweb·web
IT规划师1 小时前
Redis 数据类型全解 - Stream 深度:Pending 回收、重试死信与消息队列选型(十一)
redis·.net
lsc_blog6 小时前
年终自评别贴提交记录,把技术产出讲成变化
数据库·面试·职场和发展·跳槽·求职招聘