Tips:本篇笔记附带了一份代码样例,查看 https://github.com/zchengb/orm-cache
1. 概述
MyBatis的缓存机制实质上说的是它的一级缓存与二级缓存
一级缓存说的是在同一个SqlSession
中,反复地执行同一条查询SQL,在当第一次查询得到结果后,MyBatis会将该结果进行Session级别的缓存,打个比方,第一次查询到的是id = 1
的User
对象,则在查询完成后,MyBatis会将该对象存储进一个哈希表(默认是HashMap
)内,此后的每一次相同的id = 1
的User
对象查询,则会直接返回哈希表内先前已有缓存的User
对象
整个一级缓存的概念图如下所示:
二级缓存说的是在多个SqlSession
内,反复执行同一条SQL查询,会在跨Session范围内对查询结果进行缓存,以达到后续的相同SQL查询能够命中缓存进行快速返回
整个二级缓存的概念图如下所示:
什么是
SqlSession
?每当使用MyBatis开启一次与数据库的会话时,MyBatis会创建出一个Session对象表示一次数据库会话,也就是一次与MySQL的链接
在正常业务开发过程中,实际上默认都是用不到一二级缓存的,当不通过手动开启事务或者无标注@Transactional
注解的情况下,默认一次查询将会是一次独立的SqlSession
,因此多次查询并不会共享缓存,Session
级别的缓存会在每次查询后的会话关闭时进行销毁
2. 一级缓存
一级缓存的开启一般是通过手动开启一个SqlSession
来确保会话内的相同SQL查询结果能够被正确地缓存下来,因为一级缓存是不跨SqlSession
的,示例代码如下所示:
1 | SqlSession sqlSession = sqlSessionFactory.openSession(); |
每个SqlSession
中实际上都持有了Executor
,其中Executor
中有一个LocalCache
,MyBatis
会根据当前执行的语句来生成MappedStatement
对象,并在Local Cache
中查询,如果查询结果命中,则会直接返回查询结果,如果查询不命中的话,则会进一步查询数据库,并在查询完数据库后,将获取到的查询结果缓存到Local Cache
中,相关的具体实现类如下所示:
一级缓存的目的在于减少数据库的访问次数,并减少不必要的连接创建,加快整体的查询效率,MyBatis的一级缓存有2种不同的级别,分别是:Session
级别和Statement
级别
- Session级别:Session级别缓存是与MyBatis的
SqlSession
绑定的,在一个SqlSession
的生命周期内,所有的查询结果都会被缓存,在同一个SqlSession
内,相同的查询将直接从缓存中获取结果,而不会再次访问数据库,并在当SqlSession
被关闭时,会顺带将其中的缓存进行清理 - Statement级别:这个级别实际上相当于禁用了MyBatis的一级缓存策略,因为这个级别会在每次
SqlSession
查询结束后都进行缓存清理,具体源码可参考BaseExector
,如下所示:
以下是几个有关MyBatis一级缓存的测试:
- 通过
@Transactional
促使方法内使用同一SqlSession
,并确认重复执行SQL时实际仅会查询一次
1 |
|
执行结果中显示只有一条SQL查询语句,耗时为137ms,执行结果如下所示:
- 使用常规查询方式,验证每次查询都将创建新的
SqlSession
,进行数据库查询
1 |
|
执行结果中显示每次查询都将直接请求数据库,1000次查询请求共计耗时1940ms,如下所示:
- 验证当在同一
SqlSession
中,进行更新时会清除现有缓存
1 |
|
执行结果中显示当第一次查询后再次进行更新,更新后再次进行查询时,会再从数据库中获取最新的数据,如下所示:
- 当跨
SqlSession
时,查询后的缓存不会受到其中另一个SqlSession
更新而失效
1 |
|
执行结果如下所示:
默认情况下,MyBatis的一级缓存使用的是PerpetualCache
作为缓存类,其底层使用的是HashMap
,并且这个哈希表没有做数量的限制,也就是说,如果长时间持续查询不同的对象,并且过程中没有执行任何的更新语句(更新语句会导致SqlSession
清空缓存),则会导致后续底层HashMap
过大,最后导致OOM
因此,MyBatis的一级缓存就是使用了简单的HashMap
,MyBatis只负责将查询数据库的结果放置到缓存当中,不会去判断缓存的大小、过期时间等等
3. 二级缓存
二级缓存会在SQL执行前统一查询本地缓存表,这个本地缓存表是跨SqlSession
的,也就是说在所有的SqlSession
内都是共享的
MyBatis的Mapper可以通过@CacheNamespace
来开启二级缓存,如下所示:
1 |
|
而对应的使用方法如下:
1 |
|
可以观察到整个使用过程中进行了3次数据库交互,对应是3个SqlSession
,因为并没有开启事务,也没有通过SqlSessionFactory
来创建统一的SqlSession
,但实际上的执行效果确是在第二次查询时,从缓存中获取到了缓存值,从执行结果可以看出实际的数据库查询只发生了一次,如下所示:
整个缓存策略实际上依赖的是TransactionalCacheManager
,一般来说,在二级缓存执行流程后就会进入一级缓存的执行流程
4. 总结
MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession
之间缓存数据的共享,同时粒度更加的细,能够到namespace
级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强
一二级缓存都可能会由于缓存出现脏数据,在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高,也就是说相比于MyBatis的一二级缓存来说,直接用分布式缓存或许会更香一些(不需要去实现二级缓存的底层自定义逻辑)