spring cache redis 高并发下返回null

本文针对SpringData操作Redis缓存时并发访问导致的数据准确性问题,详细解析了原有RedisCache类get方法存在的逻辑缺陷,并提供了一种改进方案,通过调整get方法的执行顺序来避免并发读取空值的问题,确保了业务系统的稳定性和数据一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

嗯,完美解决,性能稍微下降了一点点,不过对业务系统来说稳定性最重要

<think>嗯,用户问的是Redis中getCacheObject方法返回null的原因和解决方法。首先,我需要回忆一下之前用户提供的引用内容,特别是引用[1]和[2],里面提到了缓存击穿、空值处理和逻辑错误的情况。 首先,用户提到getCacheObject返回null,可能有很多原因。常见的比如缓存未命中,这时候数据确实不在缓存里,可能没有设置或者过期了。然后,引用[1]里的代码示例显示,RedisCache类的get方法存在先检查存在再获取的逻辑,这中间有时间差,如果缓存刚好在这时过期,就会返回null。这种情况属于缓存击穿吗?还是缓存穿透? 然后,引用[2]里提到对空值进行缓存,设置短过期时间,这样可以避免频繁查询数据库。比如当数据库查询结果为空时,缓存一个null,并设置5分钟过期,这样下次请求就不会直接打到数据库。所以用户的问题可能涉及这种情况,是否getCacheObject没有处理空值缓存? 另外,缓存雪崩也有可能,如果大量key同时过期,导致请求直接访问数据库,但返回null的情况可能更偏向于缓存击穿,即某个热点key过期后,高并发请求同时访问数据库。 还有可能是序列化问题,比如存储和读取时使用的序列化方式不一致,导致反序列化失败,返回null。比如引用[3]提到Jackson和FastJson的不同版本,可能配置错误导致。 还有,内存淘汰策略,当内存不足时,Redis自动删除一些key,导致缓存丢失。这时候需要检查内存使用情况和淘汰策略设置。 再考虑代码逻辑问题,比如是否在写入缓存时出现异常,导致数据没有正确存入,或者在获取时key生成规则不一致,导致无法正确读取。引用[4]里的工具类封装可能涉及到key的生成方式,需要检查是否正确。 解决方案方面,首先是空值缓存,即使数据库查询为空,也缓存null并设置过期时间,避免穿透。然后,缓存预热,在系统启动时加载热点数据。使用互斥锁或分布式锁防止缓存击穿,保证只有一个线程去查数据库。检查序列化配置是否一致,确保读写使用相同的序列化方式。调整内存淘汰策略,比如LRU或TTL,避免关键数据被删除。还可以用布隆过滤器过滤无效请求,防止大量不存在的key查询。 需要结合用户的代码示例,比如引用[1]中的get方法逻辑是否有问题,是否存在先检查存在再获取导致的竞态条件。用户提到的getCacheObject可能也存在类似的逻辑错误,需要检查是否存在类似的“先检查存在再获取”的问题,导致在检查后、获取前缓存过期,从而返回null。 总结起来,可能的原因包括缓存未命中、缓存击穿、缓存穿透、序列化问题、内存淘汰、代码逻辑问题等。解决方法对应每个原因进行处理,比如空值缓存、互斥锁、缓存预热、检查序列化配置、调整内存策略、修复代码逻辑等。</think>### Redis的getCacheObject返回null原因及解决方法 #### 一、可能原因分析 1. **缓存未命中** - 数据未加载到缓存中(如首次访问、缓存预热未完成) - 缓存已过期或主动删除(如设置了TTL或手动执行`DEL`命令)[^2] 2. **缓存击穿(Hot Key失效)** - 高并发场景下,某个热点Key过期后大量请求直接穿透到数据库[^2] 3. **缓存穿透(无效Key请求)** - 频繁查询数据库中不存在的数据(如恶意攻击或参数异常),导致缓存未存储无效Key[^2] 4. **序列化/反序列化问题** - 缓存存储与读取时使用的序列化方式不一致(如Jackson与FastJson混用)[^3] - 反序列化失败导致返回`null`(如字段类型不匹配) 5. **内存淘汰策略触发** - Redis内存不足时,根据`maxmemory-policy`自动淘汰部分Key[^2] 6. **代码逻辑缺陷** - 存在类似“先检查Key存在,再获取值”的竞态条件(如引用[1]中的问题) --- #### 二、解决方案 ##### 1. 空值缓存策略 ```java public Object getCacheObjectWithNull(String key) { Object value = redisTemplate.opsForValue().get(key); if (value == null) { value = db.query(key); // 数据库查询 redisTemplate.opsForValue().set(key, value, 300, TimeUnit.SECONDS); // 缓存空值 } return value; } ``` - **作用**:即使数据库返回`null`,仍缓存空值并设置短过期时间(如300秒),避免重复穿透[^2] ##### 2. 缓存击穿防护 ```java public Object getWithMutex(String key) { Object value = redisTemplate.opsForValue().get(key); if (value == null) { synchronized (this) { // 分布式环境下需用Redis分布式锁 value = redisTemplate.opsForValue().get(key); if (value == null) { value = db.query(key); redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES); } } } return value; } ``` - **作用**:通过互斥锁限制单一线程重建缓存 ##### 3. 布隆过滤器 ```java // 初始化布隆过滤器 BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 1000000, 0.01); public Object getWithBloomFilter(String key) { if (!bloomFilter.mightContain(key)) { return null; // 直接拦截无效请求 } return redisTemplate.opsForValue().get(key); } ``` - **作用**:拦截明显无效的Key请求[^2] ##### 4. 序列化配置检查 确保RedisTemplate配置一致性: ```yaml spring: redis: host: localhost port: 6379 lettuce: pool: max-active: 8 # 使用Jackson序列化 value-default-type: com.example.Entity ``` ```java @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setKeySerializer(RedisSerializer.string()); template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); return template; } ``` ##### 5. 内存优化策略 - 调整`redis.conf`配置: ```conf maxmemory 2gb maxmemory-policy allkeys-lru # 优先淘汰最近最少使用的Key ``` --- #### 三、排查步骤 1. **检查Key是否存在** ```bash redis-cli EXISTS your_key ``` 2. **查看TTL剩余时间** ```bash redis-cli TTL your_key ``` 3. **验证序列化数据** ```bash redis-cli GET your_key # 检查原始数据格式 ``` 4. **监控内存使用** ```bash redis-cli INFO memory ``` ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值