bugfix: 如果RedisTemplate指定KeySerializer,可能会导致加锁成功,无法解锁#3890
bugfix: 如果RedisTemplate指定KeySerializer,可能会导致加锁成功,无法解锁#3890GuoHaoZai wants to merge 1 commit intobinarywang:developfrom
Conversation
There was a problem hiding this comment.
Pull request overview
该 PR 旨在修复 RedisTemplate 配置自定义 keySerializer(例如给 key 增加前缀)时,分布式锁在“加锁成功但解锁失败”的不一致问题,确保锁的 key 序列化行为一致,避免因实际 Redis key 不同而无法删除锁。
Changes:
- 调整
RedisTemplateSimpleDistributedLock#unlock()的解锁实现,改为通过底层connection.eval(...)执行 Lua 脚本删除 key - 使用
executePipelined(...)包裹解锁脚本调用
| 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; | ||
| }); |
There was a problem hiding this comment.
当前实现新增了 RedisCallback/ReturnType 的使用,但文件顶部仍保留了 DefaultRedisScript/RedisScript/Collections 等旧实现的 import;同时缺少 RedisCallback、ReturnType 等必要 import。未清理/补全 import 会导致编译或 Checkstyle 直接失败。建议补齐需要的 import 并移除不再使用的 import。
| 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; | ||
| }); |
There was a problem hiding this comment.
这里用 executePipelined 只执行一次 eval,既不需要 pipeline,也会引入额外的连接/结果处理开销且返回值被忽略。建议改用 redisTemplate.execute(...)(RedisCallback 或 RedisScript 方式均可),并按需处理脚本返回值。
| 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(); | ||
| } |
There was a problem hiding this comment.
valueThreadLocal.remove() 目前不在 finally 中;如果 connection.eval 或 executePipelined 在运行时抛异常(连接问题、脚本执行错误等),ThreadLocal 可能残留在线程池线程上,导致后续同线程的锁语义异常/内存泄漏风险。建议用 try/finally 确保无论解锁是否成功都清理 ThreadLocal。
| 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; |
There was a problem hiding this comment.
这里使用 key.getBytes()/valueThreadLocal.get().getBytes() 会绕过 StringRedisTemplate 的 key/value serializer(尤其是自定义 keySerializer 会加前缀的场景),可能再次导致“加锁/解锁使用的 Redis key 不一致”并解锁失败。建议改为使用 redisTemplate 的序列化器对 key/value 做 serialize(或恢复使用 RedisScript + redisTemplate.execute 的写法,以保持与 opsForValue() 一致的序列化行为)。
当redisTemplate指定keySerializer,且keySerializer中会对redis key添加自定义前缀时,RedisStringCommands#get/set方法会忽略keySerializer,而StringRedisTemplate#execute不会忽略keySerializer,所以会导致加锁成功,而解锁失败(由于redis key不一样)