You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Data Redis中CrudRepository查询偶尔返回null问题排查

Spring Data Redis CrudRepository查询偶尔返回null问题排查

环境与配置

Redis连接配置

redis:
  database: 1
  host: my.redis.com
  port: 6380
  password: redis
  timeout: 5000ms 
  lettuce:
    pool:
      max-active: 50 
      max-idle: 10 

Redis配置类

@Configuration
@Slf4j
@EnableCaching
public class RedisConfig {
    @Bean
    public RedisTemplate<String, byte[]> byteRedisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, byte[]> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(RedisSerializer.byteArray());
        return template;
    }
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
    {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //设置value和hashValue的序列化
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(
                Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
        serializer.setObjectMapper(om);
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashValueSerializer(serializer);
        //设置key和hashKey的序列化
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .disableCachingNullValues()
                .entryTtl(Duration.ofMinutes(10))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();

        return redisCacheManager;
    }

}

数据实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@RedisHash(RedisKeyPrefix.UNIT_CONNECT_STATUS)
@Builder
public class UnitConnectStatus implements Serializable {

    @Id
    private Integer unitId;
    private Integer unitType;
    private Boolean server2gateway;
    private Boolean gateway2unit;
}

Repository接口

public interface ConnectRepo extends CrudRepository<UnitConnectStatus,Integer> {
}

异步保存逻辑

仅通过带@Async注解的方法异步保存设备在线状态,未设置过期时间:

@Async
public <T extends Unit>void saveOnline(T unit){
    Assert.notNull(unit, "unit is null");
    Assert.notNull(unit.getId(), "unit id is null");
    connectRepo.save(UnitConnectStatus.builder()
            .unitId(unit.getId())
            .unitType(unit.getType())
            .server2gateway(true)
            .gateway2unit(true)
            .build());
}

测试代码与异常日志

测试方法循环查询指定ID的记录:

@Test
public void testCon(){
    for (int i = 0; i < 10000; i++) {
        log.info("{}",connectRepo.findById(56).orElse(null));
        Thread.sleep(200);
    }
}

测试日志显示查询偶尔返回null:

2024-01-26 11:39:48.361 [][] INFO  c.t.controller.TestLeila:20  - UnitConnectStatus(unitId=56, unitType=1, server2gateway=true, gateway2unit=true)
2024-01-26 11:39:48.582 [][] INFO  c.t.controller.TestLeila:20  - UnitConnectStatus(unitId=56, unitType=1, server2gateway=true, gateway2unit=true)
2024-01-26 11:39:48.803 [][] INFO  c.t.controller.TestLeila:20  - UnitConnectStatus(unitId=56, unitType=1, server2gateway=true, gateway2unit=true)
2024-01-26 11:39:49.023 [][] INFO  c.t.controller.TestLeila:20  - null
2024-01-26 11:39:49.235 [][] INFO  c.t.controller.TestLeila:20  - UnitConnectStatus(unitId=56, unitType=1, server2gateway=true, gateway2unit=true)
2024-01-26 11:39:49.453 [][] INFO  c.t.controller.TestLeila:20  - UnitConnectStatus(unitId=56, unitType=1, server2gateway=true, gateway2unit=true)
2024-01-26 11:39:49.673 [][] INFO  c.t.controller.TestLeila:20  - UnitConnectStatus(unitId=56, unitType=1, server2gateway=true, gateway2unit=true)
2024-01-26 11:39:49.906 [][] INFO  c.t.controller.TestLeila:20  - UnitConnectStatus(unitId=56, unitType=1, server2gateway=true, gateway2unit=true)

问题描述

确认项目中无删除RedisKeyPrefix.UNIT_CONNECT_STATUS相关Key的逻辑,也未设置过期时间,但findById偶尔返回null,请问原因是什么?


可能原因及解决办法

1. Spring Data Redis Hash存储的两步操作非原子

用@RedisHash注解的实体,Spring Data Redis执行save时会分两步执行:

  • 将实体数据写入对应的Hash键(例如UNIT_CONNECT_STATUS:56)
  • 将ID添加到总集合键UNIT_CONNECT_STATUS中

这两步没有原子性保证,如果在写入Hash之后、添加ID到集合之前触发查询,Spring Data Redis会先检查集合中是否存在该ID,发现不存在就直接返回null,哪怕Hash中已经有完整数据。

2. 多RedisTemplate的序列化冲突

你配置了两个RedisTemplate:byteRedisTemplate用字节数组序列化,redisTemplate用JSON序列化。Spring Data Redis的Repo默认会自动注入容器中的RedisTemplate,如果注入的是byteRedisTemplate,查询时无法正确反序列化JSON格式的实体,就会返回null。

3. Redis连接或超时问题

Redis配置的超时时间为5000ms,但如果Redis服务器负载过高、网络出现波动,查询命令可能超时,此时Spring Data Redis不会抛出异常,而是直接返回null。可以开启Redis客户端日志,查看是否有超时或命令执行失败的记录。

4. 异步方法的事务问题

如果saveOnline所在类被Spring事务管理,异步方法默认不会继承当前事务,可能出现保存操作还未完成,查询就已经执行的情况,导致查不到数据。


解决建议

  • 添加Redis操作日志:开启Spring Data Redis的DEBUG日志,查看每次save和findById对应的Redis命令,确认操作是否完整执行。
  • 原子化保存操作:使用Redis事务或Lua脚本,将写入Hash和更新集合的操作绑定在一起执行,避免中间状态被查询到。
  • 指定默认RedisTemplate:在redisTemplate的@Bean注解上添加@Primary,确保Repo使用的是JSON序列化的模板,避免序列化冲突。
  • 检查Redis服务器状态:查看Redis的慢查询日志、内存使用情况,确认服务器是否存在异常导致数据暂时不可用。

内容的提问来源于stack exchange,提问作者liang li

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 18:02:07