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

