这次改造的核心目标是:把「解锁时的非原子操作」改成「原子操作」,彻底解决分布式锁的「判断 + 删除」时序问题。
一、先回顾:旧版解锁的致命缺陷
旧版解锁逻辑:
public void unlock() { // 1. 获取当前线程标识 String threadId = ID_PREFIX + Thread.currentThread().getId(); // 2. 从Redis获取锁标识(get操作) String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); // 3. 判断标识是否一致 if (threadId.equals(id)) { // 4. 删除锁(delete操作) stringRedisTemplate.delete(KEY_PREFIX + name); } }问题出在:get和delete是两次独立的 Redis 命令,中间存在时间窗口。
二、用 Lua 脚本实现原子解锁
Lua 脚本的特性:Redis 会将整个脚本作为一个原子操作执行,中间不会被其他命令打断。
新增 Lua 脚本配置
// 定义解锁脚本 private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; static { UNLOCK_SCRIPT = new DefaultRedisScript<>(); // 加载resources下的unlock.lua文件 UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); // 指定脚本返回值类型 UNLOCK_SCRIPT.setResultType(Long.class); }DefaultRedisScript:Spring Data Redis 提供的脚本执行器static静态块:类加载时就加载脚本,避免每次解锁都重新读取文件unlock.lua:存储解锁逻辑的 Lua 脚本文件
替换 Java 解锁逻辑为 Lua 脚本调用
@Override public void unlock() { // 调用Lua脚本执行解锁 stringRedisTemplate.execute( UNLOCK_SCRIPT, // 脚本对象 Collections.singletonList(KEY_PREFIX + name), // KEYS参数:锁的key ID_PREFIX + Thread.currentThread().getId() // ARGV参数:当前线程标识 ); }1、stringRedisTemplate.execute(...)
作用:Spring 提供的执行 Redis Lua 脚本的核心方法
特点:Redis 会把整个脚本当作一条原子命令执行,不会被其他命令打断
2、UNLOCK_SCRIPT
提前加载好的Lua 解锁脚本
3.Collections.singletonList(KEY_PREFIX + name)
传给 Lua 的KEYS 数组参数
内容:
lock:order:userId(锁的 Redis Key)必须用 List 集合,是 Redis 脚本的参数规范
4.ID_PREFIX + Thread.currentThread().getId()
传给 Lua 的ARGV 参数
内容:当前线程的唯一标识(UUID + 线程 ID)
用于 Lua 脚本判断:这把锁是不是当前线程加的
三、unlock.lua脚本内容
-- 比较线程标示与锁中的标示是否一致 if (redis.call('get', KEYS[1]) == ARGV[1]) then -- 释放锁:标识一致,删除锁 return redis.call('del', KEYS[1]) end -- 标识不一致,直接返回 return 0redis.call('get', KEYS[1]):获取锁的标识(原子操作)== ARGV[1]:和当前线程的标识对比(原子操作)redis.call('del', KEYS[1]):如果一致,删除锁(原子操作)