无法从Redis缓存中获取集合

3
我们在应用程序中使用Redis缓存来存储数据。我们直接使用@Cacheable来允许缓存,并在底层使用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 Operations","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模式进行缓存,并且整个集合也存在于缓存中。但它仍然进入缓存方法并从数据库获取值。我们如何防止这种情况并直接从缓存中使用它。
更新: 如果我们使用LAZY加载,集合对象将不会被缓存并且为null。但我们需要缓存的集合,因为方法不会按顺序调用,缓存方法稍后将返回null。
2个回答

2
找到了所需的答案这里。我的缓存集合引用没有正确地进行反序列化。在应用所需的更改后,我能够成功地从Redis缓存中反序列化缓存集合对象。
现有Redis配置的更改 -
@Bean
public RedisCacheConfiguration cacheConfiguration(ObjectMapper objectMapper) {
    objectMapper = objectMapper.copy();
    objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    objectMapper.registerModules(new JavaTimeModule(), new Hibernate5Module(), new Jdk8Module())
            .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).addMixIn(Collection.class, HibernateCollectionMixIn.class);
    return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofDays(redisTTL))
            .disableCachingNullValues()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));
}

修复中添加了两个新的类 -
class HibernateCollectionIdResolver extends TypeIdResolverBase {

public HibernateCollectionIdResolver() {
}

@Override
public String idFromValue(Object value) {
    //translate from HibernanteCollection class to JDK collection class
    if (value instanceof PersistentArrayHolder) {
        return Array.class.getName();
    } else if (value instanceof PersistentBag || value instanceof PersistentIdentifierBag || value instanceof PersistentList) {
        return List.class.getName();
    } else if (value instanceof PersistentSortedMap) {
        return TreeMap.class.getName();
    } else if (value instanceof PersistentSortedSet) {
        return TreeSet.class.getName();
    } else if (value instanceof PersistentMap) {
        return HashMap.class.getName();
    } else if (value instanceof PersistentSet) {
        return HashSet.class.getName();
    } else {
        //default is JDK collection
        return value.getClass().getName();
    }
}

@Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
    return idFromValue(value);
}

//deserialize the json annotated JDK collection class name to JavaType
@Override
public JavaType typeFromId(DatabindContext ctx, String id) throws IOException {
    try {
        return ctx.getConfig().constructType(Class.forName(id));
    } catch (ClassNotFoundException e) {
        throw new UnsupportedOperationException(e);
    }
}

@Override
public JsonTypeInfo.Id getMechanism() {
    return JsonTypeInfo.Id.CLASS;
}

}

@JsonTypeInfo(
    use = JsonTypeInfo.Id.CLASS
)
@JsonTypeIdResolver(value = HibernateCollectionIdResolver.class)
public class HibernateCollectionMixIn {
}

1
你能说明一下,当应用于你的使用情况时,你必须进行的更改吗? - undefined

1
JsonMappingException意味着Jackson试图反序列化一个Hibernate代理对象,但由于在反序列化过程中Hibernate会话不可用,因此无法完成反序列化操作。
因此,在序列化之前,您需要确保employeeEntities集合已正确初始化为非代理状态,以便Jackson能够正确地从缓存中反序列化CompanyEntity对象,而无需Hibernate会话。

您可以通过调整服务方法来确保集合的正确初始化,以在缓存CompanyEntity之前强制初始化employeeEntities集合!

@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
这假设您正在使用{{link1:FetchType.EAGER}},这意味着在获取CompanyEntity时,employeeEntities集合将自动加载。
如果问题仍然存在,您可以检查是否分离实体有所帮助。
@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实体和被缓存的内容之间创建一个清晰的分离,有助于避免类似这样的问题。

你好Von,抱歉。在上述力量检查中,也存在相同的异常。 - undefined
@Neil 好的。我已经编辑了答案以回应您的评论。 - undefined
你好 @Vonc,上面的建议看起来是正确的,但上面的答案对我起作用。我已经发布了一个答案。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接