Redis 与 Lua 脚本(Part 2)
这篇是 Redis 学习笔记 Part 2,重点放在 Lua 脚本能力和在 Redis/OpenResty 场景中的实际使用。
Lua 基础快速梳理
参考文档:The Programming Language Lua
最近需要阅读和编写一些 Lua 脚本,先把最关键的语法点集中整理一遍。

1. 变量与作用域
Lua 默认变量是全局变量,只有显式写 local 才是局部变量。
2. 数据类型
Lua 共有 8 种类型:
nil:空值。给变量赋nil可以理解为删除该键。boolean:true/false。number:Lua 5.1 中是双精度浮点数。string:不可变字符串。function:函数是一等公民。userdata:保存 C 数据结构。thread:协程。table:Lua 唯一的复合数据结构。
Lua 的核心是 table。它既能当数组,也能当字典。
数组示例(注意索引从 1 开始):
-- 隐式索引local list = { "Misc", "Web", "Pwn" }
-- 显式索引local list2 = { [1] = "Misc", [2] = "Web", [3] = "Pwn"}
print(list[1]) -- Miscprint(list[0]) -- nil长度获取:
local list = { [1] = "Chinese", [2] = "English", [3] = "Math"}
print(#list) -- 3字典示例:
local user = { name = "bx", -- 等价于 ["name"] = "bx" ["age"] = 18, [10001] = "id"}
print(user.name)print(user["name"])print(user["age"])print(user[10001])遍历时常用 ipairs 和 pairs:
| 函数 | 适用对象 | 顺序 | 遇到 nil 的行为 |
|---|---|---|---|
ipairs | 连续数字索引数组 | 从 1 递增 | 遇到第一个 nil 停止 |
pairs | 全部键值对 | 不保证顺序 | 不因 nil 提前停止 |
ipairs 示例:
local t = {10, 20, 30}
for i, v in ipairs(t) do print(i, v)endpairs 示例:
local t = { name = "bx", age = 18}
for k, v in pairs(t) do print(k, v)end3. 其他常用点
- 字符串拼接使用
..,不是+。
local key_prefix = "admin:"local id = 1001
local key = key_prefix .. idprint("Key:", key)
local message = string.format("User ID: %d", id)print("Message:", message)- Lua 没有
switch,只有if / elseif / else。
local score = 0
-- 在 Lua 里,0 也是真值if score then print("0 在 Lua 中为 true")end
if score > 90 then print("A")elseif score > 60 then print("B")else print("C")endLua 真值规则:只有 false 和 nil 为假,其他值(包括 0、""、{})都为真。
- 元表(Metatable)
元表可以改写 table 的默认行为,常见元方法有:
__index:访问不存在字段时触发。__newindex:给不存在字段赋值时触发。__add/__sub:运算符重载。__call:让table像函数一样被调用。__tostring:自定义打印输出。
Redis 中的 Lua 脚本
1. 原子性
在 Redis 里执行 Lua 脚本时,Redis 会把整段脚本作为一个原子操作执行。脚本运行期间不会插入其他命令,因此非常适合实现“读-改-写”一体化逻辑。
2. redis.call 与 redis.pcall
| 方法 | 出错时行为 |
|---|---|
redis.call | 抛异常并终止脚本 |
redis.pcall | 返回错误对象,不直接中断 |
3. EVAL 与 numkeys
Python 端常见调用方式:
r.eval(script, numkeys, *keys_and_args)参数拆分规则:
r.eval(script, numkeys, key1, key2, ..., arg1, arg2, ...)numkeys 的意义是告诉 Redis:后续参数中前多少个属于 Key。
- 前
numkeys个参数进入KEYS[] - 剩余参数进入
ARGV[]
所以写脚本时要明确区分 KEYS[] 与 ARGV[]。
4. 固定窗口限流示例
规则:用户 user:1001 在 10 秒内最多请求 3 次。
import redisimport time
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def example_rate_limiter(): user_id = "user:1001" limit = 3 window_seconds = 10
rate_limit_script = """ local key = KEYS[1] local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2])
local current = redis.call("INCR", key)
if tonumber(current) == 1 then redis.call("EXPIRE", key, window) end
if tonumber(current) > limit then return 0 -- Blocked else return 1 -- Allowed end """
print(f"Simulating requests for {user_id} (limit={limit}, window={window_seconds}s)")
for i in range(1, 6): allowed = r.eval(rate_limit_script, 1, f"ratelimit:{user_id}", limit, window_seconds) status = "Allowed" if allowed == 1 else "Blocked" print(f"Request {i}: {status}") time.sleep(0.5)
if __name__ == "__main__": example_rate_limiter()输出示例:
🔍Simulating requests for user:1001 (limit=3, window=10s)
Request 1: Allowed
Request 2: Allowed
Request 3: Allowed
Request 4: Blocked
Request 5: Blocked
OpenResty 与 Lua
OpenResty 本质是 NGINX + LuaJIT。
它把 Lua 脚本能力嵌入 NGINX 请求处理链路,让我们可以在网关层做鉴权、限流、路由和安全逻辑。

小结
Lua 本身不复杂,但 table、真值规则、元表这三部分很关键。放到 Redis 场景后,核心是理解原子执行模型、参数分离(KEYS/ARGV)和错误处理方式(call/pcall)。
下一步可以继续扩展:滑动窗口限流、令牌桶限流,以及在 OpenResty 中落地成网关策略。