mybatis 一级、二级缓存源码详解
文章中若附原文链接,那部分内容对你有所帮助,请给原作者先点赞!
文章中若附原文链接,那部分内容对你有所帮助,请给原作者先点赞!
文章中若附原文链接,那部分内容对你有所帮助,请给原作者先点赞!
缓存
- 什么是缓存?
缓存就是存在于内存中的临时数据。 - 为什么要使用缓存?
为了减少和数据库交互的次数,提高执行效率。 - 适用于缓存的数据
经常查询并且不经常改变的数据。
数据的正确与否对最终结果影响不大的。 - 不适用于缓存的数据
经常改变的数据。
数据的正确与否对最终结果影响很大的。例如:商品的库存、银行的汇率、股市的牌价等。
一级缓存
一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush 或 close,它就会存在。当调用 SqlSession 的修改、添加、删除、commit()、close()、clearCache() 等方法时,就会清空一级缓存。
一级缓存流程如下图:
- 第一次发起查询用户 id 为 1 的用户信息,Mybatis 会先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息。
得到用户信息,将用户信息存储到一级缓存中。 - 如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),那么 Mybatis 就会清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
- 第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。
Mybatis 默认就是使用一次缓存的,不需要配置。
一级缓存中存放的是对象。(一级缓存其实就是 Map 结构,直接存放对象)mybatis一级缓存,缓存在SqlSession中。
一级缓存是默认的,不需要配置,localCache中没有缓存时,才去执行queryFromDatabase方法,去查询数据库,并将结果缓存到localCache中。
MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement
源码
public abstract class BaseExecutor implements Executor {
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 {
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}
List list;
try {
++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 {
--this.queryStack;
}
if (this.queryStack == 0) {
Iterator var8 = this.deferredLoads.iterator();
while(var8.hasNext()) {
BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
deferredLoad.load();
}
this.deferredLoads.clear();
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
}
return list;
}
}
二级缓存
- 二级缓存是 Mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 SQL 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。
二级缓存流程如下图:
- 当 sqlSession1 去查询用户信息的时候,Mybatis 会将查询数据存储到二级缓存中。
- 如果 sqlSession3 去执行相同 Mapper 映射下的 SQL 语句,并且执行 commit 提交,那么 Mybatis 将会清空该 Mapper 映射下的二级缓存区域的数据。
- sqlSession2 去查询与 sqlSession1 相同的用户信息,Mybatis 首先会去缓存中找是否存在数据,如果存在直接从缓存中取出数据。
- 如果想使用 Mybatis 的二级缓存,那么应该做以下配置
- 首先在 Mybatis 配置文件中添加配置 (这一步其实可以忽略,因为默认值为 true)
<settings>
<!-- 开启缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
- 接着在映射文件中配置
<mapper namespace="cn.ykf.mapper.UserMapper">
<!-- 使用缓存 -->
<cache/>
</mapper>
- 最后在需要使用二级缓存的操作上配置 (针对每次查询都需要最新数据的操作,要设置成
useCache="false"
,禁用二级缓存)
<select id="listAllUsers" resultMap="UserWithAccountsMap" useCache="true">
SELECT * FROM user
</select>
- 当我们使用二级缓存的时候,所缓存的类一定要实现
java.io.Serializable
接口,这样才可以使用序列化的方式来保存对象。- 由于是序列化保存对象,所以二级缓存中存放的是数据,而不是整个对象。
源码
- 首先,excutor是在sqlsessionfactory创建sqlsession时创建的,用以作为sqlsession的参数传入
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
- 接着看下excutor有什么类型
public class Configuration {
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object 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 (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);//装饰前面的三个executor
}
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
}
- 可以看出如果开启二级缓存,cachingExecutor会被作为装饰类,为什么说是装饰类呢
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
public Transaction getTransaction() {
return this.delegate.getTransaction();
}
public void close(boolean forceRollback) {
try {
if (forceRollback) {
this.tcm.rollback();
} else {
this.tcm.commit();
}
} finally {
this.delegate.close(forceRollback);
}
}
public boolean isClosed() {
return this.delegate.isClosed();
}
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
this.flushCacheIfRequired(ms);//刷新二级缓存
return this.delegate.update(ms, parameterObject);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
return this.query(ms, parameterObject, rowBounds, resultHandler, key, 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) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, parameterObject, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key);二级缓存中存在则在二级缓存中读取
if (list == null) {
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);//保存到二级缓存中
}
return list;//返回
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public List<BatchResult> flushStatements() throws SQLException {
return this.delegate.flushStatements();
}
public void commit(boolean required) throws SQLException {
this.delegate.commit(required);
this.tcm.commit();
}
public void rollback(boolean required) throws SQLException {
try {
this.delegate.rollback(required);
} finally {
if (required) {
this.tcm.rollback();
}
}
}
}
-
可以从commit等事务可以看出来是由传入的excutor(delegate)实现的,并且与TransactionalCacheManager tcm辅助完成功能,只有query自己实现了功能,即二级缓存,从中可以看出他是先读取二级缓存有无内容,没有再到一级缓存中查看,没有再去数据库查询,再保存到一级缓存(若commit或),最后返还给用户。
-
因为一个二级缓存对应多个一级缓存,所以在其中一个sqlsession需要刷新导致二级缓存刷新时,其他sqlsession的与该sqlsession无关联的数据可以在一级缓存中获取就行,但是如果是相关联的数据,此时又用其他的sqlsession来查找,其他sqlsession的一级缓存数据是否存在过期现象。(不同命名空间?)这就涉及到一级缓存什么时候提交数据到二级缓存中的问题了:
-
只有会话关闭或提交后,一级缓存中的数据才会转移到二级缓存中,并且只有是同一个namespace所以可以获取到数据;因为mybatis的缓存会被一个transactioncache类包装住,所有的cache.putObject全部都会被暂时存到一个map里,等会话commit/close以后,这个map里的缓存对象才会被真正的cache类执行putObject操作。这么设计的原因是为了防止事务执行过程中出异常导致回滚,如果get到object后直接put进缓存,万一发生回滚,就很容易导致mybatis缓存被脏读
-
这就保证了select就不会到达同的其他的sqlsession的cache中了,若是update成功,则commit成功,数据上传到二级缓存中,对该sqlsession的数据访问会直接在二级内存读取,若是失败,则不会commit,则不会上传到二级缓存中,数据因为失败仍应是原来的数据才是对的,二级缓存数据没有收到commit或close后传来的数据而不变,继续等待相应的sqlsession 进行commit/close
-
UserMapper有一个二级缓存区域(按namespace分,如果namespace相同则使用同一个相同的二级缓存区),其它mapper也有自己的二级缓存区域(按namespace分)
-
每一个namespace的mapper都有一个二缓存区域,两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同的二级缓存区域中。
-
mybatis二级缓存的局限性:
细粒度缓存就是,针对某个商品,如果要修改其信息,只修改该商品的缓存数据,缓存区的其他数据不动(不会因为某个商品信息的修改就直接清空整个缓存区)
mybatis二级缓存对细粒度级别的缓存实现不好,比如如下需求:
对商品信息进行缓存, 由于商品信息查询访问量大,但是要求用户每次都能查询到最新的商品信息,此时如果使用mybatis的二级缓存,就无法实现当一个商品信息变化时,只刷新该商品的缓存信息,而不刷新其它商品的信息。这是因为mybatis的二级缓存区域以mapper为单位进行划分,当一个商品信息变化时(使用这个mapper的任意一个sqlSession执行commt操作),会将所有商品信息的缓存数据全部清空。解决此问题,需要在业务层根据需求对数据进行针对性的缓存(三级缓存)。