一、MyBatis 缓存概述
与Hibernate一样,MyBatis 同样提供了一级缓存和二级缓存的支持。
- 一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空。
- 二级缓存: 与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义第三方存储源,如 Ehcache框架等。
- 缓存更新机制: 当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。
二、一级缓存详解
2.1 一级缓存特性
MyBatis中一级缓存是默认开启的,即在查询中(一次SqlSession中)。只要当SqlSession不关闭,那么你的操作会默认存储使用一级缓存。
2.2 一级缓存测试
Mybatis一级缓存测试
String config = "sqlMapConfig.xml"; InputStream inputStream = Resources.getResourceAsStream(config); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session = sqlSessionFactory.openSession(); // 执行在bean配置文件中定义的sql语句 User user = session.selectOne("UserMapper.findById", 1); System.out.println(user); /* * 一级缓存默认就会被使用 */ user = session.selectOne("UserMapper.findById", 1); System.out.println(user); session.close(); /* 1. 必须是同一个Session,如果session对象已经close()过了就不可能用了 */ session = MyBatisUtil.getSqlSession(); user = session.selectOne("UserMapper.findById", 1); System.out.println(user); /* 2. 查询条件必须是一样的 */ user = session.selectOne("UserMapper.findById", 2); System.out.println(user); /* 3. 没有执行过session.clearCache()清理缓存 */ //session.clearCache(); user = session.selectOne("UserMapper.findById", 2); System.out.println(user); /* 4. 没有执行过增删改的操作(这些操作都会清理缓存) */ session.update("UserMapper.updateUser", new User(2, "user", 23)); user = session.selectOne("UserMapper.findById", 2); System.out.println(user);</code>2.3 一级缓存失效场景实战示例
以下是一个具体的一级缓存失效场景的实战代码示例,展示了在同一个 SqlSession 中,先执行查询,然后执行更新操作,再执行相同查询时,缓存失效并重新查询数据库的过程:
// 获取SqlSession SqlSession session = sqlSessionFactory.openSession(); try { // 第一次查询:查询ID为1的用户 System.out.println("=== 第一次查询(会查询数据库) ==="); User user1 = session.selectOne("UserMapper.findById", 1); System.out.println("第一次查询结果:" + user1); // 此时查询结果会被缓存到一级缓存中 // 第二次查询:相同条件,相同SqlSession System.out.println("\n=== 第二次查询(从一级缓存获取) ==="); User user2 = session.selectOne("UserMapper.findById", 1); System.out.println("第二次查询结果:" + user2); System.out.println("user1 == user2: " + (user1 == user2)); // true,说明是同一个对象,来自缓存 // 执行更新操作 System.out.println("\n=== 执行更新操作 ==="); User updateUser = new User(1, "updatedName", 30); int affectedRows = session.update("UserMapper.updateUser", updateUser); System.out.println("更新影响行数:" + affectedRows); // MyBatis在执行更新操作后会自动清空一级缓存 // 第三次查询:相同条件,相同SqlSession,但在更新操作之后 System.out.println("\n=== 第三次查询(更新后,缓存失效,重新查询数据库) ==="); User user3 = session.selectOne("UserMapper.findById", 1); System.out.println("第三次查询结果:" + user3); System.out.println("user1 == user3: " + (user1 == user3)); // false,说明是新对象,来自数据库 // 提交事务 session.commit(); // 第四次查询:提交事务后,相同条件 System.out.println("\n=== 第四次查询(提交事务后) ==="); User user4 = session.selectOne("UserMapper.findById", 1); System.out.println("第四次查询结果:" + user4); } finally { // 关闭SqlSession session.close(); }代码执行结果分析:
- 第一次查询:会访问数据库,查询结果被存入一级缓存。
- 第二次查询:相同条件,直接从一级缓存获取,不会访问数据库。
- 更新操作:执行
session.update()后,MyBatis会自动清空该SqlSession的一级缓存。 - 第三次查询:由于缓存已被清空,会重新访问数据库查询数据。
- 第四次查询:提交事务后,查询结果再次被缓存。
关键点说明:
- 一级缓存是基于SqlSession的,同一个SqlSession内的相同查询会利用缓存。
- 任何增删改操作(insert、update、delete)都会导致该SqlSession的一级缓存被清空。
- 调用
session.clearCache()方法也会手动清空一级缓存。 - 关闭SqlSession(
session.close())后,该Session的所有缓存都会被清除。
这个示例清晰地展示了MyBatis一级缓存的自动失效机制:当执行更新操作后,即使是在同一个SqlSession中,后续的相同查询也会因为缓存失效而重新访问数据库,确保数据的一致性。
三、二级缓存详解
3.1 二级缓存配置
Mybatis二级缓存测试
mybaits的二级缓存是mapper范围级别,除了在SqlMapConfig.xml设置二级缓存的总开关,还要在具体的mapper.xml中开启二级缓存。
3.1.1 全局配置
在核心配置文件SqlMapConfig.xml中加入:
<settingname='cacheEnabled'value='true'/> <!-- 全局配置参数,需要时再设置 --> <settings> <!-- 开启二级缓存 默认值为true --> <settingname='cacheEnabled'value='true'/> </settings>3.1.2 Mapper配置
开启二级缓存,在userMapper.xml文件中添加如下配置:
<mapper namespace="me.gacl.mapping.userMapper"> <!-- 开启二级缓存这个必须开启,全部使用默认的配置--> <cache/>3.2 二级缓存测试代码
具体的测试代码:
/* * 测试二级缓存 * 使用两个不同的SqlSession对象去执行相同查询条件的查询,第二次查询时不会再发送SQL语句,而是直接从缓存中取出数据 */ String statement = "UserMapper.findById"; SqlSessionFactory factory = MyBatisUtil.getSqlSessionFactory(); //开启两个不同的SqlSession SqlSession session1 = factory.openSession(); SqlSession session2 = factory.openSession(); //使用二级缓存时,User类必须实现一个Serializable接口===> User implements Serializable User user = session1.selectOne(statement, 1); session1.commit(); /*一定要提交事务之后二级缓存才会起作用,因为二级缓存是从cache(mapper.xml中定义的cache)中取得 如果session不commit,那么,数据就不会放入cache中*/ ystem.out.println("user="+user); //由于使用的是两个不同的SqlSession对象,所以即使查询条件相同,一级缓存也不会开启使用 user = session2.selectOne(statement, 1); //session2.commit(); System.out.println("user2="+user);3.3 缓存标签属性详解
前面在使用<cache/>时我们说全部使用默认的配置,所以仅仅写了一个标签而已,现在我们来看这个标签包含那些属性。
看如下例子,即一个常用的cache标签属性:
<cache eviction="FIFO" <!--回收策略为先进先出--> flushInterval="60000" <!--自动刷新时间60s--> size="512" <!--最多缓存512个引用对象--> readOnly="true"/> <!--只读-->3.3.1 eviction(回收策略)
- LRU– 最近最少使用的:移除最长时间不被使用的对象。(默认的属性)
- FIFO– 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT– 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK– 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
3.3.2 flushInterval(刷新间隔)
可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
3.3.3 size(引用数目)
可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。
3.3.4 readOnly(只读)
可以被设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。
四、MyBatis整合EhCache
4.1 EhCache简介
mybatis整合ehcache
ehcache是一个分布式缓存框架。
EhCache 是一个纯Java的进程内缓存框架,是一种广泛使用的开源Java分布式缓存,具有快速、精干等特点,是Hibernate中默认的CacheProvider。 —《百度百科》
4.2 分布式缓存需求
分布缓存
我们系统为了提高系统并发,性能、一般对系统进行分布式部署(集群部署方式)
不使用分布缓存,缓存的数据在各各服务单独存储,不方便系统开发。所以要使用分布式缓存对缓存数据进行集中管理。
mybatis无法实现分布式缓存,需要和其它分布式缓存框架进行整合。
mybatis提供了一个cache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。
mybatis和ehcache整合,mybatis和ehcache整合包中提供了一个cache接口的实现类。
4.3 整合步骤
4.3.1 添加依赖
在项目中加入如下两个jar包:
<dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> <version>2.10.1</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.0.0</version> </dependency>4.3.2 配置Mapper
整合ehcache
配置mapper中cache中的type为ehcache对cache接口的实现类型。
<mapper namespace='UserMapper'> <!-- 开启本mappernamespace下的二级缓存 type:指定cache接口实现类,mybatis默认使用PerpetualCache 要和eache整合,需要配置type为ehcahe实现cache接口的类型 --> <cachetype='org.mybatis.caches.ehcache.EhcacheCache'> </cache>4.3.3 调整缓存参数
可以根据需求调整缓存参数:
<cache type='org.mybatis.caches.ehcache.EhcacheCache'> <propertyname='timeToIdleSeconds'value='3600'/> <propertyname='timeToLiveSeconds'value='3600'/> <!-- 同ehcache参数maxElementsInMemory--> <propertyname='maxEntriesLocalHeap'value='1000'/> <!-- 同ehcache参数maxElementsOnDisk --> <propertyname='maxEntriesLocalDisk'value='10000000'/> <propertyname='memoryStoreEvictionPolicy'value='LRU'/> </cache>4.3.4 配置EhCache文件
加入ehcache的配置文件
在classpath下配置ehcache.xml
<ehcache xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:noNamespaceSchemaLocation='../config/ehcache.xsd'> <diskStorepath='/home/wang/cache'/> <defaultCache maxElementsInMemory='1000' maxElementsOnDisk='10000000' eternal='false' overflowToDisk='false' timeToIdleSeconds='120' timeToLiveSeconds='120' diskExpiryThreadIntervalSeconds='120' memoryStoreEvictionPolicy='LRU'> </defaultCache> </ehcache>属性说明:
diskStore:指定数据在磁盘中的存储位置。
defaultCache:当借助CacheManager.add(‘demoCache’)创建Cache时,EhCache便会采用
<defalutCache/>指定的的管理策略
以下属性是必须的:
maxElementsInMemory - 在内存中缓存的element的最大数目
maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
overflowToDisk- 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
以下属性是可选的:
- timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过
timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
diskPersistent在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候,移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
五、二级缓存应用场景
MyBatis 二级缓存适用于以下典型场景,能够有效降低数据库访问压力,提升系统性能:
- 读多写少的查询:数据更新频率低,但查询请求量大的场景。例如:历史订单查询、商品分类信息、静态配置数据等。
- 对实时性要求不高的统计分析:复杂的统计报表、数据分析 SQL 执行耗时较长,且数据变化周期较长(如每日、每周统计)。
- 公共基础数据:如地区编码、字典表、系统参数等几乎不变的数据。
- 用户非敏感数据查询:如文章列表、公告信息、电话账单查询等,允许一定时间的数据延迟。
实现方式:通过配置<cache>标签的flushInterval属性,设置合理的缓存刷新间隔(如 30 分钟、1 小时、24 小时),让 MyBatis 定期自动清空缓存并重新加载数据,平衡数据新鲜度与性能。
注意事项:
- 确保缓存的数据对象实现了
Serializable接口。 - 在涉及多表关联查询时,需要谨慎配置缓存,避免因关联表数据更新导致缓存不一致。
- 分布式部署环境下,需结合 Redis、Ehcache 等分布式缓存框架,避免各节点缓存数据不一致。
六、二级缓存局限性
尽管 MyBatis 二级缓存能显著提升查询性能,但在实际应用中存在以下局限性,需要开发者根据业务场景权衡使用:
6.1 缓存粒度粗,更新影响范围大
MyBatis 二级缓存以 Mapper 的 namespace 为单位进行缓存。当某个 Mapper 中的任意一条数据发生增、删、改操作时,该 namespace 下的所有缓存数据都会被清空。例如:
- 商品信息表有 10000 条记录,全部被缓存。
- 当仅更新其中一件商品的价格时,整个商品表的缓存都会被清除。
- 后续查询所有商品(包括未修改的 9999 件)都会重新访问数据库,造成“缓存雪崩”效应。
6.2 分布式环境缓存不一致
在集群部署环境下,MyBatis 自带的二级缓存是进程内缓存,不同服务器节点之间的缓存数据无法共享。这会导致:
- 节点 A 更新了数据,清除了自己的缓存。
- 节点 B 的缓存中仍然是旧数据,用户从节点 B 访问会得到过期结果。
解决方案:必须整合 Redis、Memcached、Ehcache 等分布式缓存框架,实现缓存数据的集中管理。
6.3 多表关联查询缓存管理复杂
当查询涉及多个表时,缓存配置变得复杂:
- 需要在关联的多个 Mapper 中分别配置
<cache-ref>来共享同一个缓存空间。 - 任何一个关联表的数据更新,都会导致整个关联查询结果集缓存失效。
- 难以实现细粒度的缓存更新策略。
6.4 事务提交后才生效
二级缓存生效的前提是事务必须提交(session.commit())。如果查询后未提交事务,数据不会放入二级缓存,其他 Session 也无法从缓存中获取数据。这要求开发者必须显式管理事务边界。
6.5 适用场景有限
由于上述限制,MyBatis 二级缓存更适用于:
- 数据更新频率极低的静态数据。
- 对数据实时性要求不高的后台统计分析。
- 单机或小规模应用,且业务逻辑简单的场景。
总结建议:对于需要细粒度缓存控制、高实时性要求或分布式部署的生产系统,建议:
- 禁用 MyBatis 自带的二级缓存(设置
cacheEnabled=false)。 - 在业务层引入 Redis 等分布式缓存,根据业务需求实现更精细的缓存策略(如按商品 ID 缓存、设置不同的过期时间等)。
- 对于特别复杂的查询,可考虑使用 Caffeine、Guava Cache 等本地缓存作为二级缓存的补充,但需注意数据一致性问题。