高性能服务开发利器:redis+lua
Redis 与 Lua 脚本的结合,其核心价值在于 原子性操作 和 减少网络开销。
一、Redis 执行 Lua 脚本的优势
- 原子性
- Lua 脚本在 Redis 中原子执行,避免多命令竞态条件。
- 减少网络开销
- 将多个 Redis 命令合并为一个脚本,减少客户端与 Redis 之间的通信次数。
- 高性能
- Redis 会将 Lua 脚本缓存(通过 SHA1 摘要),后续通过
EVALSHA
直接调用,减少传输开销。
- Redis 会将 Lua 脚本缓存(通过 SHA1 摘要),后续通过
- 复杂逻辑支持
- 支持条件判断、循环等复杂逻辑,突破 Redis 单命令的限制。
二、基本用法
1. 执行 Lua 脚本
- **
EVAL
命令**:直接执行脚本。EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey hello -- KEYS[1] 对应 mykey,ARGV[1] 对应 hello
- **
EVALSHA
命令**:通过 SHA1 摘要执行已缓存的脚本。EVALSHA a1b2c3d4... 1 mykey hello
2. 参数传递
KEYS
和ARGV
是 Redis 与 Lua 之间的参数传递约定:KEYS
:用于指定 Redis 键(支持多个键,常用于集群分片)。ARGV
:用于传递非键参数(如值、标志位等)。
- 示例:
EVAL "return {KEYS[1], ARGV[1]}" 1 key1 arg1 -- 返回 { "key1", "arg1" }
3. 在 Lua 中调用 Redis 命令
- 使用
redis.call()
或redis.pcall()
执行 Redis 命令:-- 示例:原子递增并设置过期时间 EVAL " local count = redis.call('INCR', KEYS[1]) if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end return count " 1 my_counter 60
redis.call()
遇到错误会抛出异常,终止脚本;redis.pcall()
会捕获错误并返回错误表。
三、高级特性
1. 原子性与隔离性
- 原子性:脚本执行期间,其他客户端命令会被阻塞。
- 隔离性:Lua 脚本中不能访问外部全局变量,且 Redis 会重置脚本的 Lua 环境。
2. 脚本缓存与复用
- 使用
SCRIPT LOAD
预先缓存脚本,获取 SHA1 摘要:SCRIPT LOAD "return redis.call('GET', KEYS[1])" -- 返回 SHA1 摘要,如 'abc123...'
- 后续通过
EVALSHA
+ SHA1 执行,避免重复传输脚本内容。
3. 错误处理
- 抛出异常:使用
error()
函数返回错误信息。EVAL "if #KEYS == 0 then error('KEYS missing') end" 0
- 捕获异常:在客户端检查返回值类型,例如:
local ok, result = pcall(redis.call, 'GET', 'nonexistent_key') if not ok then return {err = result} end
4. 调试技巧
- 日志输出:使用
redis.log(redis.LOG_DEBUG, 'message')
输出调试信息到 Redis 日志。 - 模拟断点:在脚本中插入
redis.breakpoint()
(需配合调试工具)。
四、性能优化
- 避免大循环
- Lua 脚本执行期间会阻塞 Redis,避免在脚本中执行耗时操作(如遍历大表)。
- 复用脚本
- 尽量复用已缓存的脚本(
EVALSHA
),减少EVAL
的传输开销。
- 尽量复用已缓存的脚本(
- 参数最小化
- 减少
KEYS
和ARGV
的数量和大小,避免内存压力。
- 减少
五、常见应用场景
1. 限流(Rate Limiting)
-- KEYS[1]: 限流键,ARGV[1]: 时间窗口(秒),ARGV[2]: 最大请求数
local current = redis.call('GET', KEYS[1])
if current and tonumber(current) >= tonumber(ARGV[2]) then
return 0
else
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[1])
return 1
end
2. 原子操作(如扣减库存)
-- KEYS[1]: 库存键,ARGV[1]: 扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1 -- 成功
else
return 0 -- 库存不足
end
3. 分布式锁
-- KEYS[1]: 锁键,ARGV[1]: 锁值,ARGV[2]: 过期时间(毫秒)
if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then
return 1 -- 加锁成功
else
return 0 -- 加锁失败
end
4. 批量操作(如删除匹配模式的键)
-- 注意:慎用,可能阻塞 Redis!
local keys = redis.call('KEYS', ARGV[1])
for i, key in ipairs(keys) do
redis.call('DEL', key)
end
return #keys
六、注意事项
- 脚本阻塞风险
- 避免在脚本中执行
KEYS *
或大循环,防止长时间阻塞 Redis。
- 避免在脚本中执行
- 参数大小限制
- Lua 脚本和参数的总大小默认限制为 1MB(可通过
client-query-buffer-limit
调整)。
- Lua 脚本和参数的总大小默认限制为 1MB(可通过
- 集群模式
- 在 Redis Cluster 中,脚本操作的键必须位于同一节点(通过
{}
哈希标签强制路由)。
- 在 Redis Cluster 中,脚本操作的键必须位于同一节点(通过
七、调试与监控
- 查看脚本缓存
redis-cli SCRIPT EXISTS sha1_value
- 清除脚本缓存
redis-cli SCRIPT FLUSH
- 监控脚本执行
- 通过 Redis 的
SLOWLOG
监控慢脚本。
- 通过 Redis 的