MyBatis一级缓存


MyBatis源码学习系列文章目录



前言

MyBatis为了减少对数据库的查询,避免频繁的数据库交互,提供了一级缓存和二级缓存。本文将对一级缓存进行介绍并结合源码分析如果关闭一级缓存


提示:以下是本篇文章正文内容,下面案例可供参考

一、一级缓存

一级缓存在MyBatis中对应的属性为org.apache.ibatis.executor.BaseExecutor#localCache。在构造BaseExecutor对象的时候就会实例化这个属性

protected PerpetualCache localCache; // 一级缓存

protected BaseExecutor(Configuration configuration, Transaction transaction) {
	this.transaction = transaction;
	this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
	this.localCache = new PerpetualCache("LocalCache");
	this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
	this.closed = false;
	this.configuration = configuration;
	this.wrapper = this;
}

MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入到上面的localCache当中。这个缓存对象如下

public class PerpetualCache implements Cache {

	private final String id;

	private Map<Object, Object> cache = new HashMap<Object, Object>();

	public PerpetualCache(String id) {
		this.id = id;
	}
    
    // 其他方法略
}	

不难看出,其实就是存储到一个HashMap当中的。在MyBatis执行查询方法的时候

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
		throws SQLException {
	BoundSql boundSql = ms.getBoundSql(parameter);
	CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
	return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

这里就是通过org.apache.ibatis.executor.BaseExecutor#createCacheKey这个方法来创建缓存的主键对象的。这里面的几个参数都决定了最后的缓存主键对象。其中MappedStatement代表的是MyBatis中mapper.xml文件中的带有<select>标签的语句,第二个parameter代表的是传入的参数,而第三个rowBounds是分页参数(一般仅在分页的时候使用),而boundSql是查询语句、参数以及参数类型的包装类。所以,最终决定两次查询是否是同一个的就是必须是同一个mapper文件中同一个<select>,也就同一个namespace+id,另外还有传入的参数必须一致,另外如果是分页语句,则分页的条件必须一致,如下所示:

cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());

一级缓存是存在于SqlSession的生命周期的,也叫本地缓存。为什么这么说呢?在这个类当中存在以下的方法

@Override
public void clearLocalCache() {
	if (!closed) {
		localCache.clear();
		localOutputParameterCache.clear();
	}
}

这个方法就是用于清空一级缓存的。查看这个方法的使用情况
在这里插入图片描述
只要对事务有一点了解的话,就不难看出在这里commit和rollback方法中都会调用清空缓存的方法,所以只要事务结束一级缓存就被清空了,而在MyBatis当中,SqlSession(会话)就代表一个事务了。在上面我们还可以看到在update方法中也调用了clearLocalCache方法,其实这里update代表了insert、update、delete三中类型的。以下为SqlSession接口在MyBatis中的唯一实现类DefaultSqlSession中的实现
在这里插入图片描述
所以,我们可以得出结论,任何的INSERT、UPDATE、DELETE操作都会清空一级缓存。
最后让我们来具体分析org.apache.ibatis.executor.BaseExecutor#query方法中一级缓存是如何起作用的。

@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.");
	}
	1. 如果<select>标签中flushCache属性设置为true的话 在查询之前就会清空缓存
	if (queryStack == 0 && ms.isFlushCacheRequired()) {
		clearLocalCache();
	}
	List<E> list;
	try {
		queryStack++;
		2. resultHandler如果为空 就会根据key查询一级缓存
		list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; 
		3. 通过一级缓存查询的结果不为空 则尝试处理缓存的参数 只在查询类型为CALLABLE有用 
		if (list != null) {
			handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
		} else {
		    4. 如果没有缓存数据 则执行数据库查询 并将查询结果缓存
			list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); 
		}
	} finally {
		queryStack--;
	}
	if (queryStack == 0) {
		for (DeferredLoad deferredLoad : deferredLoads) {
			deferredLoad.load();
		}
		// issue #601
		deferredLoads.clear();
		5. 如果localCacheScope设置为STATEMENT类型,则查询完就清空缓存(语句级别)
		if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
			// issue #482
			clearLocalCache();
		}
	}
	return list;
}

