redis在Spring中应用相关
local key = KEYS[1];
local threadId = ARGV[1];
local releaseTime = ARGV[2];-- lockname不存在
if(redis.call('exists', key) == 0) thenredis.call('hset', key, threadId, '1');redis.call('expire', key, releaseTime);return 1;
end;-- 当前线程已id存在
if(redis.call('hexists', key, threadId) == 1) thenredis.call('hincrby', key, threadId, '1');redis.call('expire', key, releaseTime);return 1;
end;
return 0;
/*** 获取锁*/public String tryLock(String lockName, long releaseTime) {// 存入的线程信息的前缀String key = UUID.randomUUID().toString();// 执行脚本Long result = (Long) redisTemplate.execute(lockScript,Collections.singletonList(lockName),key + Thread.currentThread().getId(),releaseTime);if (result != null && result.intValue() == 1) {return key;} else {return null;}}
这里lua脚本中,KEYS[1]和上面execute参数对应关系是什么?同时 Collections.singletonList(lockName)解释下这个
好问题 👍,你这个 Lua 脚本和 Java 调用里有两个核心点需要搞清楚:
1️⃣ KEYS 和 ARGV 的对应关系
在 Redis 的 Lua 脚本执行机制里,参数分为两类:
KEYS
:传入的 key 列表(由 Redis 官方约定必须放在 KEYS,便于集群模式下路由到正确的槽位)ARGV
:传入的 普通参数(值、过期时间、线程 id 等)
调用时:
redisTemplate.execute(script, keys, args...)
keys
→ 会传给 Lua 脚本里的KEYS
数组args...
→ 会传给 Lua 脚本里的ARGV
数组
2️⃣ 你的例子对应关系
Lua 脚本:
local key = KEYS[1];
local threadId = ARGV[1];
Java 调用:
Long result = (Long) redisTemplate.execute(lockScript,Collections.singletonList(lockName), // 传 KEYSkey + Thread.currentThread().getId(), // ARGV[1]releaseTime // ARGV[2]
);
对应关系:
KEYS[1] = lockName
(就是 Redis 的锁名,比如"myLock"
)ARGV[1] = key + Thread.currentThread().getId()
(线程唯一标识,用来判断是不是自己的锁)ARGV[2] = releaseTime
(锁的过期时间,脚本里可能用来刷新 TTL)
3️⃣ Collections.singletonList(lockName)
的意思
在 Java 里:
Collections.singletonList(lockName)
- 创建一个 只包含一个元素的不可变 List
- 这里就是
[lockName]
因为 execute()
的第二个参数要求是一个 List<K>
,即 可能有多个 Redis key,所以就用 singletonList
来包装成一个单元素的 List。
✅ 总结
KEYS
对应 Java 里传的第二个参数(List 类型)ARGV
对应 Java 里后面跟着的可变参数(Object… args)Collections.singletonList(lockName)
:只是把一个锁名包装成单元素 List,以便传到 Lua 脚本的KEYS[1]
。
下图并发问题解释
很好 👍 你写的这个 1.0 版本的 Redis 分布式锁思路已经很对了:
tryLock()
:用SETNX + EXPIRE
实现加锁(Spring Data Redis 封装成setIfAbsent
)。unlock()
:解锁时通过 校验 value 来防止误删别人的锁。
但是小张指出的问题也对:
🚨 问题:解锁非原子性
你现在的 unlock
分两步:
get(lockName)
del(lockName)
这两个操作中间 不是原子操作。可能会发生:
- 线程 A 执行
get
,发现是自己锁,准备执行del
- 此时锁过期了,线程 B 成功
tryLock
,重新设置了锁 - 线程 A 继续执行
del
,把 线程 B 的锁给删掉了 ❌
这就是并发场景下的 锁误删问题。
✅ 解决方案:Lua 脚本
Redis 本身是单线程执行命令的,所以如果把 get
+ del
合并成一个原子脚本,就不会有问题。
Lua 脚本可以让 Redis 保证这两个操作在一次执行过程中不被打断。
典型 Lua 脚本:
if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])
elsereturn 0
end
含义:
KEYS[1]
:锁的 keyARGV[1]
:传入的 value(uuid)- 如果锁的 value 和传入的 uuid 一致 → 执行
del
删除 - 否则返回
0
,不做删除
这样整个检查 + 删除就是 Redis 内部原子执行,不会被其它线程打断。
📌 Spring Data Redis 使用 Lua 解锁
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setScriptText("if redis.call('get', KEYS[1]) == ARGV[1] then " +" return redis.call('del', KEYS[1]) " +"else " +" return 0 " +"end");UNLOCK_SCRIPT.setResultType(Long.class);
}public void unlock(String key, String uuid) {redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(key), uuid);
}
🎯 总结
- 1.0 版本的问题:
get + del
不是原子操作,可能误删他人锁。 - 用 Lua 脚本:在 Redis 内部一次性完成
比较 + 删除
,避免竞态条件。 - 这是分布式锁里 解锁安全性 的核心改进。
引用大佬原文: 掘金https://juejin.cn/post/6961380552519712798#heading-5