文章目录
系列文章索引
MyBatis源码分析(一)MyBatis整体架构分析
MyBatis源码分析(二)SqlSessionFactory的构建及配置文件读取过程
MyBatis源码分析(二、续)SqlSource创建流程,SQL如何解析?如何将#{id}变成?的
MyBatis源码分析(三)SqlSession的执行主流程
MyBatis源码分析(四)插件拦截器的原理及使用
MyBatis源码分析(四、续)MyBatis分页插件拦截器设计与实现
MyBatis源码分析(五)一级缓存与二级缓存的原理
MyBatis源码分析(六)MetaObject工具类的使用与源码分析
MyBatis源码分析(七)MyBatis与Spring的整合原理与源码分析
深入理解JDK动态代理原理,使用javassist动手写一个动态代理框架
一、初识MyBatis缓存
缓存就是内存中的数据,常常来自对数据库查询结果的保存。使用缓存,我们可以避免频繁的与数据库进行交互,进而提高响应速度MyBatis也提供了对缓存的支持,分为一级缓存和二级缓存,可以通过下图来理解:
一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
一、一级缓存
1、编码验证
一级缓存默认是开启的,我们编码来验证一下一级缓存:
String resource = "com/test/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(1); // 第一次查询
System.out.println(blog);
BlogMapper mapper2 = session.getMapper(BlogMapper.class);
Blog blog2 = mapper2.selectBlog(1);// 第二次查询
System.out.println(blog2);
}
我们可以发现,控制台只输出了一次MyBatis的sql日志,并且blog和blog2指向同一个对象。
2、一级缓存原理
在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)
@Override
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;
}
在查询数据库的逻辑中,对每次的查询结果都会做缓存:
// 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 {
List<E> list;
//1. 把key存入缓存,value放一个占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//2. 与数据库交互
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//3. 如果第2步出了什么异常,把第1步存入的key删除
localCache.removeObject(key);
}
//4. 把结果存入缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
这也就是MyBatis的一级缓存,默认就是开启的而且无法关闭。
3、一级缓存数据结构
一级缓存就是PerpetualCache,它使用HashMap进行缓存:
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
4、缓存key的生成
在CachingExecutor的query方法中,会首先生成一个缓存key:
// org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);// 生成缓存key
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
// org.apache.ibatis.executor.BaseExecutor#createCacheKey
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//初始化CacheKey
CacheKey cacheKey = new CacheKey();
//存入statementId
cacheKey.update(ms.getId());
//分别存入分页需要的Offset和Limit
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
//把从BoundSql中封装的sql取出并存入到cacheKey对象中
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
//下面这一块就是封装参数
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
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);
}
}
//从configuration对象中(也就是载入配置文件后存放的对象)把EnvironmentId存入
/**
* <environments default="development">
* <environment id="development"> //就是这个id
* <!--当前事务交由JDBC进行管理-->
* <transactionManager type="JDBC"></transactionManager>
* <!--当前使用mybatis提供的连接池-->
* <dataSource type="POOLED">
* <property name="driver" value="${jdbc.driver}"/>
* <property name="url" value="${jdbc.url}"/>
* <property name="username" value="${jdbc.username}"/>
* <property name="password" value="${jdbc.password}"/>
* </dataSource>
* </environment>
* </environments>
*/
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
其中,cacheKey的update方法,将这些参数进行一些计算,生成一个唯一的key。
5、清空一级缓存
(1)手动清空一级缓存
org.apache.ibatis.session.SqlSession 中有一个和缓存有关的方法—— clearCache() 清空缓存的方法:
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(1);
System.out.println(blog);
session.clearCache(); // 清空一级缓存
BlogMapper mapper2 = session.getMapper(BlogMapper.class);
Blog blog2 = mapper2.selectBlog(1);
System.out.println(blog2);
System.out.println(blog == blog2);
}
调用了clearCache方法之后,缓存被清空,再次执行相同的方法后,又会重新查询数据库。
我们看一下clearCache源码:
// org.apache.ibatis.session.defaults.DefaultSqlSession#clearCache
@Override
public void clearCache() {
executor.clearLocalCache();
}
// org.apache.ibatis.executor.CachingExecutor#clearLocalCache
@Override
public void clearLocalCache() {
delegate.clearLocalCache();
}
// org.apache.ibatis.executor.BaseExecutor#clearLocalCache
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
最终将缓存的Map进行清空。
(2)自动清空一级缓存
如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
一级缓存时执行commit,close,增删改等操作,就会清空当前的一级缓存;当对SqlSession执行更新操作(update、delete、insert)后并执行commit时,不仅清空其自身的一级缓存(执行更新操作的效果),也清空二级缓存(执行commit()的效果)。
从下面源码我们也可以看出,在执行BaseExecutor的update方法时,会清空一级缓存:
// org.apache.ibatis.executor.BaseExecutor#update
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache(); // 清除本地缓存
return doUpdate(ms, parameter);
}
// org.apache.ibatis.executor.BaseExecutor#clearLocalCache
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();// 存储过程用
}
}
二、二级缓存
Mybatis的二级缓存不是默认开启的,是需要经过配置才能使用的
1、编码验证
(1)开启映射器配置文件中的缓存配置:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
(2)在需要使用二级缓存的Mapper配置文件中配置标签
<!--type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
eviction: 定义回收的策略,常见的有FIFO,LRU。
flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
size: 最多缓存对象的个数。
readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
-->
<cache></cache>
(3)在具体CURD标签上配置 useCache=true
<select id="selectBlog" resultType="com.demo.Blog" useCache="true">
select * from blog where id = #{id}
</select>
(4)编码实现
实体类要实现Serializable接口,因为二级缓存会将对象写进硬盘,就必须序列化,以及兼容对象在网络中的传输。
try (SqlSession session = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(1);
System.out.println(blog);
session.commit(); // 必须调用sqlSession1.commit()或者close(),一级缓存中的内容才会刷新到二级缓存中
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
Blog blog2 = mapper2.selectBlog(1);
System.out.println(blog2);
System.out.println(blog == blog2); // false 二级缓存,缓存的是数据本身
}
2、标签 < cache/> 的解析
二级缓存和具体的命名空间绑定,一个Mapper中有一个Cache, 相同Mapper中的MappedStatement共用同一个Cache
在解析Mapper.xml中:
// org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers")); // 解析mapper
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
// org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse(); // 执行解析的方法
}
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
//org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper")); // 解析mapper属性
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
// org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
// 关于cache属性的处理
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
// 这里会将生成的Cache包装到对应的MappedStatement
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
// org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement
private void cacheElement(XNode context) {
if (context != null) {
//解析<cache/>标签的type属性,这里我们可以自定义cache的实现类,比如redisCache,如果没有自定义,这里使用和一级缓存相同的PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
// 构建Cache对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
构建Cache对象的过程:
// 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) {
// 1.生成Cache对象
Cache cache = new CacheBuilder(currentNamespace)
//这里如果我们定义了<cache/>中的type,就使用自定义的Cache,否则使用和一级缓存相同的PerpetualCache
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 2.添加到Configuration中
configuration.addCache(cache);
// 3.并将cache赋值给MapperBuilderAssistant.currentCache
currentCache = cache;
return cache;
}
在创建MappedStatement时,将Mapper中创建的Cache对象,加入到了每个MappedStatement对象中,也就是同一个Mapper中所有的MappedStatement中的cache属性引用的是同一个:
// org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement(java.lang.String, org.apache.ibatis.mapping.SqlSource, org.apache.ibatis.mapping.StatementType, org.apache.ibatis.mapping.SqlCommandType, java.lang.Integer, java.lang.Integer, java.lang.String, java.lang.Class<?>, java.lang.String, java.lang.Class<?>, org.apache.ibatis.mapping.ResultSetType, boolean, boolean, boolean, org.apache.ibatis.executor.keygen.KeyGenerator, java.lang.String, java.lang.String, java.lang.String, org.apache.ibatis.scripting.LanguageDriver, java.lang.String)
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);// 在这里将之前生成的Cache封装到MappedStatement
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
3、查询过程中二级缓存源码分析
在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)
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建 CacheKey,一级缓存和二级缓存共用一个
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 {
// 从 MappedStatement 中获取 Cache,注意这里的 Cache 是从MappedStatement中获取的
// 也就是我们上面解析Mapper中<cache/>标签中创建的,它保存在Configration中
// 我们在上面解析Mapper.xml时分析过每一个MappedStatement都有一个Cache对象,就是这里
Cache cache = ms.getCache();
// 如果配置文件中没有配置 <cache>,则 cache 为空
if (cache != null) {
//如果需要刷新缓存的话就刷新:flushCache="true"
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 访问二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
// 缓存未命中
if (list == null) {
// 如果没有值,则执行查询,这个查询实际也是先走一级缓存查询,一级缓存也没有的话,则进行DB查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 缓存查询结果
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果设置了flushCache=“true”,则每次查询都会刷新缓存:
<!-- 执行此语句清空缓存 -->
<select id="findbyId" resultType="com.itheima.pojo.user" useCache="true" flushCache="true" >
select * from t_demo
</select>
4、TransactionalCacheManager
注意二级缓存是从 MappedStatement 中获取的。由于 MappedStatement 存在于全局配置中,可以多个 CachingExecutor 获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个事务共用一个缓存实例,会导致脏读问题。至于脏读问题,需要借助其他类来处理,也就是上面代码中 tcm 变量对应的类型TransactionalCacheManager 。
/** 事务缓存管理器 */
public class TransactionalCacheManager {
// Cache 与 TransactionalCache 的映射关系表
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
// 获取 TransactionalCache 对象,并调用该对象的 clear 方法
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
// 直接从TransactionalCache中获取缓存
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
// 直接存入TransactionalCache的缓存中
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
// 从映射表中获取 TransactionalCache
// TransactionalCache 也是一种装饰类,为 Cache 增加事务功能
// 如果没有的话,创建一个新的TransactionalCache,并将真正的Cache对象存进去
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
}
TransactionalCacheManager 内部维护了 Cache 实例与 TransactionalCache 实例间的映射关系,该类也仅负责维护两者的映射关系,真正做事的还是 TransactionalCache。
TransactionalCache 是一种缓存装饰器,可以为 Cache 实例增加事务功能。下面分析一下该类的逻辑。
/**
第二级缓存事务缓冲区。
该类保存会话期间要添加到第2级缓存中的所有缓存项。当调用commit时,表项被发送到缓存,如果Session回滚则被丢弃。增加了阻塞缓存支持。因此,任何返回缓存缺失的get()将被一个put()紧跟,这样任何与该键相关的锁都可以被释放。
**/
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
//真正的缓存对象,和上面的Map<Cache, TransactionalCache>中的Cache是同一个
private final Cache delegate;
private boolean clearOnCommit;
// 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中
private final Map<Object, Object> entriesToAddOnCommit;
// 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public Object getObject(Object key) {
// issue #116
// 查询的时候是直接从delegate中去查询的,也就是从真正的缓存对象中查询
Object object = delegate.getObject(key);
if (object == null) {
// 缓存未命中,则将 key 存入到 entriesMissedInCache 中
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
// 将键值对存入到 entriesToAddOnCommit 这个Map中中,而非真实的缓存对象 delegate 中
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
clearOnCommit = true;
// 清空 entriesToAddOnCommit,但不清空 delegate 缓存
entriesToAddOnCommit.clear();
}
public void commit() {
// 根据 clearOnCommit 的值决定是否清空 delegate
if (clearOnCommit) {
delegate.clear();
}
// 刷新未缓存的结果到 delegate 缓存中
flushPendingEntries();
// 重置 entriesToAddOnCommit 和 entriesMissedInCache
reset();
}
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
// 清空集合
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 将 entriesToAddOnCommit 中的内容转存到 delegate 中
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
// 存入空值
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
// 调用 removeObject 进行解锁
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifying a rollback to the cache adapter. "
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}
存储二级缓存对象的时候是放到了TransactionalCache.entriesToAddOnCommit这个map中,但是每次查询的时候是直接从TransactionalCache.delegate中去查询的,所以这个二级缓存查询数据库后,设置缓存值是没有立刻生效的,主要是因为直接存到 delegate 会导致脏数据问题。
5、为何只有SqlSession提交或关闭之后才存储缓存
DefaultSqlSession:
@Override
public void commit(boolean force) {
try {
// 主要是这句
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public void close() {
try {
// 主要是这句
executor.close(isCommitOrRollbackRequired(false));
closeCursors();
dirty = false;
} finally {
ErrorContext.instance().reset();
}
}
CachingExecutor:
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit(); // 调用TransactionalCacheManager的commit
}
TransactionalCacheManager:
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit(); // 调用TransactionalCache的commit
}
}
TransactionalCache:
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 在这里真正的将entriesToAddOnCommit的对象逐个添加到delegate中,只有这时,二级缓存才真正的生效
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
6、二级缓存的更新
调用Executor的update时,会先清空二级缓存:
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache); // 清空缓存
}
}
7、二级缓存总结
MyBatis二级缓存只适用于不常进行增、删、改的数据,比如国家行政区省市区街道数据。一但数据变更,MyBatis会清空缓存。因此二级缓存不适用于经常进行更新的数据。
在二级缓存的设计上,MyBatis大量地运用了装饰者模式,如CachingExecutor, 以及各种Cache接口的装饰器。
二级缓存的刷新:
二级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别
二级缓存具有丰富的缓存策略。
二级缓存可由多个装饰器,与基础缓存组合而成
二级缓存工作由 一个缓存装饰执行器CachingExecutor和 一个事务型预缓存TransactionalCache 完成
写在后面
如果本文对你有帮助,请点赞收藏关注一下吧 ~