3. Lua 脚本基础语法:变量、数据类型、条件判断、循环、函数定义
在深入限流与缓存保护实战之前,必须掌握 Lua 的核心语法。Lua 是一种轻量、高效、可嵌入的脚本语言,其语法简洁但功能强大。本章将聚焦于 Redis 环境中 Lua 脚本最常用的语法结构,确保你能流畅地编写和阅读限流脚本。
3.1 变量与数据类型
Lua 是动态类型语言,变量无需声明类型即可直接赋值。在 Redis 脚本中,变量默认是全局的,除非使用 local 关键字显式声明为局部变量。强烈建议在函数内部和脚本主体中使用 local 变量,以避免命名冲突和性能损耗。
| 数据类型 | 说明 | 示例 |
|---|---|---|
| nil | 空值,表示“无” | local a = nil |
| boolean | 布尔值 | local flag = true |
| number | 双精度浮点数(整数也视为 number) | local count = 100 |
| string | 字符串,支持单引号或双引号 | local key = "rate:limit:user" |
| table | 表,Lua 唯一的复合数据结构(可作数组/字典) | local arr = {1, 2, 3} |
| function | 函数,一等公民 | local f = function() end |
避坑提示: Lua 的索引从 1 开始,而非 0。在遍历数组时务必注意边界。
-- 变量声明示例
local name = "redis_limiter"
local max_requests = 10
local current = redis.call("GET", name) -- 调用 Redis 命令
if current == false then
current = 0
end
3.2 条件判断
Lua 使用 if-then-elseif-else-end 结构。条件表达式中的 false 和 nil 视为假,其余所有值(包括 0 和空字符串)视为真。这一点与许多语言不同,需特别注意。
local count = redis.call("GET", "counter")
if count == false then
-- key 不存在
redis.call("SET", "counter", 1)
elseif tonumber(count) > 100 then
-- 超过阈值,触发限流
return 0
else
-- 正常递增
redis.call("INCR", "counter")
end
常见错误: 不要使用
if count == 0 then 来判断 key 是否存在,因为 Redis 返回的 false 表示 key 不存在,而 0 是一个有效数值。应使用 if count == false then。
3.3 循环结构
Lua 提供 while、repeat-until 和 for 三种循环。在限流脚本中,for 循环最常用,用于遍历时间窗口内的记录或批量操作。
| 循环类型 | 语法 | 典型场景 |
|---|---|---|
| 数值 for | for i=start, end, step do ... end |
遍历固定次数的操作 |
| 泛型 for | for k, v in pairs(t) do ... end |
遍历 table 的所有键值对 |
| while | while condition do ... end |
条件控制的循环 |
-- 数值 for:清理过期的时间戳记录
local now = redis.call("TIME")[1]
local window = 60 -- 60 秒窗口
for i = #timestamps, 1, -1 do
if timestamps[i] < now - window then
table.remove(timestamps, i)
end
end
-- 泛型 for:遍历哈希表
local keys = redis.call("KEYS", "rate:*")
for _, key in ipairs(keys) do
local ttl = redis.call("TTL", key)
if ttl < 0 then
redis.call("DEL", key)
end
end
性能建议: 在 Redis Lua 脚本中,尽量避免使用
KEYS 命令进行大规模遍历,它会阻塞 Redis 主线程。推荐使用 SCAN 或基于有序集合的范围查询。
3.4 函数定义
函数是 Lua 脚本的核心组织单元。在限流脚本中,通常将核心逻辑封装为函数,便于复用和测试。函数支持多返回值,这在处理 Redis 返回结果时非常有用。
-- 定义一个简单的限流检查函数
local function check_rate_limit(key, max_requests, window_seconds)
local current = redis.call("GET", key)
if current == false then
redis.call("SET", key, 1, "EX", window_seconds)
return 1, max_requests - 1
end
local count = tonumber(current)
if count >= max_requests then
return 0, 0 -- 被限流
end
redis.call("INCR", key)
return 1, max_requests - count - 1
end
-- 调用函数
local allowed, remaining = check_rate_limit("user:123:requests", 10, 60)
if allowed == 1 then
-- 允许请求
else
-- 返回限流响应
end
函数最佳实践:
- 始终使用
local定义函数,避免全局污染。 - 函数名应体现其行为,如
check_rate_limit、acquire_slot。 - 利用多返回值传递状态和剩余配额,减少额外查询。
3.5 综合示例:滑动窗口限流脚本
将上述语法知识整合为一个完整的滑动窗口限流脚本,该脚本使用有序集合(Sorted Set)记录请求时间戳,并利用 Lua 的原子性保证并发安全。
-- 滑动窗口限流脚本
-- KEYS[1]: 限流 key
-- ARGV[1]: 窗口大小(秒)
-- ARGV[2]: 最大请求数
-- ARGV[3]: 当前时间戳(毫秒)
local key = KEYS[1]
local window = tonumber(ARGV[1])
local max_requests = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local window_start = now - window * 1000
-- 移除窗口外的过期记录
redis.call("ZREMRANGEBYSCORE", key, 0, window_start)
-- 获取当前窗口内的请求数
local current_count = redis.call("ZCARD", key)
if current_count >= max_requests then
-- 超过限流阈值,返回剩余配额为 0
return {0, max_requests, redis.call("TTL", key)}
end
-- 添加当前请求时间戳
redis.call("ZADD", key, now, now .. ":" .. math.random())
redis.call("EXPIRE", key, window * 2) -- 设置过期时间,防止内存泄漏
-- 返回允许通过、当前窗口请求数、剩余配额
return {1, current_count + 1, max_requests - current_count - 1}
脚本执行说明: 该脚本通过
redis.call() 与 Redis 交互,利用 Lua 的原子性确保整个限流逻辑在单次执行中完成,无需担心并发竞态条件。这是 Lua 脚本在 Redis 中的核心优势。
3.6 常见错误与调试技巧
| 错误类型 | 错误示例 | 正确写法 |
|---|---|---|
| 索引从 0 开始 | for i=0, #arr do |
for i=1, #arr do |
| 误用全局变量 | count = 10 |
local count = 10 |
| 字符串拼接使用 + | "key" + 123 |
"key" .. 123 |
| 比较时忽略类型 | if count == false then |
if count == false then(正确,但注意 Redis 返回 false 表示 key 不存在) |
| 忘记转换类型 | if count > 10 then |
if tonumber(count) > 10 then |
调试建议: 在 Redis 中调试 Lua 脚本时,可以使用
redis.log(redis.LOG_WARNING, message) 输出日志到 Redis 日志文件。避免在脚本中使用 print(),因为它不会输出到标准日志。