Mybatis介绍之缓存
8 Mybatis介绍之缓存
Mybatis中有一级缓存和二级缓存,默认情况下一级缓存是开启的,而且是不能关闭的。一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中进行相同的SQL语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存1024条SQL。二级缓存是指可以跨SqlSession的缓存。
Mybatis中进行SQL查询是通过org.apache.ibatis.executor.Executor接口进行的,总体来讲,它一共有两类实现,一类是BaseExecutor,一类是CachingExecutor。前者是非启用二级缓存时使用的,而后者是采用的装饰器模式,在启用了二级缓存时使用,当二级缓存没有命中时,底层还是通过BaseExecutor来实现的。
8.1 一级缓存
一级缓存是默认启用的,在BaseExecutor的query()方法中实现,底层默认使用的是PerpetualCache实现,PerpetualCache采用HashMap存储数据。一级缓存会在进行增、删、改操作时进行清除。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
一级缓存的范围有SESSION和STATEMENT两种,默认是SESSION,如果我们不需要使用一级缓存,那么我们可以把一级缓存的范围指定为STATEMENT,这样每次执行完一个Mapper语句后都会将一级缓存清除。如果只是需要对某一条select语句禁用一级缓存,则可以在对应的select元素上加上flushCache="true"。如果需要更改一级缓存的范围,请在Mybatis的配置文件中,在<settings>下通过localCacheScope指定。
<setting name="localCacheScope" value="SESSION"/>
为了验证一级缓存,我们进行如下测试,在testCache1中,我们通过同一个SqlSession查询了两次一样的SQL,第二次不会发送SQL。在testCache2中,我们也是查询了两次一样的SQL,但是它们是不同的SqlSession,结果会发送两次SQL请求。需要注意的是当Mybatis整合Spring后,直接通过Spring注入Mapper的形式,如果不是在同一个事务中每个Mapper的每次查询操作都对应一个全新的SqlSession实例,这个时候就不会有一级缓存的命中,如有需要可以启用二级缓存。而在同一个事务中时共用的就是同一个SqlSession。这块有兴趣的朋友可以去查看MapperFactoryBean的源码,其父类SqlSessionDaoSupport在设置SqlSessionFactory或设置SqlSessionTemplate时的逻辑。
/**
* 默认是有一级缓存的,一级缓存只针对于使用同一个SqlSession的情况。<br/>
* 注意:当使用Spring整合后的Mybatis,不在同一个事务中的Mapper接口对应的操作也是没有一级缓存的,因为它们是对应不同的SqlSession。在本示例中如需要下面的第二个语句可使用一级缓存,需要testCache()方法在一个事务中,使用@Transactional标注。
*/
@Test
public void testCache() {
PersonMapper mapper = session.getMapper(PersonMapper.class);
mapper.findById(5L);
mapper.findById(5L);
}
@Test
public void testCache2() {
SqlSession session1 = this.sessionFactory.openSession();
SqlSession session2 = this.sessionFactory.openSession();
session1.getMapper(PersonMapper.class).findById(5L);
session2.getMapper(PersonMapper.class).findById(5L);
}
8.2 二级缓存
8.2.1简介
二级缓存是默认启用的,如想取消,则可以通过Mybatis配置文件中的<settings>元素下的子元素<setting>来指定cacheEnabled为false。
<settings>
<setting name="cacheEnabled" value="false" />
</settings>
cacheEnabled默认是启用的,只有在该值为true的时候,底层使用的Executor才是支持二级缓存的CachingExecutor。具体可参考Mybatis的核心配置类org.apache.ibatis.session.Configuration的newExecutor方法实现。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
要使用二级缓存除了上面一个配置外,我们还需要在我们对应的Mapper.xml文件中定义需要使用的cache,具体可以参考CachingExecutor的以下实现,其中使用的cache就是我们在对应的Mapper.xml中定义的cache。还有一个条件就是需要当前的查询语句是配置了使用cache的,即下面源码的useCache()是返回true的,默认情况下所有select语句的useCache都是true,如果我们在启用了二级缓存后,有某个查询语句是我们不想缓存的,则可以通过指定其useCache为false来达到对应的效果。
@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) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, 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);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
8.2.2cache定义
刚刚说了我们要想使用二级缓存,是需要在对应的Mapper.xml文件中定义其中的查询语句需要使用哪个cache来缓存数据的。这有两种方式可以定义,一种是通过cache元素定义,一种是通过cache-ref元素来定义。但是需要注意的是对于同一个Mapper来讲,它只能使用一个Cache,当同时使用了<cache>和<cache-ref>时使用<cache>定义的优先级更高。Mapper使用的Cache是与我们的Mapper对应的namespace绑定的,一个namespace最多只会有一个Cache与其绑定。
8.2.2.1cache元素定义
使用cache元素来定义使用的Cache时,最简单的做法是直接在对应的Mapper.xml文件中指定一个空的<cache/>元素,这个时候Mybatis会按照默认配置创建一个Cache对象,准备的说是PerpetualCache对象,更准确的说是LruCache对象(底层用了装饰器模式)。具体可以参考XMLMapperBuilder中的cacheElement()方法中解析cache元素的逻辑。空cache元素定义会生成一个采用最近最少使用算法最多只能存储1024个元素的缓存,而且是可读写的缓存,即该缓存是全局共享的,任何一个线程在拿到缓存结果后对数据的修改都将影响其它线程获取的缓存结果,因为它们是共享的,同一个对象。
cache元素可指定如下属性,每种属性的指定都是针对都是针对底层Cache的一种装饰,采用的是装饰器的模式。
Ø blocking:默认为false,当指定为true时将采用BlockingCache进行封装,blocking,阻塞的意思,使用BlockingCache会在查询缓存时锁住对应的Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,这样可以阻止并发情况下多个线程同时查询数据,详情可参考BlockingCache的源码。
Ø eviction:eviction,驱逐的意思。也就是元素驱逐算法,默认是LRU,对应的就是LruCache,其默认只保存1024个Key,超出时按照最近最少使用算法进行驱逐,详情请参考LruCache的源码。如果想使用自己的算法,则可以将该值指定为自己的驱逐算法实现类,只需要自己的类实现Mybatis的Cache接口即可。除了LRU以外,系统还提供了FIFO(先进先出,对应FifoCache)、SOFT(采用软引用存储Value,便于垃圾回收,对应SoftCache)和WEAK(采用弱引用存储Value,便于垃圾回收,对应WeakCache)这三种策略。
Ø flushInterval:清空缓存的时间间隔,单位是毫秒,默认是不会清空的。当指定了该值时会再用ScheduleCache包装一次,其会在每次对缓存进行操作时判断距离最近一次清空缓存的时间是否超过了flushInterval指定的时间,如果超出了,则清空当前的缓存,详情可参考ScheduleCache的实现。
Ø readOnly:是否只读,默认为false。当指定为false时,底层会用SerializedCache包装一次,其会在写缓存的时候将缓存对象进行序列化,然后在读缓存的时候进行反序列化,这样每次读到的都将是一个新的对象,即使你更改了读取到的结果,也不会影响原来缓存的对象,即非只读,你每次拿到这个缓存结果都可以进行修改,而不会影响原来的缓存结果;当指定为true时那就是每次获取的都是同一个引用,对其修改会影响后续的缓存数据获取,这种情况下是不建议对获取到的缓存结果进行更改,意为只读。这是Mybatis二级缓存读写和只读的定义,可能与我们通常情况下的只读和读写意义有点不同。每次都进行序列化和反序列化无疑会影响性能,但是这样的缓存结果更安全,不会被随意更改,具体可根据实际情况进行选择。详情可参考SerializedCache的源码。
Ø size:用来指定缓存中最多保存的Key的数量。其是针对LruCache而言的,LruCache默认只存储最多1024个Key,可通过该属性来改变默认值,当然,如果你通过eviction指定了自己的驱逐算法,同时自己的实现里面也有setSize方法,那么也可以通过cache的size属性给自定义的驱逐算法里面的size赋值。
Ø type:type属性用来指定当前底层缓存实现类,默认是PerpetualCache,如果我们想使用自定义的Cache,则可以通过该属性来指定,对应的值是我们自定义的Cache的全路径名称。
8.2.2.2cache-ref元素定义
cache-ref元素可以用来指定其它Mapper.xml中定义的Cache,有的时候可能我们多个不同的Mapper需要共享同一个缓存的,是希望在MapperA中缓存的内容在MapperB中可以直接命中的,这个时候我们就可以考虑使用cache-ref,这种场景只需要保证它们的缓存的Key是一致的即可命中,二级缓存的Key是通过Executor接口的createCacheKey()方法生成的,其实现基本都是BaseExecutor,源码如下。
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
打个比方我想在PersonMapper.xml中的查询都使用在UserMapper.xml中定义的Cache,则可以通过cache-ref元素的namespace属性指定需要引用的Cache所在的namespace,即UserMapper.xml中的定义的namespace,假设在UserMapper.xml中定义的namespace是com.elim.learn.mybatis.dao.UserMapper,则在PersonMapper.xml的cache-ref应该定义如下。
<cache-ref namespace="com.elim.learn.mybatis.dao.UserMapper"/>
8.2.3自定义cache
前面提到Mybatis的Cache默认会使用PerpetualCache存储数据,如果我们不想按照它的逻辑实现,或者我们想使用其它缓存框架来实现,比如使用Ehcache、Redis等,这个时候我们就可以使用自己的Cache实现,Mybatis是给我们留有对应的接口,允许我们进行自定义的。要想实现自定义的Cache我们必须定义一个自己的类来实现Mybatis提供的Cache接口,实现对应的接口方法。注意,自定义的Cache必须包含一个接收一个String参数的构造方法,这个参数就是Cache的ID,详情请参考Mybatis初始化Cache的过程,对应XMLMapperBuilder的cacheElement()方法。以下是一个简单的MyCache的实现。
/**
* @author Elim
* 2016年12月20日
*/
publicclass MyCache implements Cache {
private String id;
private String name;//Name,故意加这么一个属性,以方便演示给自定义Cache的属性设值
private Map<Object, Object> cache = new HashMap<Object, Object>();
/**
* 构造方法。自定义的Cache实现一定要有一个id参数
* @param id
*/
public MyCache(String id) {
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
this.cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return this.cache.get(key);
}
@Override
public Object removeObject(Object key) {
return this.cache.remove(key);
}
@Override
public void clear() {
this.cache.clear();
}
@Override
public int getSize() {
return this.cache.size();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
}
定义了自己的Cache实现类后我们就可以在需要使用它的Mapper.xml文件中通过<cache>标签的type属性来指定我们需要使用的Cache。如果我们的自定义Cache是需要指定参数的,则可以通过<cache>标签的子标签<property>来指定对应的参数,Mybatis在解析的时候会调用指定属性对应的set方法。针对于上面的自定义Cache,我们的配置如下。
<cache type="com.elim.learn.mybatis.cache.MyCache">
<property name="name" value="调用setName()方法需要传递的参数值"/>
</cache>
8.2.4缓存的清除
二级缓存默认是会在执行update、insert和delete语句时进行清空的,具体可以参考CachingExecutor的update()实现。如果我们不希望在执行某一条更新语句时清空对应的二级缓存,那么我们可以在对应的语句上指定flushCache属性等于false。如果只是某一条select语句不希望使用二级缓存和一级缓存,则也可以在对应的select元素上加上flushCache="true"。
<insert id="delete" parameterType="java.lang.Long" flushCache="false">
delete t_person where id=#{id}
</insert>
8.2.5自己操作Cache
Mybatis中创建的二级缓存都会交给Configuration进行管理,Configuration类是Mybatis的核心类,里面包含了各种Mybatis资源的管理,其可以很方便的通过SqlSession、SqlSessionFactory获取,如有需要我们可以直接通过它来操作我们的Cache。
@Test
public void testGetCache() {
Configuration configuration = this.session.getConfiguration();
// this.sessionFactory.getConfiguration();
Collection<Cache> caches = configuration.getCaches();
System.out.println(caches);
}
8.2.6测试
针对二级缓存进行了以下测试,获取两个不同的SqlSession执行两条相同的SQL,在未指定Cache时Mybatis将查询两次数据库,在指定了Cache时Mybatis只查询了一次数据库,第二次是从缓存中拿的。
@Test
public void testCache2() {
SqlSession session1 = this.sessionFactory.openSession();
SqlSession session2 = this.sessionFactory.openSession();
session1.getMapper(PersonMapper.class).findById(5L);
session1.commit();
session2.getMapper(PersonMapper.class).findById(5L);
}
注意在上面的代码中,我在session1执行完对应的SQL后调用了session1的commit()方法,即提交了它的事务,这样我们在第二次查询的时候才会缓存命中,才不会查询数据库,否则就会连着查询两次数据库。这是因为在CachingExecutor中Mybatis在查询的过程中又在原来Cache的基础上包装了TransactionalCache,这个Cache只会在事务提交后才真正的写入缓存,所以在上面的示例中,如果session1执行完SQL后没有马上commit就紧接着用session2执行SQL,虽然session1查询时没有缓存命中,但是此时写入缓存操作还没有进行,session2再查询的时候也就不会缓存命中了。
参考文档
http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html#cache
(注:本文是基于Mybatis3.3.1所写,写于2016年12月21日星期三)