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脚本中,KEYS[1]和上面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

相关推荐
Warren982 分钟前
Spring Boot 整合网易163邮箱发送邮件实现找回密码功能
数据库·vue.js·spring boot·redis·后端·python·spring
郭京京6 小时前
redis基本操作
redis·go
似水流年流不尽思念6 小时前
Redis 分布式锁和 Zookeeper 进行比对的优缺点?
redis·后端
郭京京6 小时前
go操作redis
redis·后端·go
杨杨杨大侠8 小时前
Spring AI 系列(一):Spring AI 基础概念与架构入门
人工智能·spring·架构
Warren988 小时前
Spring Boot 拦截器返回中文乱码的解决方案(附全局优化思路)
java·网络·spring boot·redis·后端·junit·lua
XXD啊9 小时前
Redis 从入门到实践:Python操作指南与核心概念解析
数据库·redis·python
A尘埃14 小时前
Spring Event 企业级应用
java·spring·event
一叶飘零_sweeeet19 小时前
SPI 机制深度剖析:Java、Spring、Dubbo 的服务发现哲学与实战指南
java·spring·dubbo