缓存是一般的ORM框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟Hibernate一样,MyBatis也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。
MyBatis 跟缓存相关的类都在cache包里面,其中有一个Cache接口,只有一个默认的实现类 PerpetualCache,它是用HashMap实现的。
除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等等。但是无论怎么装饰,经过多少层装饰,最后使用的还是基本的实现类(默认PerpetualCache)。
1.一级缓存
一级缓存也叫本地缓存,MyBatis的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis的一级缓存是默认开启的,不需要任何的配置。
问题一:PerpetualCache 在哪里?
在MyBatis执行的流程里面,涉及到这么多的对象,那么缓存 PerpetualCache 应该放在哪个对象里面去维护?
如果要在同一个会话里面共享一级缓存,这个对象肯定是在SqlSession里面创建的,作为SqlSession的一个属性。DefaultSqlSession里面只有两个属性,Configuration 是全局的,所以缓存只可能放在 Executor 里面维护。
SimpleExecutor、ReuseExecutor、BatchExecutor 的父类BaseExecutor的构造函数中持有了 PerpetualCache
问题二:一级缓存的作用范围?
在同一个会话里面,多次执行相同的SQL语句,会直接从内存取到缓存的结果,不会再发送 SQL 到数据库。但是不同的会话里面,即使执行的 SQL 一模一样(通过一个Mapper的同一个方法的相同参数调用),也不能使用到一级缓存。
情况一:在同一个session中共享
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlog(1));
// 如果再次发送SQL到数据库执行,说明没有命中缓存
// 如果直接打印对象,说明是从内存缓存中取到了结果
System.out.println(mapper1.selectBlog(1));
情况二:不同session不能共享
SqlSession session2 = sqlSessionFactory.openSession();
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
// 不能通过上面session的缓存进行查询
System.out.println(mapper2.selecBlog(1));
情况三:同一个会话中,update(包括delete)会导致一级缓存被清空
mapper1.updateByPrimaryKey(blog);
// 一级缓存是在 BaseExecutor 中的 update()方法中调用 clearLocalCache()清空的(无条件),query中会判断。
session1.commit();
// 会在数据库去查询数据,因为缓存已经清空了
System.out.println(mapper1.selectBlogById(1));
问题三:一级缓会存在什么问题吗?
使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。
// 会话2更新了数据,缓存清空
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
mapper2.updateByPrimaryKey(blog); session2.commit();
// 会话1依然会从自己的缓存中取数据,所以取到的结果仍然是修改前的数据
System.out.println(mapper1.selectBlog(1))
2.二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是 namespace 级别的,可以被多个SqlSession共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。
问题一:开启二级缓存后,一二级缓存的获取顺序?
作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession共享。而一级缓存是在SqlSession内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。
问题二:二级缓存放在哪个对象中维护呢?
要跨会话共享的话,SqlSession 本身和它里面的 BaseExecutor 已经满足不了需求了,那我们应该在 BaseExecutor之外创建一个对象。实际上MyBatis用了一个装饰器类来维护 – CachingExecutor。
如果启用了二级缓存,MyBatis 在创建 Executor 对象的时候会对 Executor 进行装饰。CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器 Executor 实现类
比如SimpleExecutor来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
问题三:如何开启二级缓存?
一级缓存默认是打开的,二级缓存需要配置才可以开启。
1.在 mybatis-config.xml 中配置 cacheEnabled(可以不配置,默认是true)
<setting name="cacheEnabled" value="true"/>
<!-- 只要没有显式地设置cacheEnabled=false,都会用 CachingExecutor 装饰基本的执行器。-->
2.在 Mapper.xml 中配置<cache/>标签
<!-- 声明这个namespace使用二级缓存 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024" <!-- 最多缓存对象个数, 默认1024 -->
eviction="LRU" <!-- 缓存策略 -->
flushInterval="120000" <!--自动刷新时间 ,若不配置则只有调用时刷新 -->
readOnly="false"/> <!—- 默认是false(安全),改为true支持读写时,必须支持序列化 -->
| 属性 | 含义 | 取值 |
|---|---|---|
| type | 缓存实现类 | 需要实现 Cache 接口,默认是 PerpetualCache |
| size | 最多缓存对象个数 | 默认1024 |
| eviction | 回收策略(淘汰算法) | LRU – 最近最少使用,FIFO – 先进先出, SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象 WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象 |
| flushInterval | 定时自动清空缓存间隔 | 自动刷新时间,单位 ms,未配置时只有调用时刷 |
| readOnly | 是否只读 | true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象 不能被修改。这提供了很重要的性能优势。 false:读写缓存;会返回缓存对象的拷贝(通过序列化),不会共享。这会慢一些,但是安全,因此默认是 false。 改为 false 可读写时,对象必须支持序列化。 |
| blocking | 是否使用可重入锁实现缓存并发控制 | true,会使用 BlockingCache 对 Cache 进行装饰 默认 false |
3.Mapper.xml配置了<cache>之后, select()会被缓存。update()、delete()、 insert()会刷新缓存。
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System. out .println(mapper1.selectBlogById(1));
// 事务不提交的情况下,二级缓存不会写入
// session1.commit();
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
// session2仍然走数据库
System.out.println(mapper2.selectBlogById(1));
注:select后必须提交事务,不然无法写入二级缓存
因为二级缓存使用 TransactionalCacheManager(TCM)来管理,最后又调用了TransactionalCache 的 getObject()、 putObject 和 commit 方法。TransactionalCache 里面持有真正的Cache对象,比如是经过层层封装的 PerpetualCache。
在 putObject 的时候,只是添加到了 entriesToAddOnCommit 里面,只有它的 commit() 方法被调用的时候才会调用 flushPendingEntries() 真正写入缓存。它就是在 DefaultSqlSession 调用 commit() 的时候被调用的。
问题四:有什么方法验证二级缓存吗?
使用不同的session和mapper,在其他的session中执行增删改操作使缓存刷新,验证二级缓存可以跨session
Blog blog = new Blog();
blog.setBid(1);
blog.setName("357");
mapper3.updateByPrimaryKey(blog);
session3.commit();
// 执行了更新操作 ,二级缓存失效 ,再次发送sql查询
System.out.println(mapper2.selectBlogById(1));
问题五:二级缓存使用场景?
二级缓存常用场景如下:
- 查询为主:因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。
- 一表一namespace:如果多个namespace中有针对于同一个表的操作,比如Blog 表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper里面只操作单表的情况使用。
问题六:如何让多个namespace共享一个二级缓存?
cache-ref 代表引用别的命名空间的Cache 配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。
<cache-ref namespace="com.my.crud.dao.DepartmentMapper"/>
但是在这种情况下,多个Mapper的操作都会引起缓存刷新,缓存的意义已经不大了。
3.第三方缓存做二级缓存
除了MyBatis自带的二级缓存之外,我们也可以通过实现Cache接口来自定义二级缓存。
MyBatis官方提供了一些第三方缓存集成方式,比如 ehcache 和 redis:
第一步,pom引入mybatis对应缓存依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
第二步,Mapper.xml 配置 cache type 使用 RedisCache
<cache type="org.mybatis.caches.redis.RedisCache" eviction="FIFO" flushInterval="60000"
size="512" readOnly="true"/>
第三步,redis.properties 配置
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0








还没有评论,来说两句吧...