Mybatis提供了两种缓存,一种是一级缓存,一种是二级缓存,今天先学习下一级缓存。
一级缓存的作用域是sqlsession会话级别,同一个会话中,相同的MapperStatement
(调用相同mapper的相同方法,上一篇mapper注册注入的文章说到过,mapper的方法将会解析生成对应的MapperStatement
),传相同的参数,那么将会从缓存中查询,不会再查询数据库。
下面通过几个例子验证:
首先定义了一个mapper(两个方法的sql是一样的,后面将会说到为什么这样):
1 |
|
然后调用mapper:
1 |
|
可以看到,我使用
sqlSessionFactory
来手动调用openSession
创建会话,为什么不直接注入foodMapper呢,其实上一篇mapper注册与注入的文章,我已经知道,其实使用mapper调用方法,实际上是通过MapperProxy代理对象创建MapperMethod
对象并调用execute()
方法,而execute()
调用SqlSessionTemplate
的对应方法,SqlSessionTemplate
则创建代理会话sqlSessionProxy处理。所以我们每次使用foodMapper.getById
都会创建一个会话,就不满足一级缓存的条件了。
- 在test2()中,我在一个会话中,两次调用了
getById()
方法,并都传入参数1,结果如下:
1 | Cache Hit Ratio [site.wetsion.mybatislearning.mapper.FoodMapper]: 0.0 |
可以看到,只打印了一条查询sql,只查询了一次,第二次从缓存中读取的
- 在test3()中,在同一个会话中,我分别调用
getById
和selectById
,参数都为1,上面定义也知道,这两个方法执行的语句是一样的,运行结果如下:
1 | Cache Hit Ratio [site.wetsion.mybatislearning.mapper.FoodMapper]: 0.0 |
可以看到,执行了两次sql查询语句,且都一样,说明不满足一级缓存的条件.
网上有的文章说,查询语句相同的sql会被缓存,这里验证了,查询语句相同并不是必要条件
- 在test4()中,我创建了两个会话,调用相同的方法,结果就不用贴出来了,依然是执行了两次sql查询,并没使用缓存。
上面的例子证明了一级缓存确实是在同一会话,相同的MapperStatement,相同的参数,则会使用缓存。那么原理是什么呢,还是探索分析以下源码。
从上面的selectOne()入手,我们看下DefaultSqlSession#selectOne()
(上篇文章已知道mybatis实际默认使用就是DefaultSqlSession
):
1 | public <T> T selectOne(String statement, Object parameter) { |
额。。继续看selectList():
1 | public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { |
根据mapper中方法的全限定名从Configuration
中获取对应的MapperStatement
对象,然后再调用executor的query()
方法。
这个executor类型是Executor
接口,那么实际是哪个实现类呢,executor属性是通过DefaultSqlSession
构造方法设置的,而创建DefaultSqlSession
是通过DefaultSqlSessionFactory#openSession()
,再调用openSessionFromDataSource()
:
1 | private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { |
可以看到,executor来自Configuration
类:
1 | public Executor newExecutor(Transaction transaction, ExecutorType executorType) { |
默认是SimpleExecutor
,但由于默认cacheEnabled
属性为true,所以通过装饰器模式对SimpleExecutor
进行了包装,真正使用的是CacheExecutor
。紧接上文,看一下CacheExecutor
的query()
:
1 |
|
BundleSql
呢,就是持有实际的sql字符串,以及一些参数,而CacheKey
,顾名思义,就是缓存的键。即生成了sql和缓存键之后,调用query(),似乎要开始真正的查询操作:
1 |
|
Cache cache = ms.getCache()
这里是获取二级缓存,由于这里没设置二级缓存,暂不提,继续看,调用了delegate.query(),这里delegate就是上文说的默认的SimpleExecutor
,而SimpleExecutor
自身没有query(),继承了父类BaseExecutor
的query(),所以一级缓存的核心就是BaseExecutor
的query():
1 |
|
可以看到这样一行list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
即从本地缓存localCache中获取,这个locaCache是BaseExecutor中的属性,PerpetualCache对象,就是一级缓存,内部实现其实就是一个hashMap,如果这个list结果为空,就会执行list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
:
1 | private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { |
这是真正的从数据库查询,查询后再往一级缓存localCache中放一份。
到这里,就已经知道一级缓存查询的原理了,那为什么当执行更新操作时,就会清除一级缓存呢,也顺带把这个也看一下。
同理,先看DefaultSqlSession
的update方法:
1 |
|
大同小异,从上面我们已经知道实际是CacheExecutor,所以看CacheExecutor的update:
1 |
|
这里flushCacheIfRequired(ms)
是和二级缓存相关,暂不看,从上面我们也知道,delegate是SimpleExecutor,但SimpleExecutor的方法都继承自BaseExecutor
,所以直接看BaseExecutor
的update:
1 |
|
从方法名我们也知道了,clearLocalCache()
如果会话没被关闭,清空一级缓存,然后再执行数据库操作。
至此,一级缓存读写的原理在梳理源码的过程中已经很清晰了,之后再学习下二级缓存原理。