主题
分享记录一下MyBatis的一级缓存相关的学习.
Demo
public static void firstLevelCache() {
init("mybatis-config.xml"); SqlSession session = sqlSessionFactory.openSession(); UserMapper mapper = session.getMapper(UserMapper.class);
User u = mapper.selectByPrimaryKey(1);
System.out.println(u); User u2 = mapper.selectByPrimaryKey(1);
System.out.println(u2);
session.close(); System.out.println("==============session2=============="); SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User u3 = mapper2.selectByPrimaryKey(1);
System.out.println(u3);
session2.close(); }
从1个Demo来学习.
上面这段代码首先通过session获取了mapper然后执行了2次同样的查询,查询用户ID为1的数据, 然后打印=======session2======,然后新开了一个session2. 然后获取了第二个mapper并执行了相同的查询.
输出结果:
2018-09-29 19:06:26,813 DEBUG [main] logging.LogFactory : Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2018-09-29 19:06:26,961 DEBUG [main] pooled.PooledDataSource : PooledDataSource forcefully closed/removed all connections.
2018-09-29 19:06:26,962 DEBUG [main] pooled.PooledDataSource : PooledDataSource forcefully closed/removed all connections.
2018-09-29 19:06:26,962 DEBUG [main] pooled.PooledDataSource : PooledDataSource forcefully closed/removed all connections.
2018-09-29 19:06:26,962 DEBUG [main] pooled.PooledDataSource : PooledDataSource forcefully closed/removed all connections.
2018-09-29 19:06:27,102 DEBUG [main] jdbc.JdbcTransaction : Opening JDBC Connection
Sat Sep 29 19:06:27 CST 2018 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2018-09-29 19:06:28,095 DEBUG [main] pooled.PooledDataSource : Created connection 1682463303.
2018-09-29 19:06:28,095 DEBUG [main] jdbc.JdbcTransaction : Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@64485a47]
2018-09-29 19:06:28,108 DEBUG [main] UserMapper.selectByPrimaryKey : ==> Preparing: select id, user_id, user_name, real_name, email, creator_uid, modifier_uid, created_at, updated_at, del from user where id = ?
2018-09-29 19:06:28,145 DEBUG [main] UserMapper.selectByPrimaryKey : ==> Parameters: 1(Integer)
2018-09-29 19:06:28,172 DEBUG [main] UserMapper.selectByPrimaryKey : <== Total: 1
User{id=1, userId=1, userName='test', realName='realName', email='jyzjyz12@163.com', creatorUid=1, modifierUid=1, createdAt=Mon Sep 24 10:10:43 CST 2018, updatedAt=Mon Sep 24 10:10:46 CST 2018, del=false}
User{id=1, userId=1, userName='test', realName='realName', email='jyzjyz12@163.com', creatorUid=1, modifierUid=1, createdAt=Mon Sep 24 10:10:43 CST 2018, updatedAt=Mon Sep 24 10:10:46 CST 2018, del=false}
2018-09-29 19:06:28,172 DEBUG [main] jdbc.JdbcTransaction : Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@64485a47]
2018-09-29 19:06:28,183 DEBUG [main] jdbc.JdbcTransaction : Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@64485a47]
2018-09-29 19:06:28,183 DEBUG [main] pooled.PooledDataSource : Returned connection 1682463303 to pool.
==============session2==============
2018-09-29 19:06:28,183 DEBUG [main] jdbc.JdbcTransaction : Opening JDBC Connection
2018-09-29 19:06:28,183 DEBUG [main] pooled.PooledDataSource : Checked out connection 1682463303 from pool.
2018-09-29 19:06:28,183 DEBUG [main] jdbc.JdbcTransaction : Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@64485a47]
2018-09-29 19:06:28,193 DEBUG [main] UserMapper.selectByPrimaryKey : ==> Preparing: select id, user_id, user_name, real_name, email, creator_uid, modifier_uid, created_at, updated_at, del from user where id = ?
2018-09-29 19:06:28,194 DEBUG [main] UserMapper.selectByPrimaryKey : ==> Parameters: 1(Integer)
2018-09-29 19:06:28,210 DEBUG [main] UserMapper.selectByPrimaryKey : <== Total: 1
User{id=1, userId=1, userName='test', realName='realName', email='jyzjyz12@163.com', creatorUid=1, modifierUid=1, createdAt=Mon Sep 24 10:10:43 CST 2018, updatedAt=Mon Sep 24 10:10:46 CST 2018, del=false}
2018-09-29 19:06:28,210 DEBUG [main] jdbc.JdbcTransaction : Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@64485a47]
2018-09-29 19:06:28,221 DEBUG [main] jdbc.JdbcTransaction : Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@64485a47]
2018-09-29 19:06:28,221 DEBUG [main] pooled.PooledDataSource : Returned connection 1682463303 to pool.
从输出中我们可以看到,红色部分是执行的SQL,蓝色部分是查询到用户信息并打印的数据...
总共执行了3次输出打印,但是SQL查询命令只运行了2次.其中第1个session的第1次查询和第2个session的第1次查询的时候会触发SQL的执行.而第1个session的第二次查询没有处罚SQL执行
从中我们可以得到结论:
一级缓存是发生在同一个SqlSession中的, 1个sqlSession 多次执行相同的SQL查询会有缓存
如何缓存
然后我们来学习下MyBatis是如何设计二级缓存的.
总体来说我觉得最核心的就是BaseExecutor这个类
看到BaseExecutor的成员域中有个wrapper字段,类型是Executor类,大家就应该能明白,Executor是一种装饰者的设计模式.这种模式在mybatis里用的真的挺多的.
我们不管是自定义Mapper还是使用SqlSession去执行SQL,最终都是委托Executor来执行的.
如上图,我们的selectByPrimaryKey方法会调用SqlSession的selectOne然后再转到selectList,最后委托给executor来执行.
而默认的情况下,我们在从SqlSessionFactory里拿到的SqlSession的时候new的是一个SimpleExecutor外层包裹着CachingExecutor.
其中CachingExecutor涉及二级缓存,而BaseExecutor(SimpleExecutor的父类)主要涉及一级缓存.
从之前的BaseExecutor的结构图中我们也可以发现有1个叫做localCache的字段.这个字段类型是PerpetualCache
PerpetualCache可以认为就是hashmap的简单封装.
@SuppressWarnings("unchecked")
@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;
}
当执行BaseExecutor的query方法的时候,如上代码所示,会先在L14那行,试着从localCache中获取,如果没有就L18,queryFromDatabase.否则就会返回localCache.getObject(key)的结果.代码还是比较清楚明了的
那么这里就涉及到一个问题.怎么样才算缓存命中? 或者换句话说CacheKey怎么计算? 因为相同的cacheKey会取到相同的缓存结果.
-1233329154:3360697231:test.mapper.UserMapper.selectByPrimaryKey:0:2147483647:select id, user_id, user_name, real_name, email, creator_uid, modifier_uid, created_at,
updated_at, del from user
where id = ?:1:development
上面这段字符串就是我debug中截取的一个CacheKey的字符串形式. CacheKey的equals和toString依赖的成员域差不多.从toString的结果上来看可能更直观一点.
首先需要说明一下,CacheKey有一个doUpdate方法,这个方法允许你加入一些需要计算缓存key的成分.也就是说加入的对象会影响key的结果.
然后我们来分析一下那个字符串,其中有很多个组成部分
-1233329154为CacheKey本身的hashcode
3360697231为doUpdate里添加过的对象的hashcode和
后面的各个部分是doUpdate中的添加过的对象的各自的toString.那么可以添加哪些东西呢?
从字符串中其实也可以看出来.
类.方法名 + 分页初始位置(默认为0) + 分页终止位置(默认为Integer.MAX_VALUE) + SQL + 调用Mapper方法的参数 + 环境名(configuration里配置的,默认是development)
从代码里也可以看出就是这么个逻辑: 下图为BaseExecutor实现Executor接口生成cacheKey的方法
另外cacheKey的equals只是比toString多了一个count对象的校验,也就是doUpdate添加过了几个对象的校验...其他部分基本是一致的.
所以如果2次查询CacheKey的hashcode和equals一致,那么就会使用之前缓存的结果.
小结
MyBatis一级缓存主要是在BaseExecutor中实现的,CacheKey涉及到很多组成部分(hashcode+各部分hashcode+类.方法名+分页界限+SQL+参数+环境等), 从直观上的感受来说.调用同一个Mapper方法,如果参数一致,那就会被取到缓存结果.