Lua 脚本 + Redisson 使用 Lua 脚本执行 Redis 命令

本文最后更新于 2026-08-04 15:16

Lua 脚本 + Redisson 使用 Lua 脚本执行 Redis 命令

本文从 Lua 脚本基础讲起,介绍 Redis 中 EVAL 命令的使用方式,并深入分析 Redisson 分布式锁源码中 Lua 脚本的实际应用。

一、Lua 脚本基础

Lua 脚本教程参考:https://www.runoob.com/lua/lua-tutorial.html

Linux 环境搭建参考:https://www.runoob.com/lua/lua-environment.html

1.1 交互式编程

在命令行输入 lua 即可进入交互模式,逐行执行 Lua 代码。

1.2 脚本式编程

将 Lua 代码写入 .lua 文件,通过 lua script.lua 执行。


二、Redis 使用 Lua 脚本

自 Redis 2.6.0 起,可以通过 EVAL 命令对 Lua 脚本进行求值,内置了 Lua 编译/解释器。

2.1 使用 Lua 脚本的好处

优势 说明
减少网络开销 将多个请求通过脚本的形式一次发送,减少网络时延
原子操作 Redis 会将整个脚本作为一个整体执行,中间不会被其他命令插入,无需担心竞态条件,无需使用事务
复用 客户端发送的脚本会永久存在 Redis 中,其他客户端可以复用而不需要用代码完成相同的逻辑

2.2 EVAL 命令

命令格式:

1
EVAL script numkeys key [key...] arg [arg...]

参数说明:

参数 说明
script 一段 Lua 5.1 脚本程序,不必定义为 Lua 函数
numkeys 指定键名参数的个数
key [key...] 要操作的键,在 Lua 脚本中通过 KEYS[1], KEYS[2] 获取
arg [arg...] 附加参数,在 Lua 脚本中通过 ARGV[1], ARGV[2] 获取

实例:

1
2
3
4
5
# 设置 key
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name testValue

# 获取 key
eval "return redis.call('get',KEYS[1])" 1 name

2.3 Lua 脚本中调用 Redis 命令

方法 说明
redis.call() 返回值就是 Redis 命令执行的返回值;如果出错,则返回错误信息,不继续执行
redis.pcall() 返回值就是 Redis 命令执行的返回值;如果出错,则记录错误信息,继续执行

注意:在脚本中使用 return 语句将返回值返回给客户端,如果没有 return,则返回 nil

2.4 执行 Lua 脚本的操作步骤

注意:RDM(Redis Desktop Manager)的命令行不支持 Lua 脚本执行,需要使用 redis-cli

第一步:找到 Redis 安装目录(2.6.0 以前的版本无法使用 Lua 脚本)

第二步:找到 redis-cli,一般在 bin 目录下

1
find -name redis-cli

第三步:使用 redis-cli 连接到 Redis 服务

1
2
3
4
5
# 命令格式
./redis-cli -h <IP> -p <PORT> -a <PASSWORD>

# 实例
./redis-cli -h 127.0.0.1 -p 6379 -a your_password

第四步:连接后如果出现权限问题(报错:(error) NOAUTH Authentication required),使用 AUTH 重新输入密码:

1
AUTH your_password

第五步:执行 EVAL 命令

1
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name testValue

三、Redisson 分布式锁源码分析

3.1 分布式锁常用方式

1
2
3
4
5
6
7
RLock lock = redissonClient.getLock("lockKey");
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}

分布式锁代码一共分为四步:

  1. 加锁(是否支持重入)
  2. 锁续期
  3. 阻塞获取
  4. 释放锁

3.2 加锁

源码位置:org.redisson.RedissonLock#tryLockInnerAsync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit,
long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 判断 key 是否存在
"if (redis.call('exists', KEYS[1]) == 0) then " +
// 不存在则创建 hash,field 为线程标识,value 为重入次数
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 设置 key 过期时间(默认 30s)
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// key 存在且当前线程持有(可重入)
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 重入次数 +1
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 刷新过期时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// key 被其他线程持有,返回剩余过期时间
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}

加锁 Lua 脚本逻辑解读:

1
2
3
1. key 不存在 → 创建 hash,设置过期时间 → 返回 nil(加锁成功)
2. key 存在且当前线程持有 → 重入次数 +1,刷新过期时间 → 返回 nil(加锁成功)
3. key 被其他线程持有 → 返回 key 的剩余过期时间(加锁失败,进入等待)

3.3 锁续期

源码位置:org.redisson.RedissonBaseLock#renewExpirationAsync

1
2
3
4
5
6
7
8
9
10
11
12
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断 key 的 field 是否还是当前线程
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 续期:重新设置过期时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}

续期逻辑:如果当前线程仍持有锁,则刷新过期时间,返回 1;否则返回 0。

这就是 Redisson 的”看门狗”机制:默认每 10 秒(internalLockLeaseTime / 3)检查一次,如果锁还被持有就续期 30 秒。

3.4 阻塞获取

源码位置:org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly)
throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// 加锁成功直接返回
if (ttl == null) {
return;
}
// 订阅锁释放消息:redisson_lock__channel:{key}
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
while (true) {
// 再次尝试加锁
ttl = tryAcquire(-1, leaseTime, unit, threadId);
if (ttl == null) {
break;
}
// 等待锁释放消息
if (ttl >= 0) {
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
future.getNow().getLatch().acquire();
}
}
} finally {
// 取消订阅
unsubscribe(future, threadId);
}
}

阻塞获取逻辑

  1. 尝试加锁,成功则直接返回
  2. 失败则订阅锁的释放频道(redisson_lock__channel:{key}
  3. 进入循环:再次尝试加锁 → 等待锁释放通知 → 重复
  4. 获取到锁后取消订阅

3.5 释放锁

源码位置:org.redisson.RedissonLock#unlockInnerAsync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断是否是当前线程持有锁
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 重入次数 -1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 还有重入次数,只刷新过期时间
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
// 重入次数为 0,删除 key 并发布锁释放消息
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getName(), getChannelName()),
LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

释放锁 Lua 脚本逻辑解读:

1
2
3
1. 不是当前线程持有锁 → 返回 nil
2. 重入次数 -1 后仍 > 0 → 只刷新过期时间,返回 0
3. 重入次数 -1 后 = 0 → 删除 key,发布锁释放消息,返回 1

四、涉及的 Redis 命令汇总

数据类型 命令 含义 返回值 示例
Key exists 检查 key 是否存在 存在返回 1,否则返回 0 exists name
Key pexpire 设置 key 过期时间(毫秒) 成功返回 1 pexpire key 15000
Key del 删除 key 被删除 key 的数量 del key
Hash hincrby 为哈希表字段加上增量值 执行后的字段值 hincrby myhash field 1
Hash hexists 查看哈希表字段是否存在 存在返回 1,否则返回 0 hexists myhash field
发布订阅 publish 向频道发送消息 接收信息的订阅者数量 publish channel message

五、参考资源


Lua 脚本 + Redisson 使用 Lua 脚本执行 Redis 命令
https://your-project-name.pages.dev/2026/07/25/lua-redis-redisson/
作者
阿川
发布于
2026年7月25日
许可协议