1. 程式人生 > >spring cache redis 高併發下返回null

spring cache redis 高併發下返回null

在使用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;
    }
}

嗯,完美解決,效能稍微下降了一點點,不過對業務系統來說穩定性最重要