文章目录
- 什么是一级缓存?
- 什么是二级缓存?
- 一级缓存二级缓存有什么不同?
- 执行流程
- 源码流程解剖
- 一级缓存失效场景分析
- 二级缓存结构及需要解决的问题
- 二级缓存执行流程
- 二级缓存获取和commit源码解剖
- 总结
什么是一级缓存?
一级缓存是指在同一个SqlSession中,对于相同的查询语句和参数,第一次查询的结果会被缓存到内存中,后续的查询会直接从缓存中获取结果,而不会再次查询数据库。一级缓存是MyBatis默认开启的,可以通过在SqlSession中调用clearCache()方法来清空缓存。
什么是二级缓存?
二级缓存是指在多个SqlSession中,对于相同的查询语句和参数,第一次查询的结果会被缓存到内存中,后续的查询会直接从缓存中获取结果,而不会再次查询数据库。二级缓存是需要手动开启的,可以通过在Mapper.xml文件中添加标签来开启。二级缓存的作用范围是Mapper级别的,也就是说,同一个Mapper.xml文件中的查询语句会共享同一个缓存。
一级缓存二级缓存有什么不同?
一级缓存和二级缓存的不同点在于作用范围和生命周期。一级缓存的作用范围是SqlSession级别的,生命周期也是和SqlSession一样的,当SqlSession关闭时,缓存也会被清空。而二级缓存的作用范围是Mapper级别的,生命周期是和应用程序一样的,当应用程序关闭时,缓存也会被清空。另外,二级缓存需要手动开启,而一级缓存是默认开启的。
执行流程
public void testMybatis()throws Exception{
SqlSessionFactoryBuilder sqlSessionFactoryBuilder=new SqlSessionFactoryBuilder();
org.springframework.core.io.ClassPathResource classPathResource=new ClassPathResource("org/apache/ibatis/user/mybatis.xml");
InputStream inputStream = classPathResource.getInputStream();
// 1.读取配置文件获取SqlSessionFactory
SqlSessionFactory sqlSessionFactory= sqlSessionFactoryBuilder.build(inputStream);
// 2.获取sqlsession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.执行获取结果
User user = new User(1L, null, null, null);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> users = mapper.selectUser(user);
mapper.selectUser(user);
System.out.println(users);
}
CachingExecutor 二级缓存执行器
CachingExecutor类直接实现了Excutor接口,是装饰器类,主要用来增强缓存相关功能。在CachingExecutor类中,为了完成缓存相关功能,需要TransactionalCacheManager和TransactionalCache两个类的支持,其中TransactionalCacheManager类主要用来管理缓存数据的对象TransactionalCache,而TransactionalCache对象是用来真正缓存数据的对象。
BaseExecutor 一级缓存执行器
BaseExecutor是MyBatis中所有Executor的基类,它实现了Executor接口中的一些通用方法,例如query()、update()、flushStatements()等。BaseExecutor中定义了一个MappedStatement和CacheKey的Map,用于缓存已经解析过的MappedStatement和CacheKey对象,以提高查询效率。BaseExecutor还定义了一个Transaction对象,用于管理事务的提交和回滚。
SimpleExecutor(默认是SimpleExecutor)每次执行SQL语句都需要进行编译
SimpleExecutor是最简单的Executor实现,每次执行SQL语句时都会创建一个新的Statement对象,执行完毕后立即关闭Statement对象。SimpleExecutor适用于短时间内需要执行大量SQL语句的场景,但是由于每次都需要创建和关闭Statement对象,所以效率相对较低。
ReuseExecutor 执行相同SQL语句时,如果之前SQL已编译,则不会在进行编译。
ReuseExecutor是在SimpleExecutor的基础上进行了优化,它会在执行SQL语句之前先检查是否已经存在可重用的Statement对象,如果存在则直接使用,否则创建一个新的Statement对象。ReuseExecutor适用于需要执行多个相同的SQL语句的场景,可以减少创建和关闭Statement对象的次数,提高效率。
BatchExecutor 批量执行
BatchExecutor是用于批量执行SQL语句的Executor实现,它会将多个SQL语句合并成一个批量操作,然后一次性发送给数据库执行。BatchExecutor适用于需要执行大量相似的SQL语句的场景,可以减少网络传输和数据库操作的次数,提高效率。
在MyBatis中,可以通过在配置文件中设置defaultExecutorType属性来指定默认的Executor实现,也可以在Mapper.xml文件中通过executorType属性来指定特定的Executor实现。
源码流程解剖
一:获取UserMapper
我们先来看看 UserMapper mapper = sqlSession.getMapper(UserMapper.class);这一行代码返回的是一个什么对象。
1.首先我们可以看到,mybatis通过动态代理模式给我们生成了一个Usermapper对象。
2.在这个mapper对象当中,有一个sqlSession对象,SqlSession里面有一个executor,这个executor是CachingExecutor(这个是二级缓存的CachingExecutor)
3.在CachingExecutor中有一个delegate它是一个SimpleExecutor。(这里就要注意了,因为BaseExecutor是SimpleExecutor的父类,只不过mybatis默认是SimpleExecutor。所以这里delegate其实是指向了一级缓存BaseExecutor)
二:执行UserMapper.selectUser(user)的查询方法
由于我们的UserMapper是动态代理生成的代理对象,那么在执行方法前会先去执行invoke方法
List<User> users = mapper.selectUser(user);
MapperProxy代理对象
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 这个就是调用mapperMethod.execute方法,看当前查询是select、update、delete等等
// 在根据具体的实现去调用DefaultSqlSession里面的(selectList()、insert()等等)
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
DefaultSqlSession,因为我们查询的是一个数组,所以就调用了selectList()方法
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
// 1. 获取一个statement
MappedStatement ms = configuration.getMappedStatement(statement);
dirty |= ms.isDirtySelect();
// 2.执行二级缓存CachingExecutor的query方法
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
CachingExecutor 二级缓存的query方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
throws SQLException {
// 这就是解析你的SQL,把SQL解析为 select * from user wehre id=?
// 并且包含了SQL语句和参数列表等信息
BoundSql boundSql = ms.getBoundSql(parameterObject);
// CacheKey对象中包含了MappedStatement的ID、查询参数、分页参数等信息,这个比较重要。后面会详细讲解
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
CachingExecutor 二级开始干事情的query方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
// 1.获取当前mappernamespace是否配置了<cache/>标签,是否开启了二级缓存 ,从二级缓存空气获取该cache
Cache cache = ms.getCache();
if (cache != null) {
// 判断是否要清除缓存,只会清空当前线程的缓存,不会影响其他线程的缓存。
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 检查SQL语句中是否包含输出参数
ensureNoOutParams(ms, boundSql);
// 从二级缓存中获取结果
List<E> list = (List<E>) tcm.getObject(cache, key);
// 从一级缓存中获取结果并将结果存储到二级缓存map当中
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
// 从二级缓存中拿出结果,进行返回
return list;
}
}
// delegate是我们的一级缓存,如果没有开启二级缓存,就直接调用一级缓存获取结果进行返回
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
BaseExecutor的query方法
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) {
// 处理本地缓存中的输出参数,不会对SQL语句进行解析或执行
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 一级缓存没有就去数据库查询结构,后面会调用SimpleExecutor的doQuery方法。
// 而且会把查询数据存入一级缓存当中
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;
}
SimpleExecutor的doQuery
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,
boundSql);
// 获取连接 ,会调用PooledDataSource.getConnection()
stmt = prepareStatement(handler, ms.getStatementLog());
// 查询数据库
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
StatementHandler.query最后会调用PreparedStatementHandler的query方法
// 看到这里大家就非常熟悉了,和jdbc一样PreparedStatement 执行SQL语句进行返回。
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
一级缓存失效场景分析
在上面我们从一级缓存获取数据时,都是通过key来获取的。我们来看看这个key到底是啥。
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) {
// 处理本地缓存中的输出参数,不会对SQL语句进行解析或执行
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
}
从一级缓存获取数据
list = resultHandler == null ? (List) localCache.getObject(key) : null;
这个CacheKey key,前面我也标注了是在二级缓存调用query方法时获取的。那我们来看看这个key里面有啥玩意
在updateList中有6参数我们来依次看看啥意思。
参数 0:指向的就是我们statementID
参数1:分页数值
参数2:分页数值
参数3:编译后的SQL语句
参数4:设置的参数值
参数5:就是你配置数据源时的default值
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value=""/>
<property name="username" value=""/>
<property name="password" value=""/>
</dataSource>
</environment>
</environments>
那么我们就可以总结一级缓存有哪些失效场景了。(修改,新增操作就忽略)
- 必须在同一个statementID的语句中执行
- SQL语句和参数值必须一致。
- 分页条件必须一致
- 必须在同一个会话中,在上面代码中我们也可以看到。我们调用mapper.selectUser(user);是会执行sqlSession的excutor方法。如果sqlSession关闭了,那么里面的一级执行器,当然也就不会在存在了。
这里可能有疑问,sqlSession什么时候销毁呢?
戳这里,在spring中sqlSession的创建及销毁
二级缓存结构及需要解决的问题
首先我们需要考虑的是,既然二级缓存是可以在不同的sqlSession进行数据共享,那么会有哪些问题需要我们去解决。
问题一:线程安全问题,既然二级缓存可以跨线程使用。那么我们是不是要考虑线程安全、脏读等问题。
问题二:既然是缓存,不是存储在内存中,就是存储在磁盘或者等三方中间件redis中。所以它们肯定是有一定的空间大小的,那就需要一些缓存淘汰策略。
问题三:在mybatis中,二级缓存是存在一个命中率记录器的。那么每次查询命中二级缓存,也是需要进行记录的。那么在并发情况下,怎么保证二级缓存命中率的准确性
等等,当然还有一些其他需要考虑问题,我就不一 一列举,
如果让我们设计一个获取缓存的方法,需要解决以上问题的话。那还不简单,直接撸一个方法,把这些解决方法逻辑加上去,就完事。但是在mybatis中,并没有采用这种方式,而是采用了责任链和装饰器设计模式。
在mybatis当中,首先定义了一个Cache接口,Cache接口定义了一些获取缓存,存放缓存等等一些方法。
通过责任链设计模式,让每一个处理逻辑的Cache去实现Cache接口,在每一个方法中做自己做的事。这样做可以达到我们的解耦和单一职责。
通过装饰器设计模式,当我们处理完线程同步的逻辑后,就会执行下一个cache对应的方法,最后一直执行到我们的内存存储的cache。这样做可以达到解耦,对原有方法进行增强,减少我们的继承类。
总之mybatis这样做的好处,就在于对代码进行可维护可拓展。如果后续有需求需要新加一个cache的逻辑,我们就可以不改动原代码,只需去编写新逻辑的cahche在把它加进去即可。
下面我们来验证下,是不是这样。
Cache cache = sqlSession.getConfiguration().getCache("org.apache.ibatis.mapper.UserMapper");
cache.putObject("123",users);
大家可以发现第一个cache是SynchronizedCache,第二个cache是LoggingCache,第三个cache是LRUCache,第四个cache是PerptualCache。其实就和我们上面的流程图一致
二级缓存执行流程
在之前流程图中,我做了一些改动。多了一个事务缓存管理器和一个缓存空间。
事务缓存管理器是存储我们当前sqlSession中二级缓存所存储的数据。当SqlSession关闭时,那么这个事务缓存管理器也会失效。为什么要有这个事务缓存管理器呢,因为我们的二级缓存是需要在提交后,在进行存储的。如果进行实时提交,事务后面出现回滚等操作。就会出现脏读等问题。
二级缓存获取和commit源码解剖
Debug解析
在没有执行SQL语句之前,TransactionalCacheManager中的transactionalCaches是空,没有任何数据。这是因为我们还没有执行SQL语句,mybatis也不知道需要将那些数据加入到我们的TransactionalCacheManager当中,你查询了哪个缓存mapper,就会将哪个缓存的mapper加入到我们的TransactionalCacheManager当中。
在我们执行一次查询后,看看有什么变化
当我们commit之后,会有什么变化
命中二级缓存源码解剖
CachingExecutor 的query方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
// 1.获取当前mappernamespace是否配置了<cache/>标签,是否开启了二级缓存 ,从二级缓存空间中获取对应的cache
Cache cache = ms.getCache();
if (cache != null) {
// 判断是否要清除缓存,只会清空当前线程的缓存,不会影响其他线程的缓存。
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 检查SQL语句中是否包含输出参数
ensureNoOutParams(ms, boundSql);
// 从二级缓存中获取结果
List<E> list = (List<E>) tcm.getObject(cache, key);
// 从一级缓存中获取结果并将结果存储到二级缓存map当中
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
// 从二级缓存中拿出结果,进行返回
return list;
}
}
// delegate是我们的一级缓存,如果没有开启二级缓存,就直接调用一级缓存获取结果进行返回
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
我们进入tcm也就是TransactionalCacheManager的getobject方法
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
// 获取一个TransactionalCache对象,并将其存储在缓存中,以便在事务中使用。如果缓存中已经存在这个对象,则直接返回;否则,创建一个新的对象并存储在缓存中。
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
public Object getObject(Object key) {
// 从二级缓存空间获取数据
Object object = delegate.getObject(key);
// 如果没有获取到
if (object == null) {
// 将当前数据加到事务缓存管理器中,进行暂时存储
entriesMissedInCache.add(key);
}
// 判断该值是否为true。如果是true就返回null。
// 因为在我们事务没提交时,如果进行修改的时候不会直接清除二级缓存空间,而是先标记一下做个假删除。等commit之后才会去清除我们的二级缓存空间。
// 同样标记为null,也是为了防止出现脏读等。
if (clearOnCommit) {
return null;
}
return object;
}
commit源码解剖
DefaultSqlSession的commit
public void commit(boolean force) {
try {
// 调用二级缓存的commit
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
CachingExecutor的commit
TransactionalCacheManager的commit
public void commit() {
// 循环进行提交
for (TransactionalCache txCache : transactionalCaches.values()) {
// 调用TransactionalCache的commit
txCache.commit();
}
}
TransactionalCache的commit
public void commit() {
// 如果标记假删除,这里提交时候就会清空
if (clearOnCommit) {
delegate.clear();
}
// 进行刷新
flushPendingEntries();
reset();
}
// 看到这里是不是就是很熟悉了,将我们事务缓存管理器的map丢到我们的二级缓存空间当中。
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
至于二级缓存失效,第一个二级缓存只有提交后才会生效,第二个发生增删改就会清空所有的二级缓存。
总结
如上就是对myabtis的一级缓存二级缓存进行分析和源码解剖,如下就做个总结。
1.mybatis一级缓存和二级缓存的区别
1.作用范围不同:一级缓存是SqlSession级别的缓存,只在当前SqlSession中有效;而二级缓存是Mapper级别的缓存,多个SqlSession可以共享同一个Mapper的二级缓存。
2.存储位置不同:一级缓存是存储在SqlSession内部的一个HashMap中,而二级缓存是存储在外部的缓存中,例如Ehcache、Redis等。
3.失效机制不同:一级缓存的失效机制是基于SqlSession的生命周期,当SqlSession关闭时,一级缓存也会被清空;而二级缓存的失效机制是基于时间的,可以通过配置缓存的过期时间来控制缓存的失效。
4.数据一致性不同:一级缓存可以保证数据的一致性,因为它只在当前SqlSession中有效;而二级缓存可能会出现数据不一致的问题,因为多个SqlSession可以共享同一个Mapper的二级缓存。
5.配置方式不同:一级缓存是默认开启的,无需进行额外的配置;而二级缓存需要进行额外的配置,包括缓存的类型、大小、过期时间等。
总之,MyBatis的一级缓存和二级缓存都是用于提高查询性能的缓存机制,但是它们的作用范围、存储位置、失效机制、数据一致性和配置方式都有所不同。在使用MyBatis时,需要根据具体的业务需求和系统性能要求,选择合适的缓存机制。
2.一级缓存失效原因有哪些
- 必须在同一个statementID的语句中执行
- SQL语句和参数值必须一致。
- 分页条件必须一致
- 必须在同一个会话中,在上面代码中我们也可以看到。我们调用mapper.selectUser(user);是会执行sqlSession的excutor方法。如果sqlSession关闭了,那么里面的一级执行器,当然也就不会在存在了。
3.为什么不建议使用mybatis的二级缓存?
虽然MyBatis的二级缓存可以提高查询性能,但是在实际应用中,使用二级缓存并不总是一个好的选择。以下是一些原因:
1.数据不一致性问题:二级缓存是一个全局的缓存,多个SqlSession共享同一个缓存。如果在一个SqlSession中修改了数据,但是这个修改没有及时更新缓存,那么在另一个SqlSession中查询这个数据时,就会出现数据不一致的问题。
2.内存占用问题:二级缓存是一个全局的缓存,会占用大量的内存。如果缓存中存储了大量的数据,那么就会导致内存占用过高,从而影响系统的性能。
3.缓存失效问题:二级缓存的失效机制是基于时间的,如果缓存中的数据长时间没有被访问,那么就会被自动清除。但是这种失效机制并不总是可靠的,有时候缓存中的数据可能已经过期,但是仍然被使用,从而导致数据不一致的问题。
4.配置复杂性问题:使用二级缓存需要进行复杂的配置,包括缓存的类型、大小、过期时间等。如果配置不当,就会导致缓存的效果不佳,甚至影响系统的性能。
5.综上所述,虽然MyBatis的二级缓存可以提高查询性能,但是在实际应用中,使用二级缓存需要考虑到数据一致性、内存占用、缓存失效和配置复杂性等问题。因此,建议在使用MyBatis时,根据具体的业务需求和系统性能要求,谨慎选择是否使用二级缓存。