Redis缓存无法读取集合:EAGER加载仍重复查库求助
我们的应用使用Redis作为缓存存储,通过@Cacheable注解结合Redis实现缓存,相关配置及代码如下:
Redis配置
@Configuration @EnableCaching @RequiredArgsConstructor public class RedisConfig implements CachingConfigurer { @Value("${spring.cache.redis.time-to-live}") Long redisTTL; @Bean public RedisCacheConfiguration cacheConfiguration(ObjectMapper objectMapper) { objectMapper = objectMapper.copy(); objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); objectMapper.registerModules(new JavaTimeModule(), new Hibernate5Module()) .setSerializationInclusion(JsonInclude.Include.NON_NULL) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(redisTTL)) .disableCachingNullValues() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper))); } @Bean public RedissonClient reddison(@Value("${spring.redis.host}") final String redisHost, @Value("${spring.redis.port}") final int redisPort, @Value("${spring.redis.cluster.nodes}") final String clusterAddress, @Value("${spring.redis.use-cluster}") final boolean useCluster, @Value("${spring.redis.timeout}") final int timeout) { Config config = new Config(); if (useCluster) { config.useClusterServers().addNodeAddress(clusterAddress).setTimeout(timeout); } else { config.useSingleServer().setAddress(String.format("redis://%s:%d", redisHost, redisPort)).setTimeout(timeout); } return Redisson.create(config); } @Bean public RedissonConnectionFactory redissonConnectionFactory(RedissonClient redissonClient) { return new RedissonConnectionFactory(redissonClient); } @Bean public RedisCacheManager cacheManager(RedissonClient redissonClient, ObjectMapper objectMapper) { this.redissonConnectionFactory(redissonClient).getConnection().flushDb(); RedisCacheManager redisCacheManager= RedisCacheManager.builder(this.redissonConnectionFactory(redissonClient)) .cacheDefaults(this.cacheConfiguration(objectMapper)) .build(); redisCacheManager.setTransactionAware(true); return redisCacheManager; } @Override public CacheErrorHandler errorHandler() { return new RedisCacheErrorHandler(); } @Slf4j public static class RedisCacheErrorHandler implements CacheErrorHandler { @Override public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { log.info("Unable to get from cache " + cache.getName() + " : " + exception.getMessage()); } @Override public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) { log.info("Unable to put into cache " + cache.getName() + " : " + exception.getMessage()); } @Override public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) { log.info("Unable to evict from cache " + cache.getName() + " : " + exception.getMessage()); } @Override public void handleCacheClearError(RuntimeException exception, Cache cache) { log.info("Unable to clean cache " + cache.getName() + " : " + exception.getMessage()); } } }
Service类
@Service @AllArgsConstructor @Transactional public class CompanyServiceImpl implements CompanyService { private final CompanyRepository companyRepository; @Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager") public Optional<CompanyEntity> findByName(String companyName) { return companyRepository.findByName(companyName); } }
Company实体类
@Entity @Jacksonized @AllArgsConstructor @NoArgsConstructor public class CompanyEntity { @Id private Long id; @ToString.Exclude @OneToMany(mappedBy = "comapnyENtity", cascade = CascadeType.ALL,fetch = FetchType.EAGER) private List<EmployeeEntity> employeeEntities; }
问题现象
首次查询时缓存正常写入,执行get Company::ABC可看到Redis中已存储完整的CompanyEntity集合数据,但二次查询时仍会进入被缓存的方法查询数据库,日志报错:
Unable to get from cache Company : Could not read JSON: failed to lazily initialize a collection, could not initialize proxy - no Session (through reference chain: com.abc.entity.CompanyEntity$CompanyEntityBuilder["employeeEntities"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a c collection, could not initialize proxy - no Session (through reference chain: com.abc.entity.CompanyEntity$CompanyEntityBuilder["employeeEntities"])
已知该错误是由于代理子对象缺少Session导致,但已使用FetchType.EAGER加载,且集合数据已完整缓存,却仍无法直接读取缓存。若改用LAZY加载,集合无法被缓存会返回null,不符合业务需求,求助如何解决该问题,实现直接从Redis缓存读取数据。
1. 优化Hibernate5Module配置,强制解包代理对象
当前配置中虽然注册了Hibernate5Module,但默认不会自动解包Hibernate代理对象。需要开启相关特性,确保序列化和反序列化时能处理代理:
修改RedisConfig中的cacheConfiguration方法,调整Hibernate5Module的配置:
objectMapper.registerModules(new JavaTimeModule(), new Hibernate5Module() .enable(Hibernate5Module.Feature.UNWRAP_PROXIES) // 解包代理对象,还原为真实实体类 .enable(Hibernate5Module.Feature.FORCE_LAZY_LOADING) // 强制加载懒加载对象(即使当前无Session) );
这样在序列化时会把代理对象转换成真实的实体对象,避免反序列化时出现代理无法初始化的问题。
2. 缓存DTO而非实体类,脱离Hibernate绑定
直接缓存实体类容易遇到Hibernate代理、Session绑定等问题,更稳妥的方式是定义DTO(数据传输对象),查询数据库后将实体转换为DTO再缓存:
- 定义
CompanyDTO及EmployeeDTO:
@Data @AllArgsConstructor @NoArgsConstructor public class CompanyDTO { private Long id; private List<EmployeeDTO> employeeEntities; } @Data @AllArgsConstructor @NoArgsConstructor public class EmployeeDTO { // 定义需要的字段,如id、name等 }
- 修改Service层代码,转换为DTO后缓存:
@Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager") public Optional<CompanyDTO> findByName(String companyName) { return companyRepository.findByName(companyName) .map(this::convertToDTO); } private CompanyDTO convertToDTO(CompanyEntity entity) { CompanyDTO dto = new CompanyDTO(); dto.setId(entity.getId()); dto.setEmployeeEntities(entity.getEmployeeEntities().stream() .map(emp -> { EmployeeDTO empDto = new EmployeeDTO(); // 设置EmployeeDTO的字段 return empDto; }).collect(Collectors.toList())); return dto; }
这种方式缓存的是纯粹的POJO,完全脱离Hibernate的Session和代理,不会出现反序列化时的Session问题。
3. 确保缓存前实体已完全加载
即使使用FetchType.EAGER,某些情况下Hibernate可能仍会生成代理对象。可以在查询后手动触发关联对象的加载,确保缓存的是完全初始化的实体:
修改Service方法:
@Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager") public Optional<CompanyEntity> findByName(String companyName) { Optional<CompanyEntity> companyOpt = companyRepository.findByName(companyName); companyOpt.ifPresent(company -> { // 触发集合加载,确保在Session关闭前完成初始化 Hibernate.initialize(company.getEmployeeEntities()); }); return companyOpt; }
需要导入org.hibernate.Hibernate类,这样缓存前集合已经被完全加载为真实的List,而非代理对象,反序列化时就不会出现Session缺失的问题。
4. 临时改用JDK序列化(不推荐)
如果上述方法都不想改,可以临时改用JDK序列化代替Jackson JSON序列化,但这种方式缓存的数据可读性差,且容易出现类版本兼容问题,仅作为应急方案:
修改RedisCacheConfiguration中的值序列化器:
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()))
内容的提问来源于stack exchange,提问作者Narendra Pandey

