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。
  • 利用多返回值传递状态和剩余配额,减少额外查询。
Lua 脚本语法结构图 Lua 脚本语法结构图 Lua 脚本 变量与数据类型 条件判断 循环结构 函数定义 local 声明 nil / boolean number / string table if-then elseif-else false / nil 判断 数值 for 泛型 for while / repeat local function 多返回值 参数传递 核心要点 • 始终使用 local 声明变量和函数,避免全局污染 • 注意 Lua 索引从 1 开始,条件判断中 false 和 nil 为假

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(),因为它不会输出到标准日志。