Hibernate二级缓存每次调用均执行put操作且耗时过长的原因排查
问题背景
基于Spring Boot 2.7.3搭建的应用,采用Hibernate ORM、Hazelcast作为二级缓存提供商、MySQL作为数据库,目标是通过二级缓存加速findAll批量查询(开发环境数据库存有5012条实体数据)。实际运行中发现:首次调用findAll时,所有实体均从数据库加载并写入二级缓存;但后续调用仅能命中30条缓存条目,剩余4982条需重新执行写入操作,该过程耗时极长,导致系统性能反而不如关闭二级缓存时。
相关配置信息
application.yml 核心配置
properties: hibernate.jdbc.time_zone: UTC hibernate.id.new_generator_mappings: true hibernate.connection.provider_disables_autocommit: true hibernate.cache.use_second_level_cache: true hibernate.cache.use_query_cache: false hibernate.generate_statistics: false # modify batch size as necessary hibernate.jdbc.batch_size: 25 hibernate.order_inserts: true hibernate.order_updates: true hibernate.query.fail_on_pagination_over_collection_fetch: true hibernate.query.in_clause_parameter_padding: true hibernate.cache.region.factory_class: com.hazelcast.hibernate.HazelcastCacheRegionFactory
实体类缓存配置
实体类已添加@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)注解,启用二级缓存支持。
Hazelcast 缓存配置
@Bean public Config hazelcastConfig() { Config config = new Config(); config.setInstanceName("placeholder"); MapConfig mapConfig = new MapConfig("default"); mapConfig.setBackupCount(1); mapConfig.getEvictionConfig().setEvictionPolicy(EvictionPolicy.LRU); mapConfig.getEvictionConfig().setMaxSizePolicy(MaxSizePolicy.USED_HEAP_SIZE); mapConfig.setTimeToLiveSeconds(3600); config.addMapConfig(mapConfig); return Hazelcast.newHazelcastInstance(config); }
findAll 方法实现
public List<SinkSourceAddDTO> findAll() { return HouseRepository.findAll() .stream().map(HouseAddMapper::toDto) .collect(Collectors.toCollection(LinkedList::new)); }
会话指标详情
首次调用指标
Session Metrics { 1968100 nanoseconds spent acquiring 1 JDBC connections; 0 nanoseconds spent releasing 0 JDBC connections; 41430100 nanoseconds spent preparing 32 JDBC statements; 52658900 nanoseconds spent executing 32 JDBC statements; 0 nanoseconds spent executing 0 JDBC batches; 2712459300 nanoseconds spent performing 5012 L2C puts; 0 nanoseconds spent performing 0 L2C hits; 8894900 nanoseconds spent performing 30 L2C misses; 0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections); 97100 nanoseconds spent executing 2 partial-flushes (flushing a total of 0 entities and 0 collections) }
首次调用完成5012条实体的缓存写入操作。
第二次及后续调用指标
Session Metrics { 290100 nanoseconds spent acquiring 1 JDBC connections; 0 nanoseconds spent releasing 0 JDBC connections; 111100 nanoseconds spent preparing 2 JDBC statements; 14781000 nanoseconds spent executing 2 JDBC statements; 0 nanoseconds spent executing 0 JDBC batches; 1450759900 nanoseconds spent performing 4982 L2C puts; 9978700 nanoseconds spent performing 30 L2C hits; 0 nanoseconds spent performing 0 L2C misses; 0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections); 11100 nanoseconds spent executing 2 partial-flushes (flushing a total of 0 entities and 0 collections) }
仅命中30条缓存条目,剩余4982条需重新写入缓存,耗时约1.4秒。
已尝试无效方案
- 切换缓存提供商至EhCache
- 增大缓存容量上限
- 延长缓存TTL(Time To Live)时间
- 更换缓存淘汰策略(如从LRU改为LFU)
针对性解决方案
1. 启用查询缓存
Hibernate二级缓存仅缓存单个实体实例,findAll这类批量查询的结果集默认不会被缓存。每次调用findAll都会重新加载所有实体,进而触发大量缓存写入操作。需开启查询缓存:
- 修改
application.yml,设置hibernate.cache.use_query_cache: true - 在Repository的
findAll方法上添加@Cacheable注解,或在HQL查询中显式调用setCacheable(true)
2. 修正实体的equals/hashCode实现
Hibernate依赖实体的equals和hashCode方法生成缓存键。若这两个方法实现不当(比如基于非主键字段),会导致缓存键不一致,无法命中已有缓存。确保这两个方法基于实体主键实现。
3. 为实体配置专属缓存区域
当前使用默认缓存区域,可能因通用策略不匹配导致缓存失效。为目标实体单独配置缓存区域:
@Bean public Config hazelcastConfig() { Config config = new Config(); config.setInstanceName("placeholder"); // 为House实体配置专属缓存区域 MapConfig houseMapConfig = new MapConfig("com.yourpackage.House"); houseMapConfig.setBackupCount(1); houseMapConfig.getEvictionConfig().setEvictionPolicy(EvictionPolicy.LRU); houseMapConfig.getEvictionConfig().setMaxSizePolicy(MaxSizePolicy.USED_HEAP_PERCENTAGE); houseMapConfig.setTimeToLiveSeconds(3600); // 临时测试可禁用TTL,排查是否为过期导致的缓存失效 // houseMapConfig.setTimeToLiveSeconds(0); config.addMapConfig(houseMapConfig); MapConfig defaultMapConfig = new MapConfig("default"); defaultMapConfig.setBackupCount(1); defaultMapConfig.getEvictionConfig().setEvictionPolicy(EvictionPolicy.LRU); defaultMapConfig.getEvictionConfig().setMaxSizePolicy(MaxSizePolicy.USED_HEAP_SIZE); defaultMapConfig.setTimeToLiveSeconds(3600); config.addMapConfig(defaultMapConfig); return Hazelcast.newHazelcastInstance(config); }
4. 确保查询运行在只读事务中
READ_WRITE缓存策略下,Hibernate会在事务提交后更新缓存。若事务边界处理不当,可能导致缓存状态不一致。为findAll方法添加只读事务注解:
@Transactional(readOnly = true) public List<SinkSourceAddDTO> findAll() { return HouseRepository.findAll() .stream().map(HouseAddMapper::toDto) .collect(Collectors.toCollection(LinkedList::new)); }
5. 验证实体序列化正确性
Hazelcast默认使用Java序列化,若实体或其关联对象未实现Serializable接口,会导致缓存条目无法正确存储或读取。确保所有需要缓存的实体及关联对象均实现Serializable接口。
内容的提问来源于stack exchange,提问作者Furkan Calik

