前言:
有点项目经验的朋友都知道缓存的重要性是不言而喻的,不仅仅我们在开发项目业务功能的时候使用了各种缓存,框架在设计的时候也有框架层面的缓存,尤其在查询多的场景下,缓存可以大大的减少数据库访问,提升系统效率,Mybatis 也提供了缓存,分别为一级缓存和二级缓存,默认的情况下,Mybatis 只开启一级缓存。
Mybatis 相关知识传送门
初识 MyBatis 【MyBatis 核心概念】
MyBatis 源码分析–SqlSessionFactory
MyBatis 源码分析–获取SqlSession
MyBatis 源码分析-- getMapper(获取Mapper)
MyBatis 源码分析-- SQL请求执行流程( Mapper 接口方法的执行的过程)
MyBatis 源码分析-- 插件(拦截器)原理
MyBatis 插件(拦截器)实战(自定义实现拦截器)
一级缓存
什么是一级缓存?
一级缓存是 SqlSession 级别的缓存,在操作数据库时需要构造 SqlSession 对象,在对象中有一个 Map(HashMap)用于存储缓存数据,不同的 SqlSession 之间的缓存是互相不影响的,即同一个 SqlSession 对象, 在参数和 SQL 完全一样的情况下,多次查询只执行一次 SQL 语句,因为第一次查询后,MyBatis 会将其放在缓存中,后面再次查询的时候,如果没有声明需要刷新,且缓存没有超时的情况下,SqlSession 会取出当前缓存的数据,不会再次发送 SQL 到数据库进行查询,这就是一级缓存。
一级缓存编码验证:
代码示例:
@Transactional(rollbackFor = Exception.class)
@Override
public void queryById(Long id) {selectById(id);log.info("第一次查询");selectById(id);log.info("第二次查询");
}
演示结果:
通过输出日志可以知道,两次查询只执行了一次查询数据库的操作,表名一级缓存生效了(Mybatis 默认开启一级缓存)。
注意:如果想要使用 Mybatis 的一级缓存,需要保证是在同一个事务中,只有在同一个事务中,才能获取到同一个 SqlSession。
一级缓存失效的场景:
- 使用不同的 SqlSession。
- 两次查询的查询条件不一致。
- 两次查询之间有增删改操作。
- 两次查询之间手动清除了一级缓存。
一级缓存源码分析
一级缓存的原理其实在前文分支 SQL 的执行过程的时候已经分析过了,这里我们在回忆一下,在 BaseExecutor#query 方法中会判断一级缓存中是否有数据,有就返回,没有才会去查询数据库。
//org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
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 (this.closed) {throw new ExecutorException("Executor was closed.");} else {//queryStack 查询堆栈 防止递归查询重复处理缓存 是否刷新缓存 flushCacheRequired 是 true 清除一级缓存if (this.queryStack == 0 && ms.isFlushCacheRequired()) {//清除一级缓存this.clearLocalCache();}List list;try {//查询堆栈 +1++this.queryStack;//从一级缓存中获取数据list = resultHandler == null ? (List)this.localCache.getObject(key) : null;if (list != null) {//从一级缓存中获取数据this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {//从数据库查询数据list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {//查询堆栈-1--this.queryStack;}if (this.queryStack == 0) {Iterator i$ = this.deferredLoads.iterator();while(i$.hasNext()) {BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next();deferredLoad.load();}this.deferredLoads.clear();if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {this.clearLocalCache();}}return list;}
}
一级缓存的存入时机就是每次查询数据库的时候,只要查询了数据库,结果都会缓存到一级缓存中,这也就是一级缓存默认打开的原因。
//org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {//一级缓存 存储一个占位符this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);List list;try {//执行数据库查询list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {//清除占位符缓存this.localCache.removeObject(key);}//存入一级缓存this.localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {this.localOutputParameterCache.putObject(key, parameter);}return list;
}
我们知道 Mybatis 的 INSERT、UPDATE、DELETE 最终执行的都是 update 方法,在 update 方法中我们看到了清除一级缓存的代码,这就是为什么两次查询之间有 INSERT、UPDATE、DELETE 操作时候,就无法使用一级缓存的原因。
//org.apache.ibatis.executor.BaseExecutor#update
public int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());if (this.closed) {throw new ExecutorException("Executor was closed.");} else {//清除一级缓存this.clearLocalCache();return this.doUpdate(ms, parameter);}
}
一级缓存源码结构
一级缓存的源码结构很简单,就是一个简单的 Map 结构,提供了一些常用的 put、get、remove、clear 方法,本质就是内存中的一个 Map。
public class PerpetualCache implements Cache {private final String id;private final Map<Object, Object> cache = new HashMap();public PerpetualCache(String id) {this.id = id;}public String getId() {return this.id;}public int getSize() {return this.cache.size();}public void putObject(Object key, Object value) {this.cache.put(key, value);}public Object getObject(Object key) {return this.cache.get(key);}public Object removeObject(Object key) {return this.cache.remove(key);}public void clear() {this.cache.clear();}public boolean equals(Object o) {if (this.getId() == null) {throw new CacheException("Cache instances require an ID.");} else if (this == o) {return true;} else if (!(o instanceof Cache)) {return false;} else {Cache otherCache = (Cache)o;return this.getId().equals(otherCache.getId());}}public int hashCode() {if (this.getId() == null) {throw new CacheException("Cache instances require an ID.");} else {return this.getId().hashCode();}}
}
缓存 key 的生成方法 createCacheKey 源码分析
从源码中我们可以知道 cacheKey 是根据 MappedStatement id + RowBounds offset + RowBounds limit + SQL + Parameter值 + Environment id 来确定唯一性的。
//org.apache.ibatis.executor.BaseExecutor#createCacheKey
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {//判断执行器是否关闭if (this.closed) {throw new ExecutorException("Executor was closed.");} else {//创建 缓存key 对象CacheKey cacheKey = new CacheKey();//根据接口的全限定类名+方法名更新 cacheKeycacheKey.update(ms.getId());//根据查询数据的偏移量更新 cacheKeycacheKey.update(rowBounds.getOffset());//根据查询数据的条数更新 cacheKeycacheKey.update(rowBounds.getLimit());//根据sql 语句更新 cacheKeycacheKey.update(boundSql.getSql());//其实是从查询 sql 占位符中解析出的参数元数据信息 然后进行遍历 获取每个参数名 并从查询接口传入的参数中获取相应的参数值List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();//获取类型处理器注册表TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();//循环遍历入参 for(int i = 0; i < parameterMappings.size(); ++i) {//获取参数映射ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);//参数类型判断判断if (parameterMapping.getMode() != ParameterMode.OUT) {//获取参数属性名称String propertyName = parameterMapping.getProperty();//参数值Object value;//是否有附加参数if (boundSql.hasAdditionalParameter(propertyName)) {//根据参数名称获取参数值value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {//参数为空value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {//参数类型注册表中有参数类型 value = parameterObject;} else {//MetaObject提供了一些便捷的方法 可以根据 parameterObject 类型 获取相应 propertyName 值的方法 不需要直接使用反射操作MetaObject metaObject = this.configuration.newMetaObject(parameterObject);//使用 metaObject 获取参数值value = metaObject.getValue(propertyName);}//根据环境id更新 cacheKeyif (this.configuration.getEnvironment() != null) {cacheKey.update(this.configuration.getEnvironment().getId());}//根据环境 value 更新 cacheKeycacheKey.update(value);}}return cacheKey;}
}
CacheKey#update 方法源码分析
CacheKey#update 会把生产的唯一 CacheKey 进行 HashCode,并更新到 updateList 中,判断两个 CacheKey 对象相等的充分必要条件是两个对象代表的组合序列中的每个元素必须都相等,为了避免每次比较都要进行一次循环(遍历组合List),CacheKey 采用 hashCode–>checksum–>count–>updateList 的顺序比较,只要有一个不相等,则视为两个 CacheKey 对象不相等。
//org.apache.ibatis.cache.CacheKey#update
public void update(Object object) {//如果 object 为空 hashcode 就为1 不为 null 就获取 hashcodeint baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);//count ++++this.count;//checksum 等于 checksum +object的 hashcodethis.checksum += (long)baseHashCode;//baseHashCode 等于 baseHashCode 乘以 countbaseHashCode *= this.count;//重新计算 hashcodethis.hashcode = this.multiplier * this.hashcode + baseHashCode;//更新到列表中this.updateList.add(object);
}
二级缓存
什么是二级缓存?
二级缓存是存户在 SqlSessionFactory 中,二级缓存和名称空间绑定,也就是说通常所说的二级缓存是 Mapper 级别的缓存,即多个 SqlSession 共享同一个 Mapper 命名空间下的缓存,它的作用是缓存 Mapper 执行的结果,避免频繁地访问数据库,提高系统的性能。
配置二级缓存
Mybatis 二级缓存默认是关闭的,二级缓存有全局开关、局部开关和在某个查询 SQL 上配置使用缓存,可以根据自己的需求灵活配置。
全局开启二级缓存:
在 mybatis-config.xml 中如下配置。
<settings><setting name="cacheEnabled" value="true"/>
</settings>
局部开启二级缓存:
在 Mapper.xml 文件中开启二级缓存。
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
标签解释:
- eviction:清除缓存的策略,FIFO 表示先进先出的策略,默认是 LRU 最近最少使用。
- flushInterval:缓存刷新时间间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值。
- size:缓存存放多少个元素。
- readOnly:是否只读,true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。
- type:指定自定义缓存的全类名,自己实现Cache 接口即可。
- blocking:缓存中找不到对应的key,是否会一直 blocking,直到有对应的数据进入缓存。
eviction 缓存回收策略
- FIFO :先进先出,按照缓存进入的顺序来移除。
- LRU:最近最少使用,移除最长时间不被使用的对象。
- SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象。
- WEAK:弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象。
在某个查询 SQL 上配置使用缓存,如下:
<select id="selectUser" resultType="com.my.study.User" useCache="true" flushCache="true">select * from user where id = #{id}
</select>
二级缓存的创建
二级缓存的创建,其实就是解析 标签,这个解析动作是在构建 SqlSessionFactory 的过程中做的,前文在分析 SqlSessionFactory 的创建过程中有详细讲解,这里只分析 解析 标签的源码,如下:
SqlSession 创建传送门
//org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
private void configurationElement(XNode context) {try {//获取 namespaceString namespace = context.getStringAttribute("namespace");if (namespace != null && !namespace.isEmpty()) {//设置 namespacethis.builderAssistant.setCurrentNamespace(namespace);//解析 cache-ref 标签this.cacheRefElement(context.evalNode("cache-ref"));//解析 cache 标签this.cacheElement(context.evalNode("cache"));//解析映射参数 parameterMap this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));//解析结果集 resultMapthis.resultMapElements(context.evalNodes("/mapper/resultMap"));//解析 sqlthis.sqlElement(context.evalNodes("/mapper/sql"));//构建 crud 语句 this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));} else {throw new BuilderException("Mapper's namespace cannot be empty");}} catch (Exception var3) {throw new BuilderException("Error parsing Mapper XML. The XML location is '" + this.resource + "'. Cause: " + var3, var3);}
}
XMLMapperBuilder#cacheElement 源码分析
XMLMapperBuilder#cacheElement 源码就很简单了,其实就是对 Cache 标签的解析,解析 Cache 标签的各个属性,然后构建一个 Cache 对象返回。
//org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement
private void cacheElement(XNode context) {if (context != null) {//获取缓存类名 这里如果我们定义了<cache/>中的 type 就使用自定义的Cache类 否则使用和一级缓存相同的PerpetualCacheString type = context.getStringAttribute("type", "PERPETUAL");//从类型别名注册表中获取缓存类名的 classClass<? extends Cache> typeClass = this.typeAliasRegistry.resolveAlias(type);//缓存淘汰策略 默认 LRUString eviction = context.getStringAttribute("eviction", "LRU");//从类型别名注册表中获取缓存类名的 classClass<? extends Cache> evictionClass = this.typeAliasRegistry.resolveAlias(eviction);//获取缓存刷新时间间隔Long flushInterval = context.getLongAttribute("flushInterval");//缓存存放的元素个数Integer size = context.getIntAttribute("size");//只读属性 默认 falseboolean readWrite = !context.getBooleanAttribute("readOnly", false);//缓存中找不到对应的key 是否会一直 blocking 直到有对应的数据进入缓存 默认 falseboolean blocking = context.getBooleanAttribute("blocking", false);Properties props = context.getChildrenAsProperties();//构建一个新的二级缓存对象this.builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);}}//org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) {//创建 cache 对象Cache cache = (new CacheBuilder(this.currentNamespace)).implementation((Class)this.valueOrDefault(typeClass, PerpetualCache.class)).addDecorator((Class)this.valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size).readWrite(readWrite).blocking(blocking).properties(props).build();//添加到 configuration 中this.configuration.addCache(cache);//将 cache 赋值给 MapperBuilderAssistant.currentCachethis.currentCache = cache;return cache;
}
二级缓存在查询过程中的使用源码分析
二级缓存的使用再 CachingExecutor#query 方法中有体现,如下:
//CachingExecutor#query 方法会从 MappedStatement 中获取 SQL 信息,创建缓存 key 并执行查询,Configuration 中 cacheEnabled 属性值默认为 true,因此会执行 CachingExecutor 的 query方法。
//org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {//获取 SQL 基本信息BoundSql boundSql = ms.getBoundSql(parameterObject);//创建缓存 keyCacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);//执行查询return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}//CachingExecutor#query 会判断是否使用了缓存,如果允许使用缓存会先从二级缓存查询,二级缓存中查询不到才会去查询一级缓存或者数据库,如果二级缓存为空或者不允许使用缓存就会直接去查询一级缓存或者数据库。
//org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
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) {//是否刷新缓存//select 时候 flushCacheRequired 默认是false 不清除二级缓存 //insert update delete 时候 flushCacheRequired 是 true 会清除二级缓存this.flushCacheIfRequired(ms);//如果使用了缓存 ResultHandler 不为空 if (ms.isUseCache() && resultHandler == null) {//对存储过程的处理 确保没有存储过程 如果有存储过程 就报错this.ensureNoOutParams(ms, parameterObject, boundSql);//从二级缓存中查询数据 TransactionalCacheManagerList<E> list = (List)this.tcm.getObject(cache, key);//结果为空 判断if (list == null) {//为空 BaseExexutor 执行查询数据库list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);//加入二级缓存this.tcm.putObject(cache, key, list);}return list;}}//缓存为空 BaseExexutor 直接查询数据库return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
TransactionalCacheManager 源码分析
二级缓存 Cache 对象是从 MappedStatement 中获取的,一个 Mapper 对应一个 MapperStatment,MappedStatement(SQL语句信息) 对象是存在全局配置中,多个 CachingExecutor 多可以获取到,这样就可以在多个线程之间共用了,会存在线程安全问题,同样多个 SqlSession 使用同一个 Cache 也会出现数据脏读问题,因此使用了事务缓存管理器 TransactionalCacheManager 来管理二级缓存,TransactionalCacheManager 使用一个 Map 维护了 Cache 和 TransactionalCache 的关系,并提供了管理缓存的方法,但真正管理缓存的时候 TransactionalCache。
package org.apache.ibatis.cache;import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.ibatis.cache.decorators.TransactionalCache;public class TransactionalCacheManager {//维护 Cache ransactionalCache 之间的关系private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap();public TransactionalCacheManager() {}//清除缓存public void clear(Cache cache) {this.getTransactionalCache(cache).clear();}//获取缓存public Object getObject(Cache cache, CacheKey key) {return this.getTransactionalCache(cache).getObject(key);}//存入缓存public void putObject(Cache cache, CacheKey key, Object value) {this.getTransactionalCache(cache).putObject(key, value);}public void commit() {Iterator var1 = this.transactionalCaches.values().iterator();while(var1.hasNext()) {TransactionalCache txCache = (TransactionalCache)var1.next();txCache.commit();}}public void rollback() {Iterator var1 = this.transactionalCaches.values().iterator();while(var1.hasNext()) {TransactionalCache txCache = (TransactionalCache)var1.next();txCache.rollback();}}//获取 TransactionalCacheprivate TransactionalCache getTransactionalCache(Cache cache) {return (TransactionalCache)this.transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);}
}
TransactionalCache 源码分析
TransactionalCache 是事务缓存装饰器,可以为 Cache 增加事务功能,真正操作缓存的方法都在 TransactionalCache 中,TransactionalCache 中真正的二级缓存存在在 delegate 中,读取缓存从 delegate 中读取,存入缓存的时候会先存入到 entriesToAddOnCommit 中,只有正真提交事务的时候,才会把缓存加入到 delegate 真正的二级缓存中。
package org.apache.ibatis.cache.decorators;import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;public class TransactionalCache implements Cache {private static final Log log = LogFactory.getLog(TransactionalCache.class);//二级缓存对象private final Cache delegate;//在commit的时候是否清除数据 标记位private boolean clearOnCommit;//存放缓存中没有的对象 只有在 commit 的时候 才会真正加入到缓存中private final Map<Object, Object> entriesToAddOnCommit;//事务提交前 在二级缓存中没有找到这个对象就加入 entriesMissedInCacheprivate final Set<Object> entriesMissedInCache;//构造方法public TransactionalCache(Cache delegate) {this.delegate = delegate;this.clearOnCommit = false;this.entriesToAddOnCommit = new HashMap();this.entriesMissedInCache = new HashSet();}public String getId() {return this.delegate.getId();}public int getSize() {return this.delegate.getSize();}//获取缓存public Object getObject(Object key) {//从二级缓存中获取对象Object object = this.delegate.getObject(key);//为空if (object == null) {//key 加入到 entriesMissedInCachethis.entriesMissedInCache.add(key);}//更新二级缓存的时候 不会直接清除二级缓存 而是会将 clearOnCommit 改为true//clearOnCommit 为true 返回 null 否则返回查询到的缓存对象return this.clearOnCommit ? null : object;}//存入缓存public void putObject(Object key, Object object) {//这里只是加入到 entriesToAddOnCommit 中this.entriesToAddOnCommit.put(key, object);}public Object removeObject(Object key) {return null;}public void clear() {this.clearOnCommit = true;this.entriesToAddOnCommit.clear();}//提交事务public void commit() {if (this.clearOnCommit) {//如果提交时候清除缓存 就执行清空操作this.delegate.clear();}//将 entriesToAddOnCommit 加入二级缓存this.flushPendingEntries();//加入完成之后 执行重置操作 重置 entriesToAddOnCommit entriesMissedInCache this.reset();}//回滚public void rollback() {//清除二级缓存this.unlockMissedEntries();//重置操作 重置 entriesToAddOnCommit entriesMissedInCache this.reset();}//重置操作private void reset() {this.clearOnCommit = false;this.entriesToAddOnCommit.clear();this.entriesMissedInCache.clear();}//刷新待处理的数据 其实就是加入二级缓存private void flushPendingEntries() {//迭代遍历 entriesToAddOnCommit Iterator var1 = this.entriesToAddOnCommit.entrySet().iterator();while(var1.hasNext()) {Entry<Object, Object> entry = (Entry)var1.next();//加入二级缓存this.delegate.putObject(entry.getKey(), entry.getValue());}//迭代遍历 entriesMissedInCachevar1 = this.entriesMissedInCache.iterator();while(var1.hasNext()) {Object entry = var1.next();if (!this.entriesToAddOnCommit.containsKey(entry)) {//entriesToAddOnCommit 中不存在 则加入二级缓存 value 为 nullthis.delegate.putObject(entry, (Object)null);}}}//如果回滚就执行该防范private void unlockMissedEntries() {//迭代遍历 entriesMissedInCacheIterator var1 = this.entriesMissedInCache.iterator();while(var1.hasNext()) {Object entry = var1.next();try {//从二级缓存中移出对象this.delegate.removeObject(entry);} catch (Exception var4) {log.warn("Unexpected exception while notifiying a rollback to the cache adapter. Consider upgrading your cache adapter to the latest version. Cause: " + var4);}}}
}
二级缓存生效的时机
我们根据 DefaultSqlSession#commit 方法的源码跟进去,发现最后调用了 TransactionalCache#commit 方法,也就是二级缓存的生效时机是在 SqlSession 提交之后,这样就避免了单机情况下的脏读问题。
//org.apache.ibatis.session.defaults.DefaultSqlSession#commit()
public void commit() {this.commit(false);
}//org.apache.ibatis.session.defaults.DefaultSqlSession#commit(boolean)
public void commit(boolean force) {try {this.executor.commit(this.isCommitOrRollbackRequired(force));this.dirty = false;} catch (Exception var6) {throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + var6, var6);} finally {ErrorContext.instance().reset();}}//org.apache.ibatis.executor.CachingExecutor#commit
public void commit(boolean required) throws SQLException {this.delegate.commit(required);this.tcm.commit();
}//org.apache.ibatis.cache.TransactionalCacheManager#commit
public void commit() {Iterator var1 = this.transactionalCaches.values().iterator();while(var1.hasNext()) {TransactionalCache txCache = (TransactionalCache)var1.next();txCache.commit();}}//org.apache.ibatis.cache.decorators.TransactionalCache#commit
public void commit() {if (this.clearOnCommit) {this.delegate.clear();}this.flushPendingEntries();this.reset();
}
二级缓存更新时机
同样从 DefaultSqlSession#update 方法入手分析,发现最终调用了 TransactionalCache#clear 方法,TransactionalCache#clear 方法并没有正在的更新或者清除二级缓存,而是将 clearOnCommit 提交事务时候是否清除缓存改为 true(默认是false),事务提交时候会进行判断,如果 clearOnCommit 为 true,就会清除二级缓存,同样在查询二级缓存的时候也会判断 clearOnCommit 属性,如果 clearOnCommit 为 true,则直接返回 null,来保证数据的准确性。
//org.apache.ibatis.session.defaults.DefaultSqlSession#update(java.lang.String)
public int update(String statement) {return this.update(statement, (Object)null);
}//org.apache.ibatis.session.defaults.DefaultSqlSession#update(java.lang.String, java.lang.Object)
public int update(String statement, Object parameter) {int var4;try {this.dirty = true;MappedStatement ms = this.configuration.getMappedStatement(statement);var4 = this.executor.update(ms, this.wrapCollection(parameter));} catch (Exception var8) {throw ExceptionFactory.wrapException("Error updating database. Cause: " + var8, var8);} finally {ErrorContext.instance().reset();}return var4;
}//org.apache.ibatis.executor.CachingExecutor#update
public int update(MappedStatement ms, Object parameterObject) throws SQLException {this.flushCacheIfRequired(ms);return this.delegate.update(ms, parameterObject);
}//org.apache.ibatis.executor.CachingExecutor#flushCacheIfRequired
private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache != null && ms.isFlushCacheRequired()) {this.tcm.clear(cache);}}//org.apache.ibatis.cache.TransactionalCacheManager#clear
public void clear(Cache cache) {this.getTransactionalCache(cache).clear();
}//org.apache.ibatis.cache.decorators.TransactionalCache#clear
public void clear() {this.clearOnCommit = true;this.entriesToAddOnCommit.clear();
}
总结:二级缓存实现了 Sqlsession 之间的缓存数据共享,属于 namespace/Mapper 级别的缓存,具有丰富的缓存淘汰策略,二级缓存使用 TransactionalCache 来解决脏读的问题,只有事务提交的时候,对应的数据才会放入到二级缓存中,来避免脏读问题,同时需要注意的是默认的情况下,Mybatis将一二级缓存都存储到本地缓存中,因此在分布式情况下,二级缓存还是会出现脏读问题,分布式情况下不建议使用二级缓存。
欢迎提出建议及对错误的地方指出纠正。