问题描述
最近我们用Spring Cache + redis来做缓存。在高并发下@Cacheable 注解返回的内容是null。查看了一下源代码,在使用注解获取缓存的时候,RedisCache的get方法会先去判断key是否存在,然后再去获取值。这了就有一个漏铜,当线程1判断了key是存在的,紧接着这个时候这个key过期了,这时线程1再去获取值的时候返回的是null。
RedisCache的get方法源码:
public RedisCacheElement get(final RedisCacheKey cacheKey) {
Assert.notNull(cacheKey, "CacheKey must not be null!");
// 判断Key是否存在
Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.exists(cacheKey.getKeyBytes());
}
});
if (!exists.booleanValue()) {
return null;
}
// 获取key对应的值
return new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
}
// 获取值
protected Object lookup(Object key) {
RedisCacheKey cacheKey = key instanceof RedisCacheKey ? (RedisCacheKey) key : getRedisCacheKey(key);
byte[] bytes = (byte[]) redisOperations.execute(new AbstractRedisCacheCallback<byte[]>(
new BinaryRedisCacheElement(new RedisCacheElement(cacheKey, null), cacheValueAccessor), cacheMetadata) {
@Override
public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
return connection.get(element.getKeyBytes());
}
});
return bytes == null ? null : cacheValueAccessor.deserializeIfNecessary(bytes);
}
解决方案
这个流程有问题,解决方案就是把这个流程倒过来,先去获取值,然后去判断这个key是否存在。不能直接用获取的值根据是否是NULL判断是否有值,因为Reids可能缓存NULL值。
重写RedisCache的get方法:
public RedisCacheElement get(final RedisCacheKey cacheKey) {
Assert.notNull(cacheKey, "CacheKey must not be null!");
RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.exists(cacheKey.getKeyBytes());
}
});
if (!exists.booleanValue()) {
return null;
}
return redisCacheElement;
}
完整实现(3步):
1、重写RedisCache的get方法
package com.test.config.redis;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheElement;
import org.springframework.data.redis.cache.RedisCacheKey;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.util.Assert;
/**
* 自定义的redis缓存
*
* @author xiesihua 2018-09-20
*/
public class CustomizedRedisCache extends RedisCache {
private final RedisOperations redisOperations;
private final byte[] prefix;
public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration) {
super(name, prefix, redisOperations, expiration);
this.redisOperations = redisOperations;
this.prefix = prefix;
}
/**
* 重写父类的get函数。
* 父类的get方法,是先使用exists判断key是否存在,不存在返回null,存在再到redis缓存中去取值。这样会导致并发问题,
* 假如有一个请求调用了exists函数判断key存在,但是在下一时刻这个缓存过期了,或者被删掉了。
* 这时候再去缓存中获取值的时候返回的就是null了。
* 可以先获取缓存的值,再去判断key是否存在。
*
* 处理方法:只是把源码中的代码挪了下顺序,先取值,再查
* 注意:不能把查询的一部省掉,里面有一个flush操作。省掉后,会空指针的
* @author xiesihua 2018-09-20
*
* @param cacheKey
* @return
*/
@Override
public RedisCacheElement get(final RedisCacheKey cacheKey) {
Assert.notNull(cacheKey, "CacheKey must not be null!");
RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.exists(cacheKey.getKeyBytes());
}
});
if (!exists.booleanValue()) {
return null;
}
return redisCacheElement;
}
}
2、重写RedisCacheManager
package com.test.config.redis;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;
/**
* 自定义的redis缓存管理器
* @author xiesihua 2018-09-20
*/
public class CustomizedRedisCacheManager extends RedisCacheManager {
public CustomizedRedisCacheManager(RedisOperations redisOperations) {
super(redisOperations);
}
@Override
protected Cache getMissingCache(String name) {
long expiration = computeExpiration(name);
return new CustomizedRedisCache(
name,
(this.isUsePrefix() ? this.getCachePrefix().prefix(name) : null),
this.getRedisOperations(),
expiration);
}
}
3、配置Redis管理器
@Configuration
public class RedisConfig {
@Value("${spring.redis.cache.expiration}")
private Long cacheDefaultExpiration;
@Bean
public CacheManager cacheManager(@Qualifier("cacheRedisTemplate") RedisTemplate redisTemplate) {
//改成使用自定义的redis缓存管理器,xiesihua,2018-09-20
RedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(redisTemplate);
redisCacheManager.setDefaultExpiration(cacheDefaultExpiration);
return redisCacheManager;
}
}
demo验证
【测试代码】
redis设置的默认过期时间为 30s
ok,开始验证啦~
【改源码之前,原redis配置,执行上面死循环,控制台的输出】
很明显,在高并发的情况下,会返回空指针。
再看下优化后的效果
【改源码之后,使用了前面贴的设置,执行上面死循环,控制台的输出】
验证通过啦!无限死循环,都不会报空了!