一、为什么需要Lua+Redis这对组合
当你的网站突然涌进来大量用户,数据库开始喘不过气的时候,Redis就像一个超级快递员,能快速把数据送到用户手里。但有时候业务逻辑太复杂,光靠Redis的简单命令搞不定,这时候Lua脚本就派上用场了——它能让Redis一次性执行多个操作,还能保证这些操作像被胶水粘住一样不会中途被其他请求打断。
举个现实例子:秒杀活动中既要扣库存又要生成订单。如果分开执行,可能库存扣了但订单没生成,或者订单重复生成。用Lua脚本就能原子性完成这两步:
-- 技术栈:Redis + Lua
-- KEYS[1]库存key, KEYS[2]订单key, ARGV[1]用户ID, ARGV[2]购买数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[2]) then
redis.call('DECRBY', KEYS[1], ARGV[2])
redis.call('HSET', KEYS[2], ARGV[1], ARGV[2])
return "SUCCESS"
else
return "OUT_OF_STOCK"
end
二、Lua脚本在Redis中的正确打开方式
很多人第一次用Redis执行Lua时容易踩坑,比如脚本写得太长导致阻塞。记住三个黄金法则:
- 脚本尽量短小精悍,超过10行就要考虑拆分
- 用
redis.pcall代替redis.call避免错误中断 - 所有参数通过KEYS和ARGV传递,不要硬编码
这里有个带异常处理的增强版示例:
-- 技术栈:Redis + Lua
-- 带错误回滚的转账脚本
local from = KEYS[1]
local to = KEYS[2]
local amount = tonumber(ARGV[1])
local fromBalance = tonumber(redis.call('GET', from)) or 0
if fromBalance < amount then
return {err = "INSUFFICIENT_BALANCE"}
end
redis.call('DECRBY', from, amount)
local success = pcall(function()
redis.call('INCRBY', to, amount)
end)
if not success then
-- 回滚操作
redis.call('INCRBY', from, amount)
return {err = "TRANSFER_FAILED"}
end
return {ok = "SUCCESS"}
三、性能调优实战技巧
在压测时发现Lua脚本执行变慢?试试这些优化手段:
- 减少网络开销:把多个GET合并成MGET
- 避免类型转换:Lua的tonumber比较耗时
- 复用脚本:先用SCRIPT LOAD加载,再用EVALSHA执行
看这个优化前后的对比示例:
-- 技术栈:Redis + Lua
-- 优化前:多次网络往返
local user1 = redis.call('GET', 'user:1001')
local user2 = redis.call('GET', 'user:1002')
local user3 = redis.call('GET', 'user:1003')
-- 优化后:一次批量获取
local users = redis.call('MGET', 'user:1001', 'user:1002', 'user:1003')
local result = {}
for i, v in ipairs(users) do
result[i] = processUserData(v) -- 假设有个处理函数
end
四、避坑指南与最佳实践
最近帮一个电商团队排查了个典型问题:他们的Lua脚本在流量高峰时经常超时。最后发现是脚本里用了KEYS *这种危险操作。分享几个血泪教训:
- 不要遍历Key:用SCAN代替KEYS
- 控制脚本复杂度:时间复杂度保持在O(1)或O(logN)
- 设置超时阈值:用
redis-cli --eval timeout=500
这里有个安全遍历的示范:
-- 技术栈:Redis + Lua
-- 安全遍历用户黑名单
local cursor = "0"
local result = {}
repeat
local reply = redis.call("SCAN", cursor, "MATCH", "blacklist:*")
cursor = reply[1]
for _, key in ipairs(reply[2]) do
table.insert(result, redis.call("GET", key))
end
until cursor == "0"
return result
五、不同场景下的选择策略
不是所有高并发场景都适合用Lua脚本。根据我们的实战经验:
适合场景:
- 需要原子性的多步操作
- 对延迟敏感的核心业务逻辑
- 需要减少网络往返的场景
不适合场景:
- 需要事务回滚的复杂业务(考虑用数据库事务)
- 脚本执行超过1ms的耗时操作
- 需要跨节点协调的操作
比如社交媒体的点赞功能就特别适合:
-- 技术栈:Redis + Lua
-- 点赞去重脚本
local postKey = KEYS[1]
local userKey = KEYS[2]
local userId = ARGV[1]
if redis.call('SISMEMBER', userKey, userId) == 1 then
return 0 -- 已点赞
else
redis.call('SADD', userKey, userId)
redis.call('INCR', postKey)
return 1 -- 点赞成功
end
六、总结与展望
经过多个项目的验证,Lua+Redis的组合在高并发场景下确实能打,但要注意控制脚本复杂度。未来随着Redis 7.0的Function特性普及,可能会出现新的模式。建议现在就开始:
- 把常用操作封装成Lua脚本
- 建立脚本性能监控机制
- 定期Review脚本逻辑
最后送大家一个实用的调试技巧——在脚本里加入日志输出:
-- 技术栈:Redis + Lua
-- 带调试日志的脚本示例
redis.log(redis.LOG_NOTICE, "开始执行订单创建脚本")
local orderId = generateOrderId()
redis.log(redis.LOG_DEBUG, "生成订单ID:"..orderId)
-- ...业务逻辑...
评论