无法从Redis缓存获取集合

问题描述 投票:0回答:1

我们使用 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
@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);
}

}

公司类-

@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

" {"@class":"com.abc.entity.CompanyEntity","createdTs":1693922698604,"id":100000000002,"name":"ABC","description":"ABC 操作","active" :true,“EmployeeEntities”:[“org.hibernate.collection.internal.PersistentBag”,[{“@class”:“com.abc.entity.EmployeeEntity”,“createdTs”:1693922698604,“Id”:100000000002,” EmployeeEntity":{"@class":"com.abc.EmployeeLevel","levelId":100000000000,"name":"H1","active":true}}]]}"

但是当我们尝试第二次执行查询时,它仍然进入缓存方法并显示以下日志 -

    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"])

我从各种 SO 答案中了解到,这是由于代理子对象的会话不可用。但我们使用 EAGER 模式进行缓存,整个集合也存在于缓存中。但它仍然进入缓存的方法并从数据库获取值。我们如何防止它并直接从缓存中使用它。

更新 如果我们使用延迟加载,集合对象不会被缓存并且为空。但我们需要缓存的集合,因为方法不会按顺序调用,并且缓存的方法稍后将返回 null。

spring caching redis spring-data-redis spring-cache
1个回答
0
投票

A

JsonMappingException
表示 Jackson 正在尝试反序列化 Hibernate 代理对象,但无法执行此操作,因为 Hibernate 会话在反序列化期间不可用。
因此,您需要确保在序列化之前将
employeeEntities
集合正确初始化为非代理状态,以便 Jackson 能够正确地从缓存中反序列化
CompanyEntity
对象,而不需要 Hibernate 会话。 您可以通过调整服务方法来确保集合的正确初始化,以在缓存

employeeEntities

之前强制初始化

CompanyEntity
集合!
@Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager")
public Optional<CompanyEntity> findByName(String companyName) {
    Optional<CompanyEntity> companyEntityOpt = companyRepository.findByName(companyName);
    companyEntityOpt.ifPresent(companyEntity -> {
        companyEntity.getEmployeeEntities().size();  // Force initialization of the collection
    });
    return companyEntityOpt;
}

这样,
employeeEntities

集合就会从 Hibernate 代理转换为常规 Java 集合。这应该有助于避免您在从缓存反序列化期间遇到的

JsonMappingException
这假设您正在使用 

FetchType.EAGER

,这意味着当您获取 
employeeEntities 时,会自动加载
CompanyEntity
集合。

如果问题仍然存在,您可以检查分离实体是否有帮助:

@Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager") public Optional<CompanyEntity> findByName(String companyName) { Optional<CompanyEntity> companyEntityOpt = companyRepository.findByName(companyName); companyEntityOpt.ifPresent(companyEntity -> { companyEntity.getEmployeeEntities().size(); // Force initialization of the collection // Obtain entity manager and detach the entity EntityManager em = // get entity manager bean em.detach(companyEntity); }); return companyEntityOpt; }

从 Hibernate 会话中分离实体会将其变成普通的 POJO。

请注意,要获取

EntityManager

,您需要将其注入到您的服务类中,并且您应该确保在分离实体之前正确初始化稍后将访问的所有关系和属性。


避免直接缓存 Hibernate 托管实体或确保 Hibernate 代理不被序列化的另一种方法是使用 DTO(数据传输对象)将持久性模型与应用程序逻辑中正在使用的对象分开。

创建一个与您的
    CompanyEntity
  • 类相对应的 DTO 类。
    在缓存之前,将您的 
  • CompanyEntity
  • 实例映射到 DTO 实例。
    缓存DTO实例而不是实体实例。
  • 从缓存中读取时,您将获得一个 DTO 实例,然后可以根据需要将其映射回实体实例。
  • 在您的服务类别中,它看起来像这样:

@Service @AllArgsConstructor @Transactional public class CompanyServiceImpl implements CompanyService { private final CompanyRepository companyRepository; private final ModelMapper modelMapper; // Bean for mapping entity to DTO @Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager") public Optional<CompanyDTO> findByName(String companyName) { Optional<CompanyEntity> companyEntityOpt = companyRepository.findByName(companyName); return companyEntityOpt.map(companyEntity -> { companyEntity.getEmployeeEntities().size(); // Force initialization of the collection return modelMapper.map(companyEntity, CompanyDTO.class); // Map entity to DTO before caching }); } }

在此方法中,您将使用 
ModelMapper

或其他映射框架将实体映射到 DTO。该 DTO 将被缓存,从而避免您遇到的 Hibernate 代理问题。

请记住为 

EmployeeEntity

以及属于对象图一部分的任何其他实体创建相应的 DTO。

这种方法需要创建额外的类并修改您的服务逻辑,但它将在 Hibernate 实体和缓存内容之间创建一个清晰的分离,这有助于避免此类问题。

© www.soinside.com 2019 - 2024. All rights reserved.