本文最后更新于 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
| eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name testValue
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 目录下
第三步:使用 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 重新输入密码:
第五步:执行 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(); }
|
分布式锁代码一共分为四步:
- 加锁(是否支持重入)
- 锁续期
- 阻塞获取
- 释放锁
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, "if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "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, "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; } 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); } }
|
阻塞获取逻辑:
- 尝试加锁,成功则直接返回
- 失败则订阅锁的释放频道(
redisson_lock__channel:{key})
- 进入循环:再次尝试加锁 → 等待锁释放通知 → 重复
- 获取到锁后取消订阅
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; " + "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + "if (counter > 0) then " + "redis.call('pexpire', KEYS[1], ARGV[2]); " + "return 0; " + "else " + "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 |
五、参考资源