一、前言:为什么 Redis 开发者要学 Lua?
你是否遇到过这些场景?
- ❌ 需要原子性地执行"判断 + 修改"操作(如限流、扣库存)
- ❌ 多条 Redis 命令无法保证事务性
- ❌ 网络往返多次,性能低下
Redis 内置 Lua 脚本引擎 ,让你用一段脚本 完成复杂逻辑,并保证原子执行!
而 Lua 本身------轻量、简洁、易学,10 分钟就能上手基础语法。
本文将带你快速掌握 Lua 核心语法,并演示如何在 Redis 中使用!
二、Lua 是什么?
- 轻量级脚本语言:整个解释器仅几百 KB
- 嵌入式设计 :被广泛用于游戏(如魔兽世界)、Nginx(OpenResty)、Redis
- 语法极简:没有类、没有异常、变量无需声明类型
💡 对 Redis 开发者而言 :Lua = 实现原子复合操作的利器!
三、Lua 基础语法速览(对比 Python/JS)
3.1 注释
Lua
-- 单行注释
--[[
多行注释
--]]
3.2 变量与数据类型
Lua 只有 8 种基本类型,最常用的是:
nil(空值)boolean(true/false)number(整数和浮点统一)string(字符串)table(万能结构:数组 + 字典)
Lua
-- 变量无需声明,直接赋值
name = "Alice" -- string
age = 25 -- number
is_ok = true -- boolean
nothing = nil -- nil
-- table:既是数组又是 map
arr = {10, 20, 30} -- 数组(索引从 1 开始!)
user = {name="Bob", age=30} -- 字典
⚠️ 重要 :Lua 数组索引从 1 开始,不是 0!
3.3 控制结构
if 判断
Lua
if age >= 18 then
print("成年人")
elseif age > 0 then
print("未成年人")
else
print("无效年龄")
end
for 循环
Lua
-- 数值 for(推荐)
for i = 1, 5 do
print(i) -- 输出 1,2,3,4,5
end
-- 泛型 for(遍历 table)
for key, value in pairs(user) do
print(key, value)
end
while 循环
Lua
local i = 1
while i <= 3 do
print(i)
i = i + 1
end
3.4 函数
Lua
-- 定义函数
function add(a, b)
return a + b
end
-- 调用
result = add(3, 5)
-- 函数可作为参数(高阶函数)
3.5 局部变量(强烈推荐!)
Lua
local x = 10 -- 局部变量,作用域仅当前 block
global_y = 20 -- 全局变量(不推荐)
✅ 最佳实践 :始终用
local声明变量,避免污染全局环境!
四、Redis 中的 Lua 脚本:核心规则
在 Redis 中执行 Lua 脚本,使用 EVAL 命令:
bash
EVAL "脚本内容" key个数 [key ...] [arg ...]
4.1 两个关键全局变量
KEYS:数组,存放传入的 key(从 1 开始索引)ARGV:数组,存放传入的参数
bash
-- 示例:获取第一个 key 的值,并与第一个参数比较
local value = redis.call('GET', KEYS[1])
if tonumber(value) > tonumber(ARGV[1]) then
return "OK"
else
return "LOW"
end
4.2 调用 Redis 命令
redis.call():执行命令,出错时抛出 Lua 错误redis.pcall():执行命令,出错时返回错误对象(不中断)
bash
-- 原子扣减库存
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1 -- 成功
else
return 0 -- 库存不足
end
五、实战案例:实现分布式限流
使用令牌桶算法,每秒最多 10 次请求。
Lua 脚本(rate_limit.lua)
bash
local key = KEYS[1] -- 限流 key,如 user:1001:rate
local max_count = tonumber(ARGV[1]) -- 最大令牌数(10)
local window = tonumber(ARGV[2]) -- 时间窗口(1秒)
-- 获取当前令牌数和最后更新时间
local current_count = redis.call('HGET', key, 'count')
local last_time = redis.call('HGET', key, 'time')
local now = tonumber(redis.call('TIME')[1]) -- 当前时间戳(秒)
if not last_time then
-- 第一次访问
redis.call('HMSET', key, 'count', max_count - 1, 'time', now)
redis.call('EXPIRE', key, window)
return 1
else
local count = tonumber(current_count)
local elapsed = now - tonumber(last_time)
if elapsed >= window then
-- 窗口已过,重置令牌
count = max_count
end
if count > 0 then
redis.call('HMSET', key, 'count', count - 1, 'time', now)
redis.call('EXPIRE', key, window)
return 1 -- 允许
else
return 0 -- 拒绝
end
end
在 Java 中调用(Spring Boot + RedisTemplate)
java
String script = Files.readString(Paths.get("rate_limit.lua"));
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList("user:1001:rate"),
"10", "1"
);
if (result == 1) {
// 请求通过
} else {
// 触发限流
}
✅ 优势 :整个逻辑原子执行,无并发问题!
六、Lua 在 Redis 中的限制
为了保证安全和性能,Redis 对 Lua 脚本做了严格限制:
- ❌ 不能访问系统时间(只能用
redis.call('TIME')) - ❌ 不能有随机数(除非用
math.randomseed固定种子) - ❌ 不能有 I/O 操作
- ❌ 脚本必须是纯函数(相同输入 → 相同输出)
⚠️ 注意 :脚本执行期间,Redis 是单线程阻塞的!务必保证脚本轻量(< 10ms)。
七、调试技巧
7.1 在 Redis CLI 中测试
bash
redis-cli EVAL "
local val = redis.call('GET', KEYS[1])
return val or 'NOT_FOUND'
" 1 mykey
7.2 使用 redis.log() 打印日志(仅调试)
bash
redis.log(redis.LOG_WARNING, "Debug: count=" .. count)
日志会输出到 Redis 服务端日志文件中。
八、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!