尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MyBatis缓存深度解析:从一级缓存到二级缓存,原理、配置与避坑指南

MyBatis缓存深度解析:从一级缓存到二级缓存,原理、配置与避坑指南 1. 从一次线上慢查询说起为什么MyBatis缓存值得深究那天下午监控系统突然告警一个核心接口的响应时间从平时的50ms飙升到了2秒。我立刻登录服务器查看数据库慢查询日志发现一条非常简单的SELECT * FROM user WHERE id ?语句在短短一分钟内被重复执行了上千次而id参数的值就那么几个。这太反常了这个查询理应被MyBatis缓存起来才对。带着疑惑我检查了代码发现这个查询位于一个Service方法中而该方法被一个循环调用每次循环都创建了新的SqlSession。问题瞬间清晰我错误地认为一级缓存是“全局”的而实际上它只在单个SqlSession生命周期内有效。这次事故让我损失了半小时的排查时间但也让我下定决心必须把MyBatis缓存这个看似基础、实则暗藏玄机的机制彻底吃透。如果你也曾在使用MyBatis时对缓存的效果感到困惑——比如明明配置了缓存查询却依然频繁访问数据库或者更新了数据但查询到的还是旧结果——那么这篇文章就是为你准备的。我将结合自己踩过的坑和大量测试验证带你一次性搞懂MyBatis的一级缓存、二级缓存包括它们的工作原理、配置方式、失效场景以及那些官方文档里不会写的“坑”。无论你是正在面试准备还是想在项目中正确、高效地使用缓存来提升性能这篇超过5000字的深度解析都能给你带来实实在在的收获。2. 一级缓存SqlSession级别的“私人备忘录”一级缓存是MyBatis默认开启的无需任何配置。你可以把它理解为每个SqlSession独享的一份“私人备忘录”。在同一个SqlSession中执行两次完全相同的查询相同的SQL语句和参数第二次就会直接从这份备忘录里取结果而不会再次访问数据库。2.1 一级缓存的工作原理与生命周期它的核心实现很简单一个HashMapkey是CacheKey对象。这个CacheKey由MappedStatement的id即命名空间方法名、SQL语句、参数值、分页参数等共同决定。只要这些元素完全相同CacheKey就相同就能命中缓存。一级缓存的生命周期与SqlSession绑定开启当调用SqlSessionFactory.openSession()方法时一个新的SqlSession被创建其内部的一级缓存一个PerpetualCache对象也随之初始化。使用在SqlSession执行查询selectOne,selectList等时会先根据上述规则生成CacheKey然后去一级缓存这个HashMap里查找。找到则直接返回找不到才查库并将结果存入缓存。失效当执行了增删改操作insert,update,delete或手动调用了SqlSession.clearCache()方法或关闭SqlSession时这个“私人备忘录”就会被清空。注意这里有个关键点也是我开头踩坑的原因。一级缓存的作用域是SqlSession而不是整个应用。在常见的Spring集成场景中如果你没有正确配置事务或者在不同的方法调用中使用了不同的SqlSession例如在非事务方法中每次数据库操作都可能打开和关闭一个SqlSession那么一级缓存就形同虚设。2.2 一级缓存失效的四大场景实测理论说再多不如实测。我写了一段测试代码来验证一级缓存失效的场景// 场景一同一SqlSession相同查询缓存命中 try (SqlSession session sqlSessionFactory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User user1 mapper.selectById(1L); // 第一次查询访问数据库 User user2 mapper.selectById(1L); // 第二次查询命中一级缓存不访问数据库 System.out.println(user1 user2); // 输出 true是同一个对象默认情况下 } // 场景二执行增删改操作缓存清空 try (SqlSession session sqlSessionFactory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User user1 mapper.selectById(1L); // 第一次查询访问数据库 mapper.updateName(1L, NewName); // 执行UPDATE操作 User user2 mapper.selectById(1L); // 第二次查询因为缓存被清空再次访问数据库 } // 场景三手动清空缓存 try (SqlSession session sqlSessionFactory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User user1 mapper.selectById(1L); // 访问数据库 session.clearCache(); // 手动清空一级缓存 User user2 mapper.selectById(1L); // 再次访问数据库 } // 场景四关闭或提交SqlSession在非自动提交模式下 try (SqlSession session sqlSessionFactory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User user1 mapper.selectById(1L); // 访问数据库 session.commit(); // 提交事务在MyBatis中commit()会清空一级缓存 User user2 mapper.selectById(1L); // 再次访问数据库 }实操心得在Spring管理的事务中一个事务通常对应一个SqlSession。因此在Transactional注解的方法内部多次相同查询可以享受一级缓存。但一旦方法结束事务提交或回滚SqlSession关闭缓存也就没了。所以一级缓存更适合在单个复杂业务方法内避免重复查询短时间不变的数据。2.3 一级缓存的“坑”对象共享与序列化默认情况下一级缓存返回的是同一个对象引用。这在上面的测试中user1 user2返回true可以证明。这带来了一个潜在问题如果你在业务代码中修改了user1对象的属性那么user2对象看到的也是修改后的值因为它们根本就是同一个对象这可能会引发意想不到的副作用。为了解决这个问题可以在select标签中设置flushCachefalse默认和useCachetrue默认的同时考虑业务逻辑的隔离性。对于需要返回不同对象实例的场景一个常见的做法是让返回的实体类实现Cloneable接口或者在Service层进行深拷贝。但更根本的解决方案是不要依赖修改MyBatis返回的实体对象来传递状态它们最好被视为只读的DTO。3. 二级缓存Mapper级别的“团队共享白板”如果说一级缓存是私人的那二级缓存就是团队的。它是MapperNamespace级别的缓存多个SqlSession可以共享。这意味着SqlSession A查询了用户1的数据后只要二级缓存生效SqlSession B再查询用户1就可以直接从缓存中获取。3.1 开启与配置二级缓存二级缓存默认是关闭的需要显式开启。第一步在MyBatis核心配置文件中开启全局缓存这是总开关configuration settings !-- 默认为true通常显式写出以示明确 -- setting namecacheEnabled valuetrue/ /settings /configuration第二步在需要缓存的Mapper XML文件中添加cache/标签mapper namespacecom.example.mapper.UserMapper !-- 最简单的声明启用二级缓存 -- cache/ select idselectById resultTypeUser select * from user where id #{id} /select /mapper一个cache/标签就开启了该Mapper的二级缓存。但它的默认行为可能不符合生产要求所以我们通常需要配置它。第三步推荐详细配置cache标签cache evictionLRU flushInterval60000 size512 readOnlytrue/eviction清除策略当缓存满时如何淘汰对象。常用LRU最近最少使用和FIFO先进先出。LRU在大多数场景下更优。flushInterval刷新间隔缓存自动刷新的毫秒数。设置为60000代表每分钟清空一次缓存。不设置则代表不清空直到有增删改操作触发清空。对于更新不频繁的数据可以设置一个较大的值对于实时性要求高的可以设置较小值或依赖更新操作清空。size引用数目缓存最多可以存储多少对象。这个数量基于缓存项Cache Entries而不是物理内存。需要根据业务数据量评估。readOnly只读默认为false。如果设置为trueMyBatis会将返回对象的副本返回给调用者这更安全但会因序列化/反序列化带来轻微性能开销。如果设置为falseMyBatis会返回缓存对象的引用性能更高但有上述对象共享的问题。对于简单的、不可变的实体readOnlyfalse是安全的对于可能被修改的复杂对象建议readOnlytrue。3.2 二级缓存的工作模式与序列化要求二级缓存的数据是在多个SqlSession间共享的因此缓存的对象必须是可序列化的。你的实体类必须实现java.io.Serializable接口。public class User implements Serializable { private static final long serialVersionUID 1L; private Long id; private String name; // ... getters and setters }二级缓存的工作模式比一级缓存复杂。它并不是一个简单的HashMap。当某个SqlSession执行查询后它并不是直接把结果对象扔进一个全局Map而是先将查询结果对象序列化存储序列化后的字节数组。当另一个SqlSession执行相同查询时再从缓存中取出字节数组进行反序列化得到一个新的对象实例。这也是为什么配置readOnlytrue时返回的是副本的原因。3.3 二级缓存失效与更新的核心机制这是二级缓存最容易出错的地方。它的失效逻辑如下某个SqlSession对表T执行了INSERT/UPDATE/DELETE操作并提交了事务commit()。MyBatis会清空整个Mapper命名空间对应的二级缓存。所有后续的查询都需要重新访问数据库。这里有一个极其重要的细节事务提交是触发二级缓存清空的关键。如果你在Spring中使用了Transactional那么只有在方法成功执行完毕、事务提交时缓存才会被清空。如果方法执行过程中发生了异常并回滚则缓存不会被清空。坑点警示二级缓存是Mapper级别的。假设你有UserMapper和OrderMapperOrder对象里关联了一个User对象。你在UserMapper.xml中配置了缓存在OrderMapper.xml中也配置了缓存。当你通过OrderMapper查询一个订单连带用户信息时User数据会被缓存到OrderMapper的缓存区域。但是如果另一个操作直接通过UserMapper更新了这个用户的信息它只会清空UserMapper的缓存而OrderMapper缓存里那个旧的User数据依然存在这就导致了数据不一致。解决方案对于有关联关系的实体要么谨慎使用二级缓存要么使用更高级的缓存引用cache-ref。cache-ref可以让多个Mapper共享同一个缓存空间。例如在OrderMapper.xml中配置cache-ref namespacecom.example.mapper.UserMapper/这样OrderMapper的缓存操作就会使用UserMapper的缓存区域更新用户信息时通过OrderMapper查询到的关联用户缓存也会被清空。但这增加了耦合度需要仔细设计。4. 缓存的配置陷阱与高级工作模式仅仅知道如何开启缓存是不够的生产环境中的配置更为精细和复杂。4.1 细粒度缓存控制每个语句的缓存行为你可以在具体的select、insert、update、delete标签上通过属性来控制其与缓存的交互。useCache用于select语句。设置为false可以禁用该语句的二级缓存一级缓存不受影响。适用于实时性要求极高的查询。select idselectRealTimeData resultTypeData useCachefalse SELECT * FROM sensor_data ORDER BY time DESC LIMIT 1 /selectflushCache在select上默认为false。如果设置为true则执行该查询前会清空一级和二级缓存非常激进很少用。在insert、update、delete上默认为true。这意味着执行这些写操作后会自动清空一级和二级缓存。如果你有特殊理由不希望清空缓存风险极高可以设置为false。4.2 集成第三方缓存Ehcache与RedisMyBatis自带的二级缓存实现PerpetualCache是内存缓存单机可用但在分布式环境下会有一致性问题。因此我们常集成第三方缓存库。集成Ehcache本地内存缓存功能强大添加依赖mybatis-ehcache和ehcache。在Mapper XML中指定缓存实现cache typeorg.mybatis.caches.ehcache.EhcacheCache/在类路径下添加ehcache.xml配置文件可以精细配置内存/磁盘存储、过期时间、集群等。集成Redis分布式缓存添加依赖如mybatis-redis非官方或使用Spring Cache Redis后者更主流。通过Spring配置将MyBatis的缓存实现指向一个Redis模板。这通常需要自定义一个Cache实现类实现org.apache.ibatis.cache.Cache接口内部使用RedisTemplate进行操作。这样做的好处是所有应用实例共享同一份缓存解决了分布式一致性问题但引入了网络开销和Redis的运维成本。选型建议对于简单的单体应用使用MyBatis自带缓存或Ehcache足矣。对于微服务或分布式应用强烈建议使用Spring Cache抽象层并搭配Redis作为缓存后端这样不仅能统一缓存技术栈还能利用Spring Cache更丰富的注解如Cacheable,CacheEvict进行声明式缓存管理比在MyBatis XML中配置更灵活、更强大。4.3 缓存工作模式深度解析读写与只读前文提到的readOnly属性底层对应着不同的序列化策略。readOnlytrueMyBatis使用只读装饰器。从缓存中反序列化得到对象后直接返回。性能稍差但线程安全。readOnlyfalseMyBatis使用序列化拷贝装饰器。每次从缓存取出时会对序列化的字节数组进行反序列化创建一个全新的对象。这实际上也是一种“拷贝”但比只读装饰器更彻底。注意即使readOnlyfalse由于二级缓存本身需要序列化存储返回的也是新对象所以不会有一级缓存那种对象共享的问题。这里的“读写”指的是缓存条目本身可被替换而非返回的对象可被修改。5. 设计一个全面的MyBatis缓存测试方案理论需要实践验证。我设计了一套测试方案可以系统地验证各种缓存行为。测试环境搭建使用H2或MySQL测试数据库。使用Spring Boot MyBatis-Plus或原生MyBatis框架。开启SQL日志打印配置mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl便于观察是否真的访问了数据库。测试用例集一级缓存基础测试同一SqlSession内两次相同查询验证日志只打印一次SQL。一级缓存失效测试在两次查询间执行update验证缓存清空。在两次查询间调用sqlSession.clearCache()验证缓存清空。使用两个不同的SqlSession执行相同查询验证缓存不共享。二级缓存基础测试在两个不同的SqlSession中执行相同查询验证第二个SqlSession不打印SQL命中二级缓存。验证返回的对象不是同一个实例为false但equals为true。二级缓存失效测试SqlSession A查询数据 - SqlSession B更新同一条数据并commit- SqlSession A再次查询验证SQL再次打印缓存被清空。测试在未commit的更新操作后缓存是否被清空应该不会。缓存配置测试在select上设置useCachefalse验证二级缓存不生效。在update上设置flushCachefalse验证执行更新后后续查询是否还能命中缓存危险操作仅测试。关联查询缓存测试创建User和Order实体及Mapper。OrderMapper中配置关联查询association。测试更新User后通过OrderMapper查询到的关联User信息是否过期会过期除非使用cache-ref。通过这样的测试你不仅能巩固对缓存机制的理解还能在项目初期就发现潜在的配置错误或理解偏差避免将它们带到线上环境。6. 生产环境中的缓存实践与避坑指南结合多年的经验我总结出以下几点在生产中使用MyBatis缓存的核心建议一级缓存理解并接受其局限性。它适用于短生命周期、重复查询的场景。在Spring事务管理中合理利用。不要试图用它做跨请求的数据共享。二级缓存谨慎开启明确边界。对读远多于写、数据一致性要求不苛刻的配置表、字典表可以开启二级缓存并设置合理的flushInterval和size。对核心业务数据、更新频繁、一致性要求高的表如订单、账户不建议开启MyBatis二级缓存。数据不一致的风险远大于缓存带来的性能收益。这类场景应使用更可控的缓存策略如业务层缓存Spring Cache Redis并设计完善的缓存更新和失效逻辑。关联查询是缓存杀手。如前所述多表关联查询时缓存失效会变得非常复杂。如果一定要用考虑使用cache-ref或者放弃关联查询改为在业务层进行多次单表查询并手动组装这反而更容易控制缓存。序列化是必须的。只要用到二级缓存实体类必须实现Serializable。记得生成serialVersionUID避免反序列化失败。监控与度量。使用监控工具如Micrometer Prometheus/Grafana观察缓存命中率。如果命中率极低说明缓存配置可能不合理或者数据更新太频繁此时应该考虑关闭缓存。MyBatis-Plus的特别说明。如果你使用MyBatis-Plus它默认关闭了二级缓存。因为MP的作者认为二级缓存容易引起问题更推荐使用独立的缓存服务。如果你需要在MP中开启除了全局配置还需要在Mapper接口上添加CacheNamespace注解或在XML中配置cache。缓存是一把双刃剑。用得好它能极大提升系统性能尤其是应对高并发读场景用不好它会导致令人头疼的数据不一致问题且调试困难。我的建议是对于MyBatis自带的缓存尤其是二级缓存采取保守策略除非有非常明确的收益和充分的测试否则默认关闭。将缓存的重心转移到业务层使用像Spring Cache这样更成熟、更灵活的解决方案会让你对缓存有更强的控制力。毕竟在软件架构中清晰和可控往往比一点点的性能提升更重要。
返回列表