目录
下一篇:mybatis二级缓存源码分析
1.一级缓存的使用
说起一级缓存的使用,其实简单至极。mybatis默认开启,我们甚至不需要任何的配置。闲言少叙来看一段代码:
public class Application {
public static void main(String[] args) throws IOException {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
String res = sqlSession.selectOne("com.mybatis.demo.CronMapper.selectOne");
String res2 = sqlSession.selectOne("com.mybatis.demo.CronMapper.selectOne");
//结果为true
System.out.println(res==res2);
}
}
我想这段代码简单的不能再简单了吧,这是mybatis不借助spring的最基本用法。我们通过会话sqlSession完成两次一模一样的查询,那么第二次查询就不会去查数据库了,而是直接从一级缓存中获取。注意此时我们先不考虑二级缓存的事情,下篇文章会介绍,不要着急!
2.走进源码的世界
接下来,我们跟着debug断点,一步步的走进mybatis的源码。不过捏,不用紧张,一级缓存简单的不敢想象,不信咱们走起!
1.get的过程
public class Application {
public static void main(String[] args) throws IOException {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
//进入这个方法
String res = sqlSession.selectOne("com.mybatis.demo.CronMapper.selectOne");
String res2 = sqlSession.selectOne("com.mybatis.demo.CronMapper.selectOne");
System.out.println(res==res2);
}
}
sqlSession的默认实现是DefaultSqlSession。于是我们进入到这个方法:
@Override
public <T> T selectOne(String statement) {
//进入这个方法
return this.selectOne(statement, null);
}
进到这个方法:
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
//进入这个方法
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
进到这个方法:
@Override
public <E> List<E> selectList(String statement, Object parameter) {
//进入这个方法
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
进到这个方法:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
//进入这个方法
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
上边的代码都很简单,一步步跟就是了。接下来稍微复杂点了。此时这个executor默认实现是CachingExecutor。于是我们进入到CachingExecutor的query方法:
@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);
//进入这个方法
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 {
Cache cache = ms.getCache();
//我们此时没有开启二级缓存,所以这个判断不会进
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
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);
}
这里需要注意下,这个delegate的默认实现是SimpleExecutor,但是SimpleExecutor中并没有重写query方法,而是直接使用其父类BaseExecutor的query方法。所以此时我们会进入到BaseExecutor的query方法。OK 继续!
@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++;
//这个三目运算会进入第一个分支
//localCache.getObject(key) 这行代码就是从一级缓存中获取数据
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;
}
那么这个localCache是个啥呢?其实他就是BaseExecutor的一个属性,类型是PerpetualCache。而在PerpetualCache里边维护了一个Map,我们来看看PerpetualCache的代码:
public class PerpetualCache implements Cache {
private final String id;
//真正的一级缓存 其实就是个Map
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);
}
}
行棋至此,我们已经看过了从一级缓存中查询数据的总体流程。我们来回顾总结一下:
首先调用sqlSession查询方法(默认实现是DefaultSqlSession)
---->调用executor的查询方法(executor的默认实现是CachingExecutor)
---->调用delegate的查询方法(delegate的默认实现是SimpleExecutor,但是没有重写query方法,于是直接使用其父类BaseExecutor的query方法)
---->BaseExecutor中维护了一个属性localCache,类型为PerpetualCache。
---->PerpetualCache里边维护了一个Map,是真正的一级缓存。存取都是操作这个map而已。
2.put的过程
看过了从一级缓存中获取,那么是什么时候将数据库查询结果放到缓存中的呢?我们再来看看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.");
}
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;
}
进到这个方法:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//查询数据库
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//将结果放到一级缓存
//进入这个方法
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
进到这个方法:
@Override
public void putObject(Object key, Object value) {
//这个cache就是那个Map
cache.put(key, value);
}
存放的过程更简单是吧,其实就是直接放到了PerpetualCache的Map里边而已。
3.key的生成策略
可能有好多小伙伴会问,我们知道了数据存在哪儿,什么时候存什么时候取。那缓存的key是怎么生成的呢?和哪些因素有关呢?或者说影响缓存命中率的因素有哪些?接下来我们看看:
978273079:-578870450:com.mybatis.demo.CronMapper.selectOne:0:2147483647:select cron from public.cron where id = ?:1:development
这个就是一个key,我们可以拆开来看看每个部分分别代表什么?
(1) 978273079:hashCode 我们可以不用管,不会影响缓存的命中与否。
(2) -578870450:这货是checkSum,具体什么作用我也不知道,但是不会影响命中与否可以不管。
(3) com.mybatis.demo.CronMapper.selectOne:这个是MappedStatementId,也就是我们mapper中的方法的全限定名即包名.类名.方法名。
(4) 0:分页的偏移量offset
(5) 2147483647:每页大小,因为我没有设置分页参数,所以是默认值Integer.MaxValue
(6) select cron from public.cron where id = ?:1:这个很明显就是我们的sql语句,包括参数。
(7) development:这个是我们配置的环境id,一般配置在xml里边,如下:
<environments default="development">
//就是这个货
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="org.postgresql.Driver"/>
<property name="url" value="jdbc:postgresql://localhost:5432/postgres"/>
<property name="username" value="postgres"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
4.总结
4.1 执行流程:首先查询缓存,存在直接返回。不存在查询数据库然后放到缓存供下次查询使用。
4.2 影响一级缓存命中的因素:
1.MappedStatementId
2.sql+参数
3.分页条件
4.环境(通常这个不会有变化)
5.这个也是最重要的,必须是同一个会话即同一个sqlSession。通过代码我们可以看到,一个会话对应一个CachingExecutor,一个CachingExecutor对应一个BaseExecutor,一个BaseExecutor对应一个PerpetualCache。简单说就是一个sqlSession对应一个缓存。如果会话不同那么缓存也必定不同。我们常说一级缓存不能跨线程正是这个原因。一般情况下一个线程就会创建一个会话,那么一级缓存也一定是不同的。
好了一级缓存完结,下一篇二级缓存!