Redis 事务、Lua 脚本和分布式锁是 Redis 的高级特性,在实际开发中经常使用。
1. Redis 事务
基本概念: Redis 事务通过 MULTI、EXEC、DISCARD、WATCH 等命令实现,可以一次性执行多个命令,保证这些命令要么全部执行,要么全部不执行。
基本用法:
bash# 开启事务 MULTI # 执行命令(命令会被放入队列) SET key1 value1 SET key2 value2 GET key1 # 执行事务 EXEC
特点:
- 原子性:事务中的命令要么全部执行,要么全部不执行
- 隔离性:事务执行过程中,其他客户端的命令不会插入
- 不支持回滚:Redis 事务不支持回滚,如果某个命令执行失败,其他命令仍会执行
WATCH 命令: WATCH 命令用于实现乐观锁,在事务执行前监控一个或多个 key,如果在事务执行前这些 key 被其他客户端修改,事务将不会执行。
bash# 监控 key WATCH balance # 开启事务 MULTI # 执行命令 DECRBY balance 100 # 执行事务(如果 balance 被其他客户端修改,事务将不会执行) EXEC
事务的局限性:
- 不支持条件判断
- 不支持循环
- 不支持复杂逻辑
2. Lua 脚本
基本概念: Lua 脚本可以在 Redis 服务器端执行,支持复杂的逻辑操作,保证原子性。
基本用法:
bash# 执行 Lua 脚本 EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue # 加载脚本并返回脚本 SHA SCRIPT LOAD "return redis.call('SET', KEYS[1], ARGV[1])" # 使用 SHA 执行脚本 EVALSHA <sha> 1 mykey myvalue
Lua 脚本的优势:
- 原子性:Lua 脚本执行期间,其他客户端的命令不会插入
- 减少网络往返:多个操作可以在服务器端一次性完成
- 支持复杂逻辑:支持条件判断、循环等复杂逻辑
- 复用性:脚本可以重复使用,提高性能
Lua 脚本示例:
示例一:实现分布式锁
lua-- 获取锁 if redis.call("SETNX", KEYS[1], ARGV[1]) == 1 then redis.call("EXPIRE", KEYS[1], ARGV[2]) return 1 else return 0 end
示例二:限流器
lua-- 限流器 local key = KEYS[1] local limit = tonumber(ARGV[1]) local current = tonumber(redis.call("GET", key) or "0") if current + 1 > limit then return 0 else redis.call("INCR", key) redis.call("EXPIRE", key, ARGV[2]) return 1 end
示例三:原子操作
lua-- 原子操作:只有当 key 的值等于 expected 时才更新 local current = redis.call("GET", KEYS[1]) if current == ARGV[1] then redis.call("SET", KEYS[1], ARGV[2]) return 1 else return 0 end
Lua 脚本的注意事项:
- Lua 脚本执行时间不能过长,否则会阻塞 Redis
- Lua 脚本中不能使用随机函数,否则会导致脚本在不同节点执行结果不一致
- Lua 脚本中不能使用阻塞命令
3. 分布式锁
基本概念: 分布式锁用于在分布式系统中实现互斥访问,确保同一时间只有一个客户端能够访问共享资源。
实现方式一:SETNX + EXPIRE
javapublic boolean tryLock(String key, String value, int expireTime) { // 使用 SETNX 设置锁 Long result = redis.setnx(key, value); if (result == 1) { // 设置过期时间 redis.expire(key, expireTime); return true; } return false; } public void unlock(String key, String value) { // 只有锁的持有者才能释放锁 String currentValue = redis.get(key); if (value.equals(currentValue)) { redis.del(key); } }
实现方式二:SET NX EX(推荐)
javapublic boolean tryLock(String key, String value, int expireTime) { // 使用 SET NX EX 命令,原子性设置锁和过期时间 String result = redis.set(key, value, "NX", "EX", expireTime); return "OK".equals(result); } public void unlock(String key, String value) { // 使用 Lua 脚本保证原子性 String script = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end"; redis.eval(script, Collections.singletonList(key), Collections.singletonList(value)); }
实现方式三:Redlock 算法 Redlock 是 Redis 官方推荐的分布式锁算法,适用于 Redis Cluster 场景。
javapublic boolean tryLock(String key, String value, int expireTime) { // 获取多个 Redis 节点的锁 int successCount = 0; for (RedisClient client : redisClients) { if (client.set(key, value, "NX", "EX", expireTime).equals("OK")) { successCount++; } } // 如果大多数节点获取锁成功,则认为获取锁成功 return successCount > redisClients.size() / 2; }
分布式锁的注意事项:
- 锁的过期时间:需要设置合理的过期时间,避免死锁
- 锁的续期:对于长时间任务,需要实现锁的续期机制
- 锁的可重入性:同一个线程可以多次获取同一个锁
- 锁的释放:只有锁的持有者才能释放锁
Redisson 分布式锁: Redisson 是一个功能强大的 Redis 客户端,提供了完整的分布式锁实现。
java// 获取锁 RLock lock = redisson.getLock("myLock"); try { // 尝试获取锁,最多等待 10 秒,锁自动释放时间为 30 秒 boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (locked) { // 执行业务逻辑 } } finally { // 释放锁 lock.unlock(); }
4. 事务 vs Lua 脚本 vs 分布式锁
| 特性 | 事务 | Lua 脚本 | 分布式锁 |
|---|---|---|---|
| 原子性 | 支持 | 支持 | 支持 |
| 复杂逻辑 | 不支持 | 支持 | 不支持 |
| 网络往返 | 多次 | 一次 | 多次 |
| 适用场景 | 简单批量操作 | 复杂逻辑操作 | 互斥访问 |
5. 最佳实践
使用事务的场景:
- 需要原子性执行多个简单命令
- 不需要条件判断和循环
使用 Lua 脚本的场景:
- 需要原子性执行多个复杂命令
- 需要条件判断和循环
- 需要减少网络往返
使用分布式锁的场景:
- 需要在分布式系统中实现互斥访问
- 需要防止并发问题
总结
Redis 事务、Lua 脚本和分布式锁是 Redis 的高级特性,各有其适用场景。事务适合简单的批量操作,Lua 脚本适合复杂的逻辑操作,分布式锁适合互斥访问。在实际开发中,需要根据具体的业务场景,选择合适的技术方案。同时,需要注意这些技术的局限性和注意事项,确保系统的稳定性和可靠性。