Redis Lua 脚本详细教程
Redis 从 2.6.0 版本开始内置了 Lua 解释器允许用户在服务器端执行 Lua 脚本。通过 Lua 脚本可以将多个 Redis 命令封装在一起以原子方式执行极大提升复杂操作的效率与可靠性。为什么使用 Lua 脚本原子性Redis 在执行 Lua 脚本期间不会执行其他命令整个脚本要么完全执行要么完全不执行类似于事务但更灵活。减少网络延迟将多个命令打包成一个脚本只需一次网络请求避免多次 RTT。复用与模块化脚本可以常驻服务端被客户端反复调用。更强的逻辑能力支持条件判断、循环、变量等编程特性弥补原生命令的不足。部分替代事务传统 MULTI/EXEC 事务无法根据中间结果做决策而 Lua 脚本可以。环境准备确保 Redis 版本 2.6.0最好使用 5.0 以上版本以获得更佳体验。通过redis-cli或任意编程语言客户端如 Python、Java、Go执行脚本。基础语法与快速上手在 Redis 中执行 Lua 脚本EVALEVAL script numkeys key [key ...] arg [arg ...]scriptLua 脚本字符串。numkeys后面 key 参数的数量。key脚本中要操作的 Redis 键名可通过KEYS数组访问。arg附加参数通过ARGV数组访问。示例返回 Hello 与传入的 nameEVAL return Hello .. ARGV[1] 0 world输出Hello world访问 Redis 键在 Lua 脚本中通过redis.call()或redis.pcall()调用 Redis 命令。区别在于pcall会捕获异常并返回错误表不中断脚本。-- 原子递增并返回新值 EVAL local val redis.call(INCR, KEYS[1]); return val 1 counter参数传递-- 设置多个字段返回 OK EVAL for i1, #KEYS do redis.call(SET, KEYS[i], ARGV[i]) end return OK 2 name age Alice 25核心命令详解1.EVAL– 直接执行脚本每次都会传输完整脚本内容适合临时或小脚本。2.EVALSHA– 通过 SHA1 哈希执行如果脚本已经加载到 Redis 服务端可以通过EVALSHA执行节省带宽。SCRIPT LOAD return Hello Redis # 返回 c686f316aaf1eb01d7a4c1ae86f7d2b9c0f5f4f3 EVALSHA c686f316aaf1eb01d7a4c1ae86f7d2b9c0f5f4f3 03. 脚本管理命令SCRIPT LOAD script– 加载脚本返回 SHA。SCRIPT EXISTS sha1 [sha1 ...]– 检查脚本是否存在于缓存中。SCRIPT FLUSH– 清空所有已加载的脚本。SCRIPT KILL– 杀死运行超时的脚本仅限未执行写操作时。深入 Lua 脚本编程数据类型映射Redis 与 Lua 之间的类型转换规则Redis 返回值Lua 类型integer (或 OK)number / table (含 ok 字段)stringstringnilnilarrayLua table从1开始索引示例处理 Redis 返回的数组local res redis.call(HMGET, user:1, name, age) -- res 是一个 tableres[1] 是 nameres[2] 是 age return {res[1], tonumber(res[2])}条件与循环local key KEYS[1] local limit tonumber(ARGV[1]) local current redis.call(GET, key) if not current then redis.call(SET, key, 0) current 0 else current tonumber(current) end if current limit then return {err over limit} end redis.call(INCR, key) return {ok success}错误处理使用redis.pcall捕获错误避免脚本因异常而终止。local result redis.pcall(LPOP, KEYS[1]) if result.type err then return {error pop failed: .. result.err} end return result脚本中的全局变量默认情况下Lua 脚本不允许定义全局变量避免污染。可使用local声明局部变量。如需开启全局不推荐需修改 Redis 配置lua-enable-global。原子性说明Redis 保证 Lua 脚本执行期间不会被其他命令打断。但注意脚本中的写操作一旦执行即使脚本后续出错已执行的写操作不会回滚Redis 事务也不支持回滚。因此需谨慎设计。若脚本运行超时默认 5 秒Redis 会标记该脚本为“忙碌”不再接受其他命令。此时只能执行SCRIPT KILL或SHUTDOWN NOSAVE。性能优化建议尽量少用KEYS全局扫描脚本内调用KEYS或SCAN会阻塞 Redis改用SSCAN/HSCAN或维护索引集合。控制脚本执行时间避免在脚本内循环执行大量命令如 10 万次INCR应拆分为多次调用或使用游标。利用EVALSHA将长脚本预先加载生产环境务必使用EVALSHA。避免在脚本中生成大表返回返回值会序列化后再发送给客户端占用内存和网络。调试技巧使用redis.logredis.log(redis.LOG_NOTICE, value is , value)日志级别LOG_DEBUG,LOG_VERBOSE,LOG_NOTICE,LOG_WARNING。查看日志需调整 Redis 日志级别。使用redis.replicate_commands默认情况下脚本中的写命令会以事务模式复制到从库/AOF。如果脚本包含随机性命令如TIME、RANDOMKEY会导致主从数据不一致。解决方案redis.replicate_commands() -- 在脚本开头调用启用效果复制模式 local time redis.call(TIME)[1] redis.call(SET, KEYS[1], time)启用后Redis 会记录脚本中写命令的具体参数并复制而不是复制整个脚本。常见应用场景1. 原子性限流滑动窗口或固定窗口-- 固定窗口限流key 为 user:123:rate_limit, ARGV[1]max_requests, ARGV[2]window_seconds local key KEYS[1] local max tonumber(ARGV[1]) local window tonumber(ARGV[2]) local now redis.call(TIME)[1] local bucket redis.call(GET, key) if bucket and tonumber(bucket) max then return 0 end if not bucket then redis.call(SETEX, key, window, 1) else redis.call(INCR, key) end return 12. 分布式锁释放原子性检查并删除-- 释放锁只有 value 匹配时才删除 if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end3. 批量操作且需要中间结果例如批量获取多个 hash 的某个字段并进行过滤local result {} for i, key in ipairs(KEYS) do local val redis.call(HGET, key, status) if val active then result[#result1] key end end return result4. 条件更新计数器local key KEYS[1] local delta tonumber(ARGV[1]) local min tonumber(ARGV[2]) local max tonumber(ARGV[3]) local current tonumber(redis.call(GET, key) or 0) local new current delta if new min then new min end if new max then new max end redis.call(SET, key, new) return new注意事项与最佳实践注意点建议脚本超时设置lua-time-limit并避免长时间循环使用SCRIPT KILL处理随机性命令如需随机调用redis.replicate_commands()以保证主从一致内存消耗脚本返回值不宜过大避免返回整个 hash 或 list键的规划脚本中所有要访问的键必须通过KEYS传入不要动态拼接键名否则影响集群模式集群兼容性在 Redis Cluster 中一个脚本只能访问同一 slot 中的键可通过hash tag强制放同一 slot版本升级旧版 Redis 中 Lua 字节码可能因升级失效需重载脚本调试生产环境先使用EVAL测试稳定后改用SCRIPT LOADEVALSHA避免全局变量使用local声明所有变量避免意外共享完整示例令牌桶限流器-- 令牌桶算法 -- KEYS[1] bucket_key -- ARGV[1] rate (tokens per second) -- ARGV[2] capacity (max tokens) -- ARGV[3] requested (tokens needed) -- ARGV[4] now (timestamp in seconds) local key KEYS[1] local rate tonumber(ARGV[1]) local capacity tonumber(ARGV[2]) local requested tonumber(ARGV[3]) local now tonumber(ARGV[4]) local bucket redis.call(HMGET, key, tokens, last_time) local tokens tonumber(bucket[1]) or capacity local last_time tonumber(bucket[2]) or now local delta math.max(0, now - last_time) local filled math.min(capacity, tokens delta * rate) local allowed filled requested local new_tokens filled if allowed then new_tokens filled - requested end redis.call(HMSET, key, tokens, new_tokens, last_time, now) redis.call(EXPIRE, key, math.ceil(capacity / rate) 1) -- 过期时间略大于填满时间 return { allowed, new_tokens }调用方式EVAL $(cat token_bucket.lua) 1 rate_limit:user123 5 10 1 1620000000总结Redis Lua 脚本将计算推向数据侧提供原子性、高性能的复杂操作能力。熟练掌握EVAL、EVALSHA、脚本调试和集群约束可以大幅优化缓存中间件的使用模式。从简单的原子性 CAS到复杂的限流、队列管理Lua 脚本都是 Redis 进阶用户的必备技能。实际生产使用时建议将脚本文件独立维护通过加载工具如 Redis 自带的--eval选项进行测试并配合监控系统观察脚本执行耗时与错误率。