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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 03:54:57