Mybatis的二级缓存和一级缓存作用一样,都是希望在执行查询操作时,减少对数据库(磁盘)中数据的访问,增加对缓存(内存)中数据的访问,从而提高查询接口的性能。
与一级缓存不一样的是:一级缓存是SeqSession
级别的,每一个SeqSession
对应一个一级缓存;二级缓存是Mapper
级别的,一个二级缓存会对应一个或多个XXXMapper.xml
文件。
使用二级缓存
对于一级缓存默认是开启的,二级缓存需要开发人员手动开启,指定某一条select查询Sql使用二级缓存方式:
-
mybatis-config
中的cacheEnabled
为true,该属性默认就是true,可以不用显式配置<settings> <setting name="cacheEnabled" value="true"/> </settings>
-
在
xxxMapper.xml
配置文件中添加<cache/>
标签 -
select查询语句中将
useCache
置为true<select id="selectList" useCache="true"> </select>
-
该select查询语句执行完成后需要提交事务
测试代码:
@Test
public void test2() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> users = sqlSession.selectList("edu.hzb.dao.UserDAO.selectList");
// sqlSession.commit();
System.out.println("------------------------------");
List<User> users1 = sqlSession.selectList("edu.hzb.dao.UserDAO.selectList");
}
测试代码日志:
因为第一次select查询sql执行完后没有提交事务,导致查询的数据没有存放在二级缓存中。第二次执行相同的查询sql就没有从二级缓存中获取到数据,但是从一级缓存中获取到了之前的查询结果,所以看到只打印了一次查询数据库日志。
测试代码:
@Test
public void test2() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> users = sqlSession.selectList("edu.hzb.dao.UserDAO.selectList");
sqlSession.commit();
System.out.println("------------------------------");
List<User> users1 = sqlSession.selectList("edu.hzb.dao.UserDAO.selectList");
}
测试代码日志:
第一次执行查询sql语句完后提交事务,查询的结果就由TransactionalCacheManager
对象进行管理,等到事务提交后,就会把数据存放在二级缓存中。
二级缓存流程
CachingExecutor
是对Executor
进行增强,为Executor
增加二级缓存的功能。看CachingExecutor
的属性就可以知道:
public class CachingExecutor implements Executor {
//被增强的Executor
private final Executor delegate;
//管理TransactionalCache对象
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
}
public class TransactionalCacheManager {
//key为二级缓存对象
//TransactionCache是装饰器对象,对Cache进行增强
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
}
public class TransactionalCache implements Cache {
//被增强的Cache
private final Cache delegate;
//提交事务时,清空缓存的标识
private boolean clearOnCommit;
//待提交的数据(只有在事务提交时,才会将数据存放在二级缓存中)
private final Map<Object, Object> entriesToAddOnCommit;
//缓存中没有命中的数据
private final Set<Object> entriesMissedInCache;
}
CacheExecutor
中的方法:
- 查询方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//查询sql以及参数
BoundSql boundSql = ms.getBoundSql(parameterObject);
//缓存的key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
//执行查询方法
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//二级缓存
Cache cache = ms.getCache();
//开启了二级缓存
if (cache != null) {
//是否需要清空entriesToAddOnCommit中的数据
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//从二级缓存中获取数据
List<E> list = (List<E>) tcm.getObject(cache, key);
//没有命中
if (list == null) {
//查询一级缓存或数据库
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//将查询到的数据存放在entriesToAddOnCommit中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//没有开启二级缓存,查询一级缓存或数据库
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
- 增删改方法
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
//是否需要清空entriesToAddOnCommit中的数据
flushCacheIfRequired(ms);
//执行被增强的Executor中的update方法
return delegate.update(ms, parameterObject);
}
commit
方法
public void commit(boolean required) throws SQLException {
//被增强的Executor的commit方法
delegate.commit(required);
tcm.commit();
}
CacheingExecutor
对Executor
的commit方法增强的功能体现在tcm.commit()
方法中,进入到到该方法中:
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void commit() {
//提交事务清空缓存的标识
if (clearOnCommit) {
delegate.clear();
}
//将待提交的数据真正存放在二级缓存中,这行代码就可以知道只有在事务提交时才会将查询到的结果存放在二级缓存 //中,下一次执行相同的查询sql,二级缓存才会命中
flushPendingEntries();
//清空entriesToAddOnCommit和entriesMissedInCache
reset();
}
private void flushPendingEntries() {
//将待提交的数据真正存放在二级缓存中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
//缓存中没有命中的数据
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
//防止缓存穿透
delegate.putObject(entry, null);
}
}
}
二级缓存的选择
MyBatis对二级缓存的设计非常灵活,除了自己内部实现了一系列的Cache缓存实现类。开发人员还可以自己定义缓存的实现,或者与第三方缓存进行集成。但是要实现Mybatis提供的Cache
接口中的方法,并且在<cache type=""/>
的type属性中指定Cache接口的实现类。
Mybatis的Cache
接口:
自定义或第三方缓存实现了Cache
接口中的方法,这样Mybatis在调用Cache
接口中的方法时,就会回调自定义或第三方缓存接口实现。
package org.apache.ibatis.cache;
import java.util.concurrent.locks.ReadWriteLock;
public interface Cache {
String getId();
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
ReadWriteLock getReadWriteLock();
}
xxxMapper.xml文件中指定<cache/>
标签的type属性:
<!-- 整合EhCache-->
<!-- <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>-->
<!-- 整合OSCache-->
<!-- <cache type="org.mybatis.caches.oscache.OSCache"/>-->
<!-- 整合Redis作为Cache-->
<!-- <cache type="org.mybatis.caches.redis.RedisCache" />-->
<cache/>
所以Mybatis二级缓存的选择包含:
- MyBatis自身提供的缓存实现
- 用户自定义的Cache接口实现
- 第三方缓存,比如
EhCache、OSCache、Redis