乐闻世界logo
搜索文章和话题

Redis 事务、Lua 脚本和分布式锁的实现原理和使用场景是什么?

2月19日 19:38

Redis 事务、Lua 脚本和分布式锁是 Redis 的高级特性,在实际开发中经常使用。

1. Redis 事务

基本概念: Redis 事务通过 MULTI、EXEC、DISCARD、WATCH 等命令实现,可以一次性执行多个命令,保证这些命令要么全部执行,要么全部不执行。

基本用法

bash
# 开启事务 MULTI # 执行命令(命令会被放入队列) SET key1 value1 SET key2 value2 GET key1 # 执行事务 EXEC

特点

  1. 原子性:事务中的命令要么全部执行,要么全部不执行
  2. 隔离性:事务执行过程中,其他客户端的命令不会插入
  3. 不支持回滚: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 脚本的优势

  1. 原子性:Lua 脚本执行期间,其他客户端的命令不会插入
  2. 减少网络往返:多个操作可以在服务器端一次性完成
  3. 支持复杂逻辑:支持条件判断、循环等复杂逻辑
  4. 复用性:脚本可以重复使用,提高性能

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

java
public 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(推荐)

java
public 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 场景。

java
public 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; }

分布式锁的注意事项

  1. 锁的过期时间:需要设置合理的过期时间,避免死锁
  2. 锁的续期:对于长时间任务,需要实现锁的续期机制
  3. 锁的可重入性:同一个线程可以多次获取同一个锁
  4. 锁的释放:只有锁的持有者才能释放锁

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 脚本适合复杂的逻辑操作,分布式锁适合互斥访问。在实际开发中,需要根据具体的业务场景,选择合适的技术方案。同时,需要注意这些技术的局限性和注意事项,确保系统的稳定性和可靠性。

标签:Redis