Lua 脚本在 Redis 中的应用
在高并发场景下,Redis 不仅是一个缓存工具,更是实现分布式协调的重要组件。尤其是分布式锁,在防止资源争用、保证数据一致性方面有着关键作用。
实现分布式锁有多种方式,但无论是 SETNX
命令还是 Redisson 框架,背后都有一个绕不开的关键词——Lua 脚本。
一、什么是 Lua 脚本
Lua 是一种轻量、高效的脚本语言,Redis 原生支持用 Lua 脚本执行命令,并且可以保证脚本中的所有 Redis 操作是原子性的。
原子性:脚本中多条命令要么全部执行,要么全部不执行,中间不会被其他请求打断。
换句话说,Lua 脚本在 Redis 中相当于“批量命令的事务”,非常适合需要多步逻辑且不能被打断的场景。
二、分布式锁的常见实现
1. 使用 SETNX
实现加锁
最简单的分布式锁实现:
SETNX lock_key unique_id
lock_key
:锁的键名unique_id
:锁的唯一标识(防止误删他人锁)
加锁逻辑:
- 如果
SETNX
返回1
,表示加锁成功 - 如果返回
0
,说明锁已经被别人持有
解锁逻辑:
if redis.call("get", "lock_key") == unique_id thenredis.call("del", "lock_key")
end
2. 解锁的并发安全问题
如果用普通代码分两步实现解锁(先 GET
再 DEL
):
if (redisTemplate.opsForValue().get("lock_key").equals(unique_id)) {redisTemplate.delete("lock_key");
}
在高并发下可能发生:
- 线程 A
GET
到锁是自己的 - 线程 B 抢占了锁(设置了新的
unique_id
) - 线程 A 执行
DEL
,结果误删了线程 B 的锁
这就破坏了锁的正确性。
三、用 Lua 脚本保证解锁原子性
解决方法是用 Lua 脚本将“判断锁持有者”和“删除锁”两个步骤打包成一个原子操作:
if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])
elsereturn 0
end
Java 调用示例:
String script ="if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) else return 0 end";Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),Collections.singletonList("lock_key"),"unique_id"
);if (result != null && result > 0) {System.out.println("解锁成功");
} else {System.out.println("解锁失败");
}
这样,无论并发多高,解锁过程都是安全的。
四、Redisson 的分布式锁
如果不想手写 Lua 脚本,可以直接使用 Redisson,它内部已经用 Lua 脚本实现了加锁、解锁的原子性,并且支持:
- 可重入锁
- 公平锁
- 看门狗自动续期
示例:
RLock lock = redissonClient.getLock("lock_key");
lock.lock();
try {// 业务逻辑
} finally {lock.unlock();
}