Spring Boot项目中使用RedisTemplate.delete() 删除指定key失败
问题现象
最近收到运维报过来的一个问题:
redis中有大量的redis分布式锁的key存在,不过期,一直在持续堆积。
排查过程
看了代码发现,目前实现的方案是,在方法块执行完成后手动释放锁,将锁的key执行删除。
try {
//要执行的方法逻辑
}finally {
redisLock.unlock();
}
很明显,这里的删除应该是没有执行成功。
unlock方法的逻辑如下:
public synchronized void unlock() {
if (locked) {
redisTemplate.delete(lockKey);
locked = false;
}
}
本地调试发现执行redisTemplate.delete返回值为false,同时这个时候执行get确实还能获取到对应的Key。
很莫名奇妙,通过排查,包括阅读redisTemplate.delete的源码,发现是序列化不一致的问题:
public Boolean delete(K key) {
byte[] rawKey = this.rawKey(key);
Long result = (Long)this.execute((connection) -> {
return connection.del(new byte[][]{rawKey});
}, true);
return result != null && result.intValue() == 1;
}
private byte[] rawKey(Object key) {
Assert.notNull(key, "non null key required");
return this.keySerializer == null && key instanceof byte[] ? (byte[])((byte[])key) : this.keySerializer.serialize(key);
}
Redis 的默认序列化机制 “ defaultSerializer ” ,如果没有自定义的序列化机制,则系统默认使用的是 “ org.springframework.data.redis.serializer.JdkSerializationRedisSerializer ”
这里一般大家检查两个地方:
1. RedisConfig,自定义了 Redis 的序列化机制
这里要重点说一下,好多同学不管是从网上帖子拿来主义也好,还是自己积累了固定的RedisLock工具类,在不同的系统之间搬运,一般系统中前人都已自定义了 Redis 的序列化机制。
2. 在redis加锁时执行set逻辑的序列化机制
tips:目前我的系统里用的是这个逻辑,导致网上搜了一下解决方案还是没能解决。
是因为在执行lock,进行setNX操作时,重写setNX,自定义了序列化方法:
问题原因
没有删除成功的根本原因:
此时不能使用原生的 “ RedisTemplate redisTemplate; ” 而需要定义为泛型的 “ RedisTemplate <Object,Object> redisTemplate; ” 。
因为当我们再次新增的 key 的时候,使用的是 “ StringRedisSerializer ”序列化机制,
但是在 delete 操作的时候是使用的是原生 API ,redis 中的 redisTemplate 默认序列化机制采用的是 “ JdkSerializationRedisSerializer ”,
这样一来,即使你使用 hasKey 方法也会发现 redis 中存在这个 key ,但是实际 hasKey 返回 false,所以就会出现删除成功,但是实际的数据依然存在 Redis 服务器上。
解决方案
/**
* 锁释放
*/
public synchronized void unlock() {
if (locked) {
boolean result = this.delete(lockKey);
if (result){
logger.info("lockKey 删除成功,lockKey:{}",lockKey);
}else {
logger.info("lockKey 删除失败,lockKey:{}",lockKey);
}
locked = false;
}
}
/**
* 按key删除redis lock
* @param key
* @return
*/
private Boolean delete(String key) {
byte[] rawKey = this.rawString(key);
Long result = (Long)redisTemplate.execute((connection) -> {
return connection.del(new byte[][]{rawKey});
}, true);
return result != null && result.intValue() == 1;
}
private byte[] rawString(String key) {
StringRedisSerializer serializer = new StringRedisSerializer();
return serializer.serialize(key);
}
我个人比较建议这种方案,setNx和delete都重写,并且单独定义序列化机制,来保持一致,
这样不再受限于系统原本的Redis自定义序列化机制。
方便迁移复用。
问题引申
问题解决了,大家还会有疑问,为什么不给redis锁的key加上自动过期时间。
目前我没有进行复杂的实现的想法是:
1.在锁定的方法块的finally进行手动释放清除key,偶尔因为锁异常或者远程连接失败,导致redis的key值未删除成功的也在可接受范围。
2.那有可能因为锁异常导致死锁的情况,目前处理的机制是, 把过期时间作为key的value:
转载自:https://juejin.cn/post/7324624772978327564