ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MyBatis二级缓存机制解析与实践指南

MyBatis二级缓存机制解析与实践指南 1. MyBatis二级缓存的核心机制解析MyBatis作为Java领域主流的ORM框架其缓存机制设计直接影响着应用性能。二级缓存作为跨SqlSession的共享缓存其工作机制比一级缓存复杂得多。我们先从底层存储结构开始剖析。二级缓存的核心实现位于CachingExecutor类中这是一个典型的装饰器模式应用。当配置开启二级缓存后cacheEnabledtrueMyBatis会用CachingExecutor包装基础的Executor实现。这个设计非常巧妙——既保留了原有Executor的功能又添加了缓存层。缓存数据的存储依托于TransactionalCacheManager这个中转站。它内部维护了一个映射关系表将每个Cache实例与对应的TransactionalCache包装实例关联起来。这种设计使得缓存操作可以与事务生命周期绑定只有事务提交后缓存变更才会真正生效。// 典型的事务性缓存写入流程 public class TransactionalCache implements Cache { private final Cache delegate; private boolean clearOnCommit; private final MapObject, Object entriesToAddOnCommit new HashMap(); Override public void putObject(Object key, Object object) { entriesToAddOnCommit.put(key, object); // 暂存到待提交Map } public void commit() { if (clearOnCommit) { delegate.clear(); // 事务提交时执行清理 } flushPendingEntries(); // 将暂存数据写入真实缓存 } }缓存键的生成逻辑与一级缓存相同都是基于CacheKey机制。这个键由五个核心要素构成MappedStatement的ID、查询偏移量、查询限制条数、原始SQL语句以及所有参数值。这种设计保证了不同查询条件的缓存隔离性。关键点二级缓存默认作用域是Mapper的namespace级别。这意味着同namespace下的所有操作共享同一个缓存区域这也是多表查询时容易出现脏数据的根本原因。2. 二级缓存的配置与使用陷阱2.1 基础配置方式启用二级缓存需要两步核心配置全局开关mybatis-config.xmlsettings setting namecacheEnabled valuetrue/ /settingsMapper级别声明XML映射文件cache evictionLRU flushInterval60000 size512 readOnlytrue/这些参数控制着缓存的核心行为eviction淘汰策略LRU/FIFO/SOFT/WEAKflushInterval自动刷新间隔毫秒size缓存对象数量上限readOnly是否只读影响序列化方式2.2 典型问题场景案例1事务未提交导致的缓存失效// 测试用例1未提交事务 SqlSession session1 factory.openSession(); StudentMapper mapper1 session1.getMapper(StudentMapper.class); System.out.println(mapper1.getStudentById(1)); // 查询数据库 SqlSession session2 factory.openSession(); StudentMapper mapper2 session2.getMapper(StudentMapper.class); System.out.println(mapper2.getStudentById(1)); // 仍然查询数据库原因分析由于session1未调用commit()TransactionalCache机制使得查询结果未被真正写入共享缓存。案例2多表关联查询的脏数据// StudentMapper.xml select idgetStudentWithClass resultMapstudentWithClass SELECT s.*, c.class_name FROM student s JOIN class c ON s.class_id c.id WHERE s.id #{id} /select // ClassMapper.xml update idupdateClassName UPDATE class SET class_name#{name} WHERE id#{id} /update当ClassMapper更新班级名称后StudentMapper的关联查询结果仍然来自缓存导致数据不一致。这是因为不同namespace的缓存是隔离的。2.3 解决方案对比问题类型常规方案优缺点分析事务未提交强制调用commit()破坏业务逻辑的原子性多表关联使用cache-ref增大缓存粒度影响性能分布式环境自定义Cache实现开发成本高维护复杂经验之谈在实际项目中我们通常建议关闭二级缓存转而使用更专业的分布式缓存方案如Redis。如果必须使用应该严格限定在单表查询场景并设置合理的flushInterval。3. 二级缓存源码深度剖析3.1 缓存装饰器链MyBatis的二级缓存通过多层装饰器增强功能SynchronizedCache保证线程安全LoggingCache输出命中率日志SerializedCache序列化存储LruCache最近最少使用淘汰PerpetualCache基础HashMap存储// 典型的装饰器构建过程 public class CacheBuilder { public Cache build() { Cache cache new PerpetualCache(id); if (readOnly) { cache new SerializedCache(cache); } cache new LruCache(cache); cache new LoggingCache(cache); cache new SynchronizedCache(cache); return cache; } }3.2 查询流程解析完整的二级缓存查询路径如下创建CacheKey尝试从TransactionalCache获取未命中则查询数据库结果暂存到entriesToAddOnCommit事务提交时同步到真实缓存graph TD A[Executor.query] -- B{二级缓存开启?} B --|是| C[CachingExecutor] B --|否| D[BaseExecutor] C -- E[查询TransactionalCache] E -- F{缓存命中?} F --|是| G[返回结果] F --|否| H[查询数据库] H -- I[结果存入待提交区] I -- J[事务提交时同步]3.3 更新时的缓存清理任何insert/update/delete操作都会触发缓存清理public int update(MappedStatement ms, Object parameter) { flushCacheIfRequired(ms); // 根据配置决定是否清空缓存 return delegate.update(ms, parameter); }这个清理是namespace级别的这也是为什么多表操作时会出现脏数据——只有当前Mapper的缓存被清理关联表的Mapper缓存仍然有效。4. 生产环境实践建议经过上述分析我们可以总结出以下实战经验明确禁用场景多表关联查询高频更新的数据分布式部署环境对实时性要求高的业务优化配置方案!-- 安全配置示例 -- cache evictionLRU size1000 flushInterval30000 readOnlyfalse blockingtrue/监控指标缓存命中率通过LoggingCache输出内存占用情况特别是大对象缓存缓存刷新频率观察flush操作日志替代方案对比方案优点缺点MyBatis二级缓存集成简单功能有限Redis集中缓存功能强大需要额外维护本地缓存(Caffeine)性能极高不支持分布式在最近的一个电商项目中我们最初启用了二级缓存来优化商品查询。但当商品信息和库存信息需要联合查询时频繁出现库存显示不准确的问题。最终我们采用折中方案单表查询使用二级缓存多表关联查询使用Redis缓存并通过消息队列保证缓存更新的一致性。特别提醒测试阶段务必验证缓存一致性。我们曾遇到过一个隐蔽的bug——由于实体类没有实现Serializable接口在readOnlyfalse配置下导致缓存失效这个问题直到压测阶段才暴露出来。
返回列表