redis在Spring中应用相关

lua 复制代码
local key = KEYS[1];
local threadId = ARGV[1];
local releaseTime = ARGV[2];

-- lockname不存在
if(redis.call('exists', key) == 0) then
    redis.call('hset', key, threadId, '1');
    redis.call('expire', key, releaseTime);
    return 1;
end;

-- 当前线程已id存在
if(redis.call('hexists', key, threadId) == 1) then
    redis.call('hincrby', key, threadId, '1');
    redis.call('expire', key, releaseTime);
    return 1;
end;
return 0;
java 复制代码
    /**
     * 获取锁
     */
    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脚本中,KEYS1和上面execute参数对应关系是什么?同时 Collections.singletonList(lockName)解释下这个

好问题 👍,你这个 Lua 脚本和 Java 调用里有两个核心点需要搞清楚:


1️⃣ KEYS 和 ARGV 的对应关系

在 Redis 的 Lua 脚本执行机制里,参数分为两类

  • KEYS:传入的 key 列表(由 Redis 官方约定必须放在 KEYS,便于集群模式下路由到正确的槽位)
  • ARGV:传入的 普通参数(值、过期时间、线程 id 等)

调用时:

java 复制代码
redisTemplate.execute(script, keys, args...)
  • keys → 会传给 Lua 脚本里的 KEYS 数组
  • args... → 会传给 Lua 脚本里的 ARGV 数组

2️⃣ 你的例子对应关系

Lua 脚本:

lua 复制代码
local key = KEYS[1];
local threadId = ARGV[1];

Java 调用:

java 复制代码
Long result = (Long) redisTemplate.execute(
    lockScript,
    Collections.singletonList(lockName),   // 传 KEYS
    key + 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 里:

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 分两步:

  1. get(lockName)
  2. del(lockName)

这两个操作中间 不是原子操作。可能会发生:

  • 线程 A 执行 get,发现是自己锁,准备执行 del
  • 此时锁过期了,线程 B 成功 tryLock,重新设置了锁
  • 线程 A 继续执行 del,把 线程 B 的锁给删掉了

这就是并发场景下的 锁误删问题


✅ 解决方案:Lua 脚本

Redis 本身是单线程执行命令的,所以如果把 get + del 合并成一个原子脚本,就不会有问题。

Lua 脚本可以让 Redis 保证这两个操作在一次执行过程中不被打断。

典型 Lua 脚本:

lua 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

含义:

  • KEYS[1]:锁的 key
  • ARGV[1]:传入的 value(uuid)
  • 如果锁的 value 和传入的 uuid 一致 → 执行 del 删除
  • 否则返回 0,不做删除

这样整个检查 + 删除就是 Redis 内部原子执行,不会被其它线程打断。


📌 Spring Data Redis 使用 Lua 解锁

java 复制代码
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

相关推荐
咖啡八杯1 天前
GoF设计模式——备忘录模式
java·后端·spring·设计模式
vivo互联网技术2 天前
从 10 分钟到 1 秒:ES 深度分页任意跳页的三轮优化实战
服务器·数据库·redis·elasticsearch·深度分页
Flittly3 天前
【AgentScope Java新手村系列】(16)从RAG到多路检索
java·spring boot·spring
咖啡八杯3 天前
GoF设计模式——中介者模式
java·后端·spring·设计模式
用户3074596982075 天前
Redis 延时队列详解
redis
烤代码的吐司君5 天前
Redis 数据结构 ZSet, BIT, HyperLogLog,Geo 空间数据
redis·后端
Flittly5 天前
【AgentScope Java新手村系列】(14)人机交互
java·spring boot·spring
leeyi7 天前
Checkpoint 机制:Agent 怎么在断电后接着跑
redis·aigc·agent
云技纵横8 天前
一个 @Async 让循环依赖暴雷:Spring 代理的暗坑
redis