Skip to content

bugfix: 如果RedisTemplate指定KeySerializer,可能会导致加锁成功,无法解锁#3890

Open
GuoHaoZai wants to merge 1 commit intobinarywang:developfrom
GuoHaoZai:develop
Open

bugfix: 如果RedisTemplate指定KeySerializer,可能会导致加锁成功,无法解锁#3890
GuoHaoZai wants to merge 1 commit intobinarywang:developfrom
GuoHaoZai:develop

Conversation

@GuoHaoZai
Copy link

@GuoHaoZai GuoHaoZai commented Feb 27, 2026

当redisTemplate指定keySerializer,且keySerializer中会对redis key添加自定义前缀时,RedisStringCommands#get/set方法会忽略keySerializer,而StringRedisTemplate#execute不会忽略keySerializer,所以会导致加锁成功,而解锁失败(由于redis key不一样)

@GuoHaoZai GuoHaoZai changed the title bugfix: 如果redisTemplate指定redis prefix,会导致加锁成功,无法解锁 bugfix: 如果RedisTemplate指定KeySerializer,可能会导致加锁成功,无法解锁 Feb 27, 2026
@binarywang binarywang requested a review from Copilot February 28, 2026 02:40
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 旨在修复 RedisTemplate 配置自定义 keySerializer(例如给 key 增加前缀)时,分布式锁在“加锁成功但解锁失败”的不一致问题,确保锁的 key 序列化行为一致,避免因实际 Redis key 不同而无法删除锁。

Changes:

  • 调整 RedisTemplateSimpleDistributedLock#unlock() 的解锁实现,改为通过底层 connection.eval(...) 执行 Lua 脚本删除 key
  • 使用 executePipelined(...) 包裹解锁脚本调用

Comment on lines +96 to +100
redisTemplate.executePipelined((RedisCallback<String>) connection -> {
connection.eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end".getBytes(),
ReturnType.INTEGER, 1, key.getBytes(), valueThreadLocal.get().getBytes());
return null;
});
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前实现新增了 RedisCallback/ReturnType 的使用,但文件顶部仍保留了 DefaultRedisScript/RedisScript/Collections 等旧实现的 import;同时缺少 RedisCallback、ReturnType 等必要 import。未清理/补全 import 会导致编译或 Checkstyle 直接失败。建议补齐需要的 import 并移除不再使用的 import。

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +100
redisTemplate.executePipelined((RedisCallback<String>) connection -> {
connection.eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end".getBytes(),
ReturnType.INTEGER, 1, key.getBytes(), valueThreadLocal.get().getBytes());
return null;
});
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用 executePipelined 只执行一次 eval,既不需要 pipeline,也会引入额外的连接/结果处理开销且返回值被忽略。建议改用 redisTemplate.execute(...)(RedisCallback 或 RedisScript 方式均可),并按需处理脚本返回值。

Copilot uses AI. Check for mistakes.
Comment on lines 94 to 102
public void unlock() {
if (valueThreadLocal.get() != null) {
// 提示: 必须指定returnType, 类型: 此处必须为Long, 不能是Integer
RedisScript<Long> script = new DefaultRedisScript<>("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class);
redisTemplate.execute(script, Collections.singletonList(key), valueThreadLocal.get());
redisTemplate.executePipelined((RedisCallback<String>) connection -> {
connection.eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end".getBytes(),
ReturnType.INTEGER, 1, key.getBytes(), valueThreadLocal.get().getBytes());
return null;
});
valueThreadLocal.remove();
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valueThreadLocal.remove() 目前不在 finally 中;如果 connection.eval 或 executePipelined 在运行时抛异常(连接问题、脚本执行错误等),ThreadLocal 可能残留在线程池线程上,导致后续同线程的锁语义异常/内存泄漏风险。建议用 try/finally 确保无论解锁是否成功都清理 ThreadLocal。

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +99
redisTemplate.executePipelined((RedisCallback<String>) connection -> {
connection.eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end".getBytes(),
ReturnType.INTEGER, 1, key.getBytes(), valueThreadLocal.get().getBytes());
return null;
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里使用 key.getBytes()/valueThreadLocal.get().getBytes() 会绕过 StringRedisTemplate 的 key/value serializer(尤其是自定义 keySerializer 会加前缀的场景),可能再次导致“加锁/解锁使用的 Redis key 不一致”并解锁失败。建议改为使用 redisTemplate 的序列化器对 key/value 做 serialize(或恢复使用 RedisScript + redisTemplate.execute 的写法,以保持与 opsForValue() 一致的序列化行为)。

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants