一、为什么需要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时容易踩坑,比如脚本写得太长导致阻塞。记住三个黄金法则:

  1. 脚本尽量短小精悍,超过10行就要考虑拆分
  2. redis.pcall代替redis.call避免错误中断
  3. 所有参数通过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脚本执行变慢?试试这些优化手段:

  1. 减少网络开销:把多个GET合并成MGET
  2. 避免类型转换:Lua的tonumber比较耗时
  3. 复用脚本:先用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 *这种危险操作。分享几个血泪教训:

  1. 不要遍历Key:用SCAN代替KEYS
  2. 控制脚本复杂度:时间复杂度保持在O(1)或O(logN)
  3. 设置超时阈值:用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特性普及,可能会出现新的模式。建议现在就开始:

  1. 把常用操作封装成Lua脚本
  2. 建立脚本性能监控机制
  3. 定期Review脚本逻辑

最后送大家一个实用的调试技巧——在脚本里加入日志输出:

-- 技术栈:Redis + Lua
-- 带调试日志的脚本示例
redis.log(redis.LOG_NOTICE, "开始执行订单创建脚本")
local orderId = generateOrderId()
redis.log(redis.LOG_DEBUG, "生成订单ID:"..orderId)
-- ...业务逻辑...