Lua 脚本编程核心
Redis 中的 Lua 脚本编程完全指南
1. Lua 语言基础
1.1 Lua 简介
- 轻量级脚本语言:小巧、快速
- 嵌入式语言:易于嵌入其他程序
- 动态类型:变量无需声明类型
- 自动垃圾回收:自动内存管理
1.2 基本语法
变量和数据类型
-- 注释:单行注释
--[[
多行注释
]]
-- 变量声明(默认全局变量)
name = "Redis"
age = 10
-- 局部变量(推荐)
local count = 0
local flag = true
-- 数据类型
-- 1. nil(空值)
local x = nil
-- 2. boolean(布尔)
local isActive = true
local isDeleted = false
-- 3. number(数字)
local integer = 42
local float = 3.14
-- 4. string(字符串)
local str1 = "hello"
local str2 = 'world'
local str3 = [[多行
字符串]]
-- 5. table(表,唯一的数据结构)
local arr = {1, 2, 3} -- 数组(索引从 1 开始)
local dict = {name="Alice", age=25} -- 字典
local mixed = {1, 2, key="value"} -- 混合
-- 6. function(函数)
local function add(a, b)
return a + b
end
运算符
-- 算术运算符
local a = 10 + 5 -- 加
local b = 10 - 5 -- 减
local c = 10 * 5 -- 乘
local d = 10 / 5 -- 除
local e = 10 % 3 -- 取模
local f = 2 ^ 3 -- 幂运算
-- 关系运算符
local x = 10 == 5 -- 等于
local y = 10 ~= 5 -- 不等于
local z = 10 > 5 -- 大于
local w = 10 < 5 -- 小于
local v = 10 >= 5 -- 大于等于
local u = 10 <= 5 -- 小于等于
-- 逻辑运算符
local p = true and false -- 与
local q = true or false -- 或
local r = not true -- 非
-- 字符串连接
local str = "Hello" .. " " .. "World"
控制结构
-- if-else
if condition then
-- 代码
elseif another_condition then
-- 代码
else
-- 代码
end
-- while 循环
local i = 1
while i <= 10 do
print(i)
i = i + 1
end
-- repeat-until 循环
local j = 1
repeat
print(j)
j = j + 1
until j > 10
-- for 循环(数值)
for i = 1, 10 do
print(i)
end
for i = 1, 10, 2 do -- 步长为 2
print(i)
end
-- for 循环(迭代器)
local arr = {10, 20, 30}
for index, value in ipairs(arr) do
print(index, value)
end
local dict = {name="Alice", age=25}
for key, value in pairs(dict) do
print(key, value)
end
函数
-- 函数定义
function add(a, b)
return a + b
end
-- 局部函数
local function multiply(a, b)
return a * b
end
-- 多返回值
function divide(a, b)
if b == 0 then
return nil, "division by zero"
end
return a / b, nil
end
local result, err = divide(10, 0)
if err then
print(err)
end
-- 可变参数
function sum(...)
local total = 0
for _, v in ipairs({...}) do
total = total + v
end
return total
end
print(sum(1, 2, 3, 4, 5)) -- 15
Table(表)
-- 创建表
local t = {}
-- 数组操作(索引从 1 开始)
local arr = {10, 20, 30}
print(arr[1]) -- 10
arr[4] = 40
-- 字典操作
local dict = {}
dict.name = "Alice"
dict["age"] = 25
print(dict.name) -- Alice
print(dict["age"]) -- 25
-- 遍历数组
for i, v in ipairs(arr) do
print(i, v)
end
-- 遍历字典
for k, v in pairs(dict) do
print(k, v)
end
-- 表的长度
print(#arr) -- 数组长度
-- 表操作函数
table.insert(arr, 50) -- 插入
table.remove(arr, 1) -- 删除
table.concat(arr, ",") -- 连接
table.sort(arr) -- 排序
字符串操作
-- 字符串长度
local str = "hello"
print(#str) -- 5
print(string.len(str)) -- 5
-- 字符串连接
local result = "hello" .. " " .. "world"
-- 字符串查找
local pos = string.find("hello world", "world") -- 7
-- 字符串替换
local new_str = string.gsub("hello world", "world", "Lua")
-- 字符串截取
local sub = string.sub("hello world", 1, 5) -- hello
-- 大小写转换
print(string.upper("hello")) -- HELLO
print(string.lower("HELLO")) -- hello
-- 字符串格式化
local formatted = string.format("Name: %s, Age: %d", "Alice", 25)
2. Redis 中的 Lua
2.1 为什么使用 Lua?
- 原子性:Lua 脚本作为一个整体执行,不会被其他命令打断
- 减少网络往返:复杂逻辑在服务器端执行
- 可复用:脚本可以被缓存和复用
- 灵活性:实现复杂的业务逻辑
2.2 EVAL 命令
EVAL script numkeys key [key ...] arg [arg ...]
# script: Lua 脚本
# numkeys: key 的数量
# key: Redis 键名(通过 KEYS[1], KEYS[2] 访问)
# arg: 参数(通过 ARGV[1], ARGV[2] 访问)
示例
# 简单示例:设置并返回值
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey "myvalue"
# 获取并递增
EVAL "local val = redis.call('GET', KEYS[1]); return val" 1 mykey
# 条件判断
EVAL "
local current = redis.call('GET', KEYS[1])
if tonumber(current) > tonumber(ARGV[1]) then
return 1
else
return 0
end
" 1 counter 100
2.3 Redis API
-- 调用 Redis 命令
redis.call('SET', 'key', 'value')
redis.pcall('SET', 'key', 'value') -- 错误不会中断
-- call vs pcall
-- call: 命令错误会中断脚本
-- pcall: 命令错误返回错误对象,脚本继续
-- 示例
local result = redis.call('GET', 'key')
if result then
return result
else
return "key not found"
end
-- 返回值转换
-- Redis 命令返回值自动转换为 Lua 类型
-- Lua 返回值自动转换为 Redis 协议
2.4 EVALSHA(脚本缓存)
# 1. 加载脚本,返回 SHA1 值
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# 返回:5332031c6b470dc5a0dd9b4bf2030dea6d65de91
# 2. 使用 SHA1 执行脚本
EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 1 mykey
# 3. 检查脚本是否存在
SCRIPT EXISTS sha1 [sha1 ...]
# 4. 清空脚本缓存
SCRIPT FLUSH
# 5. 杀死正在执行的脚本
SCRIPT KILL
3. 实战案例
3.1 分布式锁
加锁脚本
-- lock.lua
local key = KEYS[1] -- 锁的键名
local value = ARGV[1] -- 锁的值(唯一标识)
local ttl = tonumber(ARGV[2]) -- 过期时间
-- SET NX EX
local result = redis.call('SET', key, value, 'NX', 'EX', ttl)
if result then
return 1 -- 加锁成功
else
return 0 -- 加锁失败
end
解锁脚本
-- unlock.lua
local key = KEYS[1]
local value = ARGV[1]
-- 检查锁是否是自己的
local current = redis.call('GET', key)
if current == value then
redis.call('DEL', key)
return 1 -- 解锁成功
else
return 0 -- 不是自己的锁
end
使用示例
# 加锁
EVAL "local key = KEYS[1]; local value = ARGV[1]; local ttl = tonumber(ARGV[2]); return redis.call('SET', key, value, 'NX', 'EX', ttl) and 1 or 0" 1 lock:order:1 uuid123 10
# 解锁
EVAL "local key = KEYS[1]; local value = ARGV[1]; local current = redis.call('GET', key); if current == value then redis.call('DEL', key); return 1 else return 0 end" 1 lock:order:1 uuid123
3.2 限流(令牌桶)
-- rate_limit.lua
local key = KEYS[1] -- 限流键名
local limit = tonumber(ARGV[1]) -- 限制次数
local window = tonumber(ARGV[2]) -- 时间窗口(秒)
-- 获取当前计数
local current = redis.call('INCR', key)
-- 第一次设置过期时间
if current == 1 then
redis.call('EXPIRE', key, window)
return 1 -- 允许
end
-- 检查是否超过限制
if current <= limit then
return 1 -- 允许
else
return 0 -- 拒绝
end
使用示例
# 限制用户每分钟最多 100 次请求
EVAL "
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
return 1
end
if current <= limit then
return 1
else
return 0
end
" 1 rate:user:123 100 60
3.3 扣减库存
-- deduct_stock.lua
local key = KEYS[1] -- 库存键名
local quantity = tonumber(ARGV[1]) -- 扣减数量
-- 获取当前库存
local stock = tonumber(redis.call('GET', key))
if not stock then
return -1 -- 商品不存在
end
if stock < quantity then
return 0 -- 库存不足
end
-- 扣减库存
redis.call('DECRBY', key, quantity)
return 1 -- 扣减成功
使用示例
# 扣减库存
EVAL "
local key = KEYS[1]
local quantity = tonumber(ARGV[1])
local stock = tonumber(redis.call('GET', key))
if not stock then
return -1
end
if stock < quantity then
return 0
end
redis.call('DECRBY', key, quantity)
return 1
" 1 stock:product:101 5
# 返回值:
# -1: 商品不存在
# 0: 库存不足
# 1: 扣减成功
3.4 排行榜(Sorted Set + 分页)
-- leaderboard.lua
local key = KEYS[1]
local page = tonumber(ARGV[1])
local page_size = tonumber(ARGV[2])
-- 计算起始位置
local start = (page - 1) * page_size
local stop = start + page_size - 1
-- 获取排行榜
local result = redis.call('ZREVRANGE', key, start, stop, 'WITHSCORES')
return result
3.5 缓存更新(先删除后更新)
-- cache_update.lua
local cache_key = KEYS[1]
local db_key = KEYS[2]
local new_value = ARGV[1]
-- 1. 删除缓存
redis.call('DEL', cache_key)
-- 2. 更新数据库(假设已在外部更新)
-- 这里只是标记需要更新
-- 3. 设置新缓存
redis.call('SET', cache_key, new_value, 'EX', 3600)
return 1
3.6 延时队列
-- delay_queue.lua
local queue_key = KEYS[1]
local current_time = tonumber(ARGV[1])
-- 获取到期的任务
local tasks = redis.call('ZRANGEBYSCORE', queue_key, 0, current_time, 'LIMIT', 0, 100)
if #tasks == 0 then
return {}
end
-- 删除已获取的任务
for i, task in ipairs(tasks) do
redis.call('ZREM', queue_key, task)
end
return tasks
3.7 防止缓存击穿(互斥锁)
-- cache_mutex.lua
local cache_key = KEYS[1]
local lock_key = KEYS[2]
local lock_value = ARGV[1]
local ttl = tonumber(ARGV[2])
-- 1. 尝试获取缓存
local cache = redis.call('GET', cache_key)
if cache then
return {1, cache} -- 缓存命中
end
-- 2. 尝试获取锁
local lock = redis.call('SET', lock_key, lock_value, 'NX', 'EX', 10)
if lock then
return {2, nil} -- 获取锁成功,需要查询数据库
else
return {3, nil} -- 获取锁失败,需要等待
end
4. 最佳实践
4.1 性能优化
-- 1. 使用局部变量
local redis_call = redis.call -- 缓存函数引用
-- 2. 批量操作
local results = {}
for i = 1, 100 do
results[i] = redis_call('GET', 'key' .. i)
end
-- 3. 避免大量数据返回
-- ❌ 不推荐
local all_data = redis_call('ZRANGE', 'large_set', 0, -1)
-- ✅ 推荐:分批获取
local batch_size = 100
local results = redis_call('ZRANGE', 'large_set', 0, batch_size - 1)
4.2 错误处理
-- 使用 pcall 捕获错误
local ok, result = pcall(redis.call, 'GET', 'key')
if not ok then
-- 处理错误
return {err = result}
end
return result
4.3 调试技巧
-- 使用 redis.log() 记录日志
redis.log(redis.LOG_WARNING, "Processing key: " .. KEYS[1])
-- 日志级别
-- redis.LOG_DEBUG
-- redis.LOG_VERBOSE
-- redis.LOG_NOTICE
-- redis.LOG_WARNING
4.4 注意事项
- 避免耗时操作:Lua 脚本会阻塞 Redis
- 避免随机性:不要使用 math.random()
- 避免全局变量:使用局部变量
- 限制脚本大小:建议小于 1KB
- 使用 EVALSHA:缓存脚本提高性能
5. 调试和测试
5.1 本地测试
# 使用 redis-cli 测试
redis-cli --eval script.lua key1 key2 , arg1 arg2
# 示例
redis-cli --eval lock.lua lock:test , uuid123 10
5.2 脚本调试
-- 在脚本中添加日志
redis.log(redis.LOG_WARNING, "Debug: value = " .. tostring(value))
-- 查看 Redis 日志
tail -f /var/log/redis/redis.log
5.3 性能测试
# 使用 redis-benchmark 测试
redis-benchmark -n 10000 EVAL "return redis.call('GET', 'key')" 0
6. 常用 Lua 脚本模板
6.1 限流模板
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
return current <= limit and 1 or 0
6.2 分布式锁模板
-- 加锁
local key = KEYS[1]
local value = ARGV[1]
local ttl = tonumber(ARGV[2])
return redis.call('SET', key, value, 'NX', 'EX', ttl) and 1 or 0
-- 解锁
local key = KEYS[1]
local value = ARGV[1]
local current = redis.call('GET', key)
if current == value then
redis.call('DEL', key)
return 1
end
return 0
6.3 原子递增/递减模板
local key = KEYS[1]
local delta = tonumber(ARGV[1])
local max = tonumber(ARGV[2])
local min = tonumber(ARGV[3])
local current = tonumber(redis.call('GET', key)) or 0
local new_value = current + delta
if new_value > max then
return {0, current} -- 超过最大值
elseif new_value < min then
return {0, current} -- 低于最小值
end
redis.call('SET', key, new_value)
return {1, new_value}
7. 高频问题
Q1: Redis 为什么使用 Lua?
答案:
- 原子性:脚本作为整体执行
- 减少网络往返:复杂逻辑服务器端执行
- 可复用:脚本可缓存
- 灵活性:实现复杂业务逻辑
Q2: EVAL 和 EVALSHA 的区别?
答案:
- EVAL:每次传输脚本内容
- EVALSHA:使用脚本的 SHA1 值,脚本已缓存
Q3: Lua 脚本如何保证原子性?
答案: Redis 执行 Lua 脚本时,会阻塞其他命令,直到脚本执行完成。
Q4: Lua 脚本有什么限制?
答案:
- 不能使用随机函数(影响主从一致性)
- 不能有耗时操作(阻塞 Redis)
- 不能访问外部资源(网络、文件)
Q5: 如何优化 Lua 脚本性能?
答案:
- 使用局部变量
- 使用 EVALSHA 缓存脚本
- 减少数据传输
- 避免大量循环
总结
Lua 脚本核心:
- ✅ Lua 基础语法
- ✅ Redis Lua API
- ✅ 实战案例(限流、锁、库存等)
- ✅ 最佳实践
- ✅ 调试和测试
应用场景:
- 分布式锁
- 限流算法
- 原子操作
- 复杂业务逻辑
推荐学习:
- Lua 官方文档:www.lua.org/manual/
- Redis Lua 文档:redis.io/commands/ev…