Redis 事务与 Lua 脚本:原子操作实战指南
🔄 Redis 事务与 Lua 脚本:原子操作实战指南
文章目录
- 🔄 Redis 事务与 Lua 脚本:原子操作实战指南
- 🧠 一、Redis 事务基础
- 💡 Redis 事务概述
- 🔧 事务基本命令
- 🛡️ WATCH 实现乐观锁
- ⚠️ 事务特性与限制
- ⚡ 二、Lua 脚本扩展能力
- 💡 为什么需要 Lua 脚本?
- 🚀 Lua 脚本基本用法
- 📊 Lua Redis API
- 🚀 三、实战案例解析
- 💰 案例1:账户转账事务
- 🔒 案例2:分布式锁实现
- 🛒 案例3:库存扣减防超卖
- 📊 事务 vs Lua 对比
- 📊 四、常见问题与优化
- ⚠️ 常见问题排查
- 🚀 性能优化建议
- 💡 五、总结与最佳实践
- 🎯 技术选型指南
- 📚 最佳实践总结
- 🔧 生产环境建议
- 🚀 扩展应用场景
🧠 一、Redis 事务基础
💡 Redis 事务概述
Redis 事务通过 MULTI/EXEC 命令组合实现,允许一次性执行多个命令,确保这些命令的连续性和隔离性。
🔧 事务基本命令
# 开始事务
MULTI# 排队命令
SET user:1001:name "张三"
SET user:1001:age 25
INCR user:1001:visits# 执行事务
EXEC# 取消事务
DISCARD
🛡️ WATCH 实现乐观锁
乐观锁机制:
# 监视关键键
WATCH user:1001:balance# 获取当前值
GET user:1001:balance # 返回 1000# 开始事务
MULTI
DECRBY user:1001:balance 100
EXEC # 如果期间balance被修改,返回(nil)
监控模式:
⚠️ 事务特性与限制
特性 | Redis 事务 | 关系型数据库事务 |
---|---|---|
原子性 | 部分支持(命令错误不影响其他) | 完全支持 |
隔离性 | 通过WATCH实现乐观锁 | 多级别隔离支持 |
一致性 | 支持 | 支持 |
持久性 | 依赖持久化配置 | 支持 |
回滚能力 | 不支持(EXEC后不能回滚) | 支持 |
⚡ 二、Lua 脚本扩展能力
💡 为什么需要 Lua 脚本?
Lua 脚本优势:
- ⚡ 原子性:整个脚本作为一个命令执行
- 📉 网络优化:减少网络往返次数
- 🔧 复杂性:实现复杂业务逻辑
- 📚 复用性:脚本可缓存和重用
🚀 Lua 脚本基本用法
执行脚本:
# 直接执行Lua脚本
EVAL "return redis.call('GET', KEYS[1])" 1 user:1001:name# 脚本缓存执行
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
EVALSHA <sha1_hash> 1 user:1001:name
脚本管理:
# 检查脚本是否存在
SCRIPT EXISTS <sha1_hash># 清除所有脚本缓存
SCRIPT FLUSH# 杀死运行中的脚本
SCRIPT KILL
📊 Lua Redis API
基本数据操作:
-- 字符串操作
redis.call('SET', KEYS[1], ARGV[1])
local value = redis.call('GET', KEYS[1])-- 哈希操作
redis.call('HSET', KEYS[1], 'field1', ARGV[1])
local fieldValue = redis.call('HGET', KEYS[1], 'field1')-- 集合操作
redis.call('SADD', KEYS[1], ARGV[1])
local exists = redis.call('SISMEMBER', KEYS[1], ARGV[1])-- 列表操作
redis.call('LPUSH', KEYS[1], ARGV[1])
local item = redis.call('RPOP', KEYS[1])
复杂操作示例:
-- 条件操作
if redis.call('EXISTS', KEYS[1]) == 1 thenreturn redis.call('GET', KEYS[1])
elsereturn nil
end-- 循环操作
for i, key in ipairs(KEYS) doredis.call('INCR', key)
end
🚀 三、实战案例解析
💰 案例1:账户转账事务
# 使用事务实现转账
WATCH account:1001:balance account:1002:balanceMULTI
DECRBY account:1001:balance 100
INCRBY account:1002:balance 100
EXEC
🔒 案例2:分布式锁实现
Lua 脚本实现原子锁:
-- 获取分布式锁
local key = KEYS[1]
local value = ARGV[1]
local ttl = ARGV[2]-- 只有键不存在时才能设置成功
local result = redis.call('SETNX', key, value)
if result == 1 then-- 设置过期时间,防止死锁redis.call('EXPIRE', key, ttl)return true
else-- 检查是否是自己持有的锁(值匹配)local currentValue = redis.call('GET', key)if currentValue == value then-- 续期redis.call('EXPIRE', key, ttl)return trueelsereturn falseend
end
Java 调用示例:
public boolean acquireLock(String key, String value, int ttl) {String script = "local result = redis.call('SETNX', KEYS[1], ARGV[1]); " +"if result == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]); return true; " +"else local current = redis.call('GET', KEYS[1]); " +"if current == ARGV[1] then redis.call('EXPIRE', KEYS[1], ARGV[2]); return true; " +"else return false; end end";Object result = jedis.eval(script, 1, key, value, String.valueOf(ttl));return Boolean.TRUE.equals(result);
}
🛒 案例3:库存扣减防超卖
Lua 脚本实现原子扣减:
-- 库存扣减脚本
local productKey = KEYS[1] -- 商品库存键
local orderKey = KEYS[2] -- 订单记录键
local quantity = tonumber(ARGV[1]) -- 购买数量
local userId = ARGV[2] -- 用户ID
local orderId = ARGV[3] -- 订单ID-- 检查库存
local stock = tonumber(redis.call('GET', productKey))
if not stock or stock < quantity thenreturn nil -- 库存不足
end-- 扣减库存
redis.call('DECRBY', productKey, quantity)-- 记录订单
redis.call('HSET', orderKey, orderId, 'user:' .. userId .. ':quantity:' .. quantity .. ':time:' .. redis.call('TIME')[1])return stock - quantity -- 返回剩余库存
Spring Boot 集成示例:
@Service
public class InventoryService {@Autowiredprivate StringRedisTemplate redisTemplate;private static final String STOCK_DEDUCT_SCRIPT ="local stock = tonumber(redis.call('GET', KEYS[1])) " +"if not stock or stock < tonumber(ARGV[1]) then return nil end " +"redis.call('DECRBY', KEYS[1], ARGV[1]) " +"redis.call('HSET', KEYS[2], ARGV[3], 'user:' .. ARGV[2] .. ':quantity:' .. ARGV[1]) " +"return stock - tonumber(ARGV[1])";public boolean deductStock(String productId, int quantity, String userId, String orderId) {List<String> keys = Arrays.asList("stock:" + productId, "orders:" + productId);Object result = redisTemplate.execute(new DefaultRedisScript<>(STOCK_DEDUCT_SCRIPT, Long.class),keys, String.valueOf(quantity), userId, orderId);return result != null;}
}
📊 事务 vs Lua 对比
场景 | 推荐方案 | 理由 |
---|---|---|
简单多命令 | 事务 | 实现简单,易于理解 |
复杂业务逻辑 | Lua脚本 | 原子性保证,减少网络开销 |
需要条件判断 | Lua脚本 | 事务不支持条件逻辑 |
高并发竞争 | Lua脚本 + WATCH | 更好的原子性和性能 |
可复用逻辑 | Lua脚本 | 脚本可缓存,多次使用 |
📊 四、常见问题与优化
⚠️ 常见问题排查
1. 事务执行失败:
# 检查WATCH键是否被修改
WATCH key
MULTI
SET key value
EXEC # 返回(nil)表示执行失败
2. Lua 脚本超时:
# 设置脚本执行超时
config set lua-time-limit 5000 # 5秒# 监控脚本执行
redis-cli --eval slow_script.lua , 监控脚本执行状态
3. 脚本调试技巧:
-- 使用redis.log进行调试
redis.log(redis.LOG_NOTICE, "Debug value: " .. tostring(value))-- 返回调试信息
return {redis.call('GET', KEYS[1]), "debug info"}
🚀 性能优化建议
1. 脚本缓存优化:
// 预加载并缓存脚本SHA
public class ScriptManager {private static Map<String, String> scriptShaCache = new HashMap<>();public static String loadScript(Jedis jedis, String script, String scriptName) {String sha = scriptShaCache.get(scriptName);if (sha == null) {sha = jedis.scriptLoad(script);scriptShaCache.put(scriptName, sha);}return sha;}
}
2. 管道优化:
// 使用管道执行多个脚本
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 100; i++) {pipeline.evalsha(scriptSha, keys, args);
}
List<Object> results = pipeline.syncAndReturnAll();
3. 超时处理:
-- 脚本内添加超时检查
local start_time = redis.call('TIME')[1]
-- 业务逻辑
if redis.call('TIME')[1] - start_time > 4 thenreturn {err = "timeout"}
end
💡 五、总结与最佳实践
🎯 技术选型指南
📚 最佳实践总结
事务使用场景:
- ✅ 简单的多命令批量执行
- ✅ 需要乐观锁的并发控制
- ✅ 命令之间没有复杂逻辑依赖
Lua脚本使用场景:
- ✅ 需要原子性的复杂业务逻辑
- ✅ 减少网络往返开销
- ✅ 条件判断和循环处理
- ✅ 可复用的业务逻辑封装
🔧 生产环境建议
1. 脚本管理规范:
- 脚本版本管理和归档
- 脚本性能测试和压测
- 脚本回滚方案准备
2. 监控告警配置:
# 监控脚本执行时间
redis-cli info commandstats | grep eval# 监控事务执行情况
redis-cli info stats | grep rejected
- 安全规范:
- 脚本参数校验和过滤
- 避免脚本无限循环
- 限制脚本执行权限
🚀 扩展应用场景
1. 分布式限流:
-- 令牌桶限流算法
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])local data = redis.call('HMGET', key, 'tokens', 'timestamp')
local tokens = tonumber(data[1]) or capacity
local last_time = tonumber(data[2]) or now-- 计算新增令牌
local time_passed = now - last_time
tokens = math.min(capacity, tokens + time_passed * rate)-- 检查是否允许请求
if tokens >= requested thentokens = tokens - requestedredis.call('HMSET', key, 'tokens', tokens, 'timestamp', now)return true
elsereturn false
end
2. 排行榜更新:
-- 原子更新排行榜
local player = ARGV[1]
local score = tonumber(ARGV[2])-- 更新有序集合
redis.call('ZADD', 'leaderboard', score, player)-- 只保留前100名
redis.call('ZREMRANGEBYRANK', 'leaderboard', 0, -101)-- 获取当前排名
return redis.call('ZREVRANK', 'leaderboard', player)