spring cache redis 高併發下返回null
阿新 • • 發佈:2018-11-13
在使用springdata操作快取中,當訪問量比較大時,有可能返回null導致資料不準確,發生機率在0.01%或以下,雖然已經低於壓測標準,但是還是會影響部分使用者,經過一番篩查,發現原因如下:
RedisCache 類中 有get方法,存在明顯的邏輯錯誤 “先判斷是否存在,再去get”,程式碼執行過程中總有時間差,如果這個時間過期,則 判定為存在,又取不到資料,所以發生了 本文所描述的情況
/** * Return the value to which this cache maps the specified key. * * @param cacheKey the key whose associated value is to be returned via its binary representation. * @return the {@link RedisCacheElement} stored at given key or {@literal null} if no value found for key. * @since 1.5 */ public RedisCacheElement get(final RedisCacheKey cacheKey) { Assert.notNull(cacheKey, "CacheKey must not be null!"); 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 new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey))); }
改進方法如下(網上很多寫法也有bug,所以自己稍微做了一點改動):
redis快取類:
package com.jinhuhang.risk.plugins.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 yuhao.wang */ 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; } public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration, boolean allowNullValues) { super(name, prefix, redisOperations, expiration, allowNullValues); this.redisOperations = redisOperations; this.prefix = prefix; } /** * 重寫父類的get函式。 * 父類的get方法,是先使用exists判斷key是否存在,不存在返回null,存在再到redis快取中去取值。這樣會導致併發問題, * 假如有一個請求呼叫了exists函式判斷key存在,但是在下一時刻這個快取過期了,或者被刪掉了。 * 這時候再去快取中獲取值的時候返回的就是null了。 * 可以先獲取快取的值,再去判斷key是否存在。 * * @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))); if(redisCacheElement.get()==null)//如果取出來的值為空 ,則直接返回null return null; 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; } /** * 獲取RedisCacheKey * * @param key * @return */ private RedisCacheKey getRedisCacheKey(Object key) { return new RedisCacheKey(key).usePrefix(this.prefix) .withKeySerializer(redisOperations.getKeySerializer()); } }
cacheManager:
package com.jinhuhang.risk.plugins.redis; import java.util.Collection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.Cache; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.core.RedisOperations; /** * 自定義的redis快取管理器 * @author yuhao.wang */ public class CustomizedRedisCacheManager extends RedisCacheManager { private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCacheManager.class); public CustomizedRedisCacheManager(RedisOperations redisOperations) { super(redisOperations); } public CustomizedRedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) { super(redisOperations, cacheNames); } @Override protected Cache getMissingCache(String name) { long expiration = computeExpiration(name); return new CustomizedRedisCache( name, (this.isUsePrefix() ? this.getCachePrefix().prefix(name) : null), this.getRedisOperations(), expiration); } }
配置類:
package com.jinhuhang.risk.plugins;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jinhuhang.risk.plugins.redis.CustomizedRedisCacheManager;
import com.jinhuhang.risk.util.JedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.query.RedisOperationChain;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Configuration
public class JedisConfiguration {
@Autowired
private JedisProperties jedisProperties;
@Bean
public JedisCluster jedisCluster() {
List<String> nodes = jedisProperties.getCluster().getNodes();
Set<HostAndPort> hps = new HashSet<>();
for (String node : nodes) {
String[] hostPort = node.split(":");
hps.add(new HostAndPort(hostPort[0].trim(), Integer.valueOf(hostPort[1].trim())));
}
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(jedisProperties.getPool().getMaxIdle());
poolConfig.setMinIdle(jedisProperties.getPool().getMinIdle());
poolConfig.setMaxWaitMillis(jedisProperties.getPool().getMaxWait());
poolConfig.setMaxTotal(jedisProperties.getMaxTotal());
JedisCluster jedisCluster1;
if (1 == jedisProperties.getIsAuth()) {
jedisCluster1 = new JedisCluster(
hps,
jedisProperties.getTimeout(),
jedisProperties.getSoTimeout(),
jedisProperties.getMaxAttempts(),
jedisProperties.getPassword(),
poolConfig);
} else {
jedisCluster1 = new JedisCluster(
hps,
jedisProperties.getTimeout(),
jedisProperties.getSoTimeout(),
poolConfig);
}
JedisUtil.setJedisCluster(jedisCluster1);
return jedisCluster1;
}
/**
* 設定資料存入redis 的序列化方式
*</br>redisTemplate序列化預設使用的jdkSerializeable,儲存二進位制位元組碼,導致key會出現亂碼,所以自定義
*序列化類
*
* @paramredisConnectionFactory
*/
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper =new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
RedisCacheManager rcm = new CustomizedRedisCacheManager(redisTemplate);
// 設定快取過期時間,單位:秒
rcm.setDefaultExpiration(60L);
return rcm;
}
}
嗯,完美解決,效能稍微下降了一點點,不過對業務系統來說穩定性最重要