Redis之Lua脚本与分布式锁改造
一、Lua脚本解决多条命令原子性问题
1.1 为什么需要Lua脚本
在分布式系统中,当需要执行多个Redis命令时,传统方式存在两个核心问题:
问题一:非原子性操作
java
// 传统释放锁方式存在竞态条件
String id = redisTemplate.opsForValue().get("lock:order");
if (id.equals(currentThreadId)) {
redisTemplate.delete("lock:order");
}
问题二:网络开销大
每次Redis命令都需要一次网络往返(RTT),多个命令会累积大量网络延迟。
1.2 Lua脚本的三大优势
- 原子性保证:Redis将整个Lua脚本作为一个整体执行,执行期间不会响应其他客户端请求,确保脚本内所有命令要么全部成功,要么全部失败。
- 减少网络开销:多个命令封装为一个脚本,只需一次网络传输,在高并发场景下性能提升显著。
- 复杂逻辑封装:支持条件判断、循环等编程特性,实现原生命令无法完成的复杂业务逻辑。
1.3 Lua脚本基础语法
核心API:
redis.call():执行Redis命令,出错时抛出异常并停止脚本redis.pcall():执行Redis命令,出错时返回错误对象而不抛出异常
参数传递机制:
KEYS数组:存储所有被操作的Redis键名(必须显式声明)ARGV数组:存储非键名参数
二、分布式锁的原子性问题
2.1 传统分布式锁的问题
误删锁场景:
- 线程A持有锁,执行业务逻辑
- 锁超时自动释放
- 线程B获取锁
- 线程A恢复执行,删除锁(误删线程B的锁)
- 线程C获取锁,导致并发执行
根本原因:判断锁标识和删除锁是两个独立的Redis操作,中间可能被其他线程插入。

2.2 Lua脚本改造方案
释放锁的Lua脚本:
java
-- KEYS[1]: 锁的key
-- ARGV[1]: 当前线程标识
-- 获取锁中的线程标识
local id = redis.call('get', KEYS[1])
-- 比较线程标识与锁中的标识是否一致
if (id == ARGV[1]) then
-- 释放锁
return redis.call('del', KEYS[1])
end
-- 不一致则返回0
return 0
简化版脚本:
java
if (redis.call('get', KEYS[1]) == ARGV[1]) then
return redis.call('del', KEYS[1])
end
return 0
Java代码实现:
java
@Component
public class SimpleRedisLock implements Ilock {
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true);
// 加载Lua脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 调用Lua脚本释放锁
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId()
);
}
}
三、Lua脚本原子性原理
3.1 底层实现机制
单线程模型:Redis采用单线程事件循环模型,所有命令按FIFO顺序执行。当执行Lua脚本时,Redis会阻塞其他客户端请求,直到脚本执行完成。
脚本级事务封装:
- 全有全无执行:脚本内所有命令要么全部成功,要么全部失败
- 数据锁定机制:执行期间自动锁定脚本操作的所有key,防止其他客户端修改
执行流程:
- 客户端通过EVAL命令提交脚本
- Redis计算脚本的SHA1哈希用于缓存
- 设置CLIENT_LUA标志,阻塞事件循环
- 执行Lua脚本中的Redis命令
- 清除CLIENT_LUA标志,恢复事件循环
3.2 与MULTI/EXEC事务对比
| 特性 | Lua脚本 | MULTI/EXEC事务 |
|---|---|---|
| 原子性范围 | 整个脚本 | 事务块内命令 |
| 错误处理 | 脚本级错误导致全部回滚 | 单个命令错误不影响后续 |
| 性能开销 | 较低(单次网络往返) | 较高(命令队列维护) |
四、总结
通过Lua脚本改造分布式锁,我们实现了:
- 原子性保证:将判断锁标识和删除锁封装为一个原子操作
- 性能优化:减少网络往返次数,提升系统吞吐量
- 安全性提升:避免误删其他线程的锁,确保数据一致性
- 可扩展性:支持可重入、锁续期等高级特性
在实际项目中,建议优先使用Redisson等成熟框架,它们已经封装了完整的分布式锁功能,包括看门狗机制、可重入锁等特性,能够更好地满足生产环境需求。