从上面的代码可以看出,一级缓存就是在执行数据库真实查询之前首先从本地缓存中读取,如果能够读取到,就不会去查询数据库。

二、关闭一级缓存

同时我们也不难看出,有两种方式可以关闭一级缓存。第一种方式:通过flushCache标签(设置为true每次查询之前都会清空缓存)

<select id="selectUserJobs1" resultMap="userAndJobs1" flushCache="true">

第二种方式:通过通过设置localCacheScope这个配置来关闭一级缓存。以下为MyBatis核心配置文件中的配置参数,其中localCacheScope就可以将一级缓存限定在语句级别,而不是SqlSession级别。从某种程度来说就是关闭了一级缓存。

<!-- 参数设置 -->
<settings>
	<!-- 这个配置使全局的映射器启用或禁用缓存 -->
	<setting name="cacheEnabled" value="true" />
	<!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载 -->
	<setting name="lazyLoadingEnabled" value="true" />
	<!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载 -->
	<setting name="aggressiveLazyLoading" value="true" />
	<!-- 允许或不允许多种结果集从一个单独的语句中返回(需要适合的驱动) -->
	<setting name="multipleResultSetsEnabled" value="true" />
	<!-- 使用列标签代替列名。不同的驱动在这方便表现不同。参考驱动文档或充分测试两种方法来决定所使用的驱动 -->
	<setting name="useColumnLabel" value="true" />
	<!-- 允许JDBC支持生成的键。需要适合的驱动。如果设置为true则这个设置强制生成的键被使用,尽管一些驱动拒绝兼容但仍然有效(比如Derby) -->
	<setting name="useGeneratedKeys" value="true" />
	<!-- 指定MyBatis如何自动映射列到字段/属性。PARTIAL只会自动映射简单,没有嵌套的结果。FULL会自动映射任意复杂的结果(嵌套的或其他情况) -->
	<setting name="autoMappingBehavior" value="PARTIAL" />
	<!--当检测出未知列(或未知属性)时,如何处理,默认情况下没有任何提示,这在测试的时候很不方便,不容易找到错误。 NONE : 不做任何处理 
		(默认值) WARNING : 警告日志形式的详细信息 FAILING : 映射失败,抛出异常和详细信息 -->
	<setting name="autoMappingUnknownColumnBehavior" value="WARNING" />
	<!-- 配置默认的执行器。SIMPLE执行器没有什么特别之处。REUSE执行器重用预处理语句。BATCH执行器重用语句和批量更新 -->
	<setting name="defaultExecutorType" value="REUSE" />
	<!-- 设置超时时间,它决定驱动等待一个数据库响应的时间 -->
	<setting name="defaultStatementTimeout" value="25000" />
	<!--设置查询返回值数量,可以被查询数值覆盖 -->
	<setting name="defaultFetchSize" value="100" />
	<!-- 允许在嵌套语句中使用分页 -->
	<setting name="safeRowBoundsEnabled" value="false" />
	<!--是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 
		的类似映射。 -->
	<setting name="mapUnderscoreToCamelCase" value="false" />
	<!--MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 
		默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 
		的不同调用将不会共享数据。 -->
	<setting name="localCacheScope" value="SESSION" />
	<!-- 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 
		NULL、VARCHAR OTHER。 -->
	<setting name="jdbcTypeForNull" value="OTHER" />
	<!-- 指定哪个对象的方法触发一次延迟加载。 -->
	<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
</settings>

如果不使用配置文件,可以直接用Java代码来设置这个参数

sqlSessionFactory.getConfiguration().setLocalCacheScope(LocalCacheScope.STATEMENT);

总结

一级缓存默认会启动,想要关闭一级缓存可以通过flushCache标签或者localCacheScope修改缓存范围,一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中(HashMap当中,这里用HashMap会有多线程安全问题吗?)。如果同一个SqlSession中执行的方法和参数完全一致(分页情况下还要考虑分页参数),那么通过算法会生成相同的键值,当Map缓存对象当中已经存在该键值时,则会返回缓存中的对象,任何的INSERT、UPDATE、DELETE操作都会清空一级缓存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值