记一次springboot 中使用redis分布式锁引发的问题
我们知道spring redis为我们提供了两个非常有用的模板:RedisTemplate,StringRedisTemplate。
1.主要分析一下RedisTemplate,我们主要看一下key和value使用的是什么序列化。
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (defaultSerializer == null) {
// 很明显,默认的序列化使用的是JDK的序列化。
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
if (enableDefaultSerializer) {
// 没有指定的情况下,key 和value都是使用默认的序列化
if (keySerializer == null) {
keySerializer = defaultSerializer;
defaultUsed = true;
}
if (valueSerializer == null) {
valueSerializer = defaultSerializer;
defaultUsed = true;
}
if (hashKeySerializer == null) {
hashKeySerializer = defaultSerializer;
defaultUsed = true;
}
if (hashValueSerializer == null) {
hashValueSerializer = defaultSerializer;
defaultUsed = true;
}
}
if (enableDefaultSerializer && defaultUsed) {
Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
}
if (scriptExecutor == null) {
this.scriptExecutor = new DefaultScriptExecutor<K>(this);
}
initialized = true;
}
由上面的代码,我们可以知道当我们使用RedisTemplate时,如果没有指定序列化,会使用默认的序列化(jdk提供的序列化,这里不说这个效率和码流大小的问题)。
那么问题来了,经过我们封装的redis工具类的SetNX代码是这样的:
/**
* 实现分布式锁
* <p>
* 只有key不存在的情况下才会操作,否则不做任何操作
*
* @param key
* @param value
* @param expireSecond
* 过期时间
* @return
*/
@SuppressWarnings("unchecked")
public boolean setNX(final String key, final Object value, long expireSecond) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
// 注意:这里key使用String的org.springframework.data.redis.serializer.StringRedisSerializer这个序列化
// value使用JdkSerializationRedisSerializer(jdk的序列化)
StringRedisSerializer serializer = new StringRedisSerializer();
JdkSerializationRedisSerializer js = new JdkSerializationRedisSerializer();
Boolean success = connection.setNX(serializer.serialize(key), js.serialize(value));
connection.close();
return success;
}
});
// 设置过期
if (expireSecond > 0 && obj != null && (Boolean) obj) {
redisTemplate.expire(key, expireSecond, TimeUnit.SECONDS);
}
} catch (Exception e) {
log.error("setNX redis error, key : {}" + key + ",e:" + e.getMessage());
}
return obj != null ? (Boolean) obj : false;
}
从上面的配置,我们可以看出了问题,SetNX方法中key使用的序列化跟模板中默认的序列化不一致。这样会导致设置的key无法过期(过期的key不被删除)。这里拓展一下:为什么会到导致key不被删除呢?我们知道redis中key过期删除的策略有三种:
1.定时删除: 在设置键的过期时间的同时,创建一个定时器,让定时器执行对键的删除操作
2.惰性删除: 每次取的时候先判断 expires 对象里面的键是否已经过期,如果过期,则删除键,否则,返回该键
3.定期删除: 每隔一段时间,程序对数据库遍历检查一遍,然后删除过期的键。
由于key的序列化不一致,就导致了redis在删除键的时候找不到(equals返回false),所以就出现了过期的key一直存在。
从而导致某个key的锁一直有效。试想一下,如果我们为了锁定一个订单是否有重复提交或者多个请求只处理一个请求的时候,当第一个请求处理,锁了该订单号,然后去保存数据库(假如参数格式不对,导致保存数据库失败)。那么修改好参数后,想再次重新保存,但是由于锁一直有效,设置不了过期时间。所以第二次保存的时候一直保存不了。
解决这个问题可以重写一下RedisTemplate的key序列化。或者修改一下SetNX方法中key的序列化为jdk序列化即可。
方法1:增加一个redis配置类
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
@Primary
public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {
return new RedisCacheManager(redisTemplate);
}
/**
* 模板key的序列化使用StringRedisSerializer,value使用默认
*
* @param factory 链接工厂
* @return 新的RedisTemplate bean
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
return template;
}
}
方法2:修改setNX的key序列化,保证和RedisTemplate模板中的一致
public boolean setNX(final String key, final Object value, long expireSecond) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
JdkSerializationRedisSerializer js = new JdkSerializationRedisSerializer();
Boolean success = connection.setNX(js.serialize(key), js.serialize(value));
connection.close();
return success;
}
});
// 设置过期
if (expireSecond > 0 && obj != null && (Boolean) obj) {
redisTemplate.expire(key, expireSecond, TimeUnit.SECONDS);
}
} catch (Exception e) {
log.error("setNX redis error, key : {}" + key + ",e:" + e.getMessage());
}
return obj != null ? (Boolean) obj : false;
}