Mybatis深入源码分析之SQLSession一级缓存原理分析

Stella981
• 阅读 702

Mybatis深入源码分析之SQLSession一级缓存原理分析

通过前面几篇文章,Mybatis深入源码分析之SqlSessionFactoryBuilder源码分析Mybatis深入源码分析之Mapper与接口绑定原理源码分析。我们对Mybatis源码也有了一定的了解。本篇文章,我们继续分析:SQLSession一级缓存原理。

一:invoke()方法源码分析

首先,当我们调用getMapper的时候,就会进入invoke()方法:

// 5.操作Mapper接口 UserMapper mapper = sqlSession.getMapper(UserMapper.class);

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable var5) { throw ExceptionUtil.unwrapThrowable(var5); } } else { MapperMethod mapperMethod = this.cachedMapperMethod(method);    //将我们的代理方法缓存起来 return mapperMethod.execute(this.sqlSession, args); } }

private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()); this.methodCache.put(method, mapperMethod); } return mapperMethod; }

缓存的目的:知道SQL语句对应的mapper接口中的方法

下面看看execute()方法

public Object execute(SqlSession sqlSession, Object[] args) {         .... } else { param = this.method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(this.command.getName(), param); //最终调用selectOne()方法 }        ..... }

到了本篇文章的重点了,下面我们就开始分析selectOne()方法里面怎么实现的。

通过源码分析我们可以知道mapper.getUser()就是调用selectOne()方法。所以下面的两行代码是等效的,效果一样

UserEntity user = mapper.getUser(2); UserEntity o = sqlSession.selectOne("com.mayikt.mapper.UserMapper.getUser", 2);

Mybatis深入源码分析之SQLSession一级缓存原理分析

进入selectOne()可知,SqlSession这个接口帮我们封装了CRUD的方法,便于我们操作。

最终执行DefaultSqlSession,因为前面new了DefaultSqlSessionFactory()

public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }

下面会执行这段代码

Mybatis深入源码分析之SQLSession一级缓存原理分析

public SqlSession openSession() { return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);//进入这里 }

Mybatis深入源码分析之SQLSession一级缓存原理分析

所以,通过上述分析,我们知道这里就是执行DefaultSqlSession

Mybatis深入源码分析之SQLSession一级缓存原理分析

public T selectOne(String statement, Object parameter) { List 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; } }

底层还是查询所有的,但是还是取第一个,查询多个的化就会抛出异常。

public List selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); }

public List selectList(String statement, Object parameter, RowBounds rowBounds) { List var5; try { MappedStatement ms = this.configuration.getMappedStatement(statement);//这里得到sql语句 var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception var9) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9); } finally { ErrorContext.instance().reset(); } return var5; }

Mybatis深入源码分析之SQLSession一级缓存原理分析

通过上面的RowBounds,我们可知这个是分页用到的类。得出结论:Mybatis默认最多能查到Integer的最大值

下面进入这个方法

MappedStatement ms = this.configuration.getMappedStatement(statement);

MappedStatement存放的 SQL语句的配置内容,到query()方法的时候,有两个实现类的方法,我们应该走哪一个方法,下面我们开始debug源码分析这块

Mybatis深入源码分析之SQLSession一级缓存原理分析

下面是debug到getMappedStatement()这块的MappedStatement

Mybatis深入源码分析之SQLSession一级缓存原理分析

由此我们可知:MappedStatement是存放我们SQL的配置内容

Mybatis深入源码分析之SQLSession一级缓存原理分析

我们可以知道了,executor为CachingExecutor,我们再来看看Executor接口下面有哪些Executor执行器

Mybatis深入源码分析之SQLSession一级缓存原理分析

这里先透露下:CachingExecutor为二级缓存执行器,BaseExecutor为一级缓存执行器。

二:OpenSession()方法源码分析

下面我们来分析下原因:先回到我们的openSession()方法

// 4.获取Session SqlSession sqlSession = sqlSessionFactory.openSession();

public SqlSession openSession() { return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false); }

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; }

Executor executor = this.configuration.newExecutor(tx, execType);

上述代码,在我们创建sqlSession的时候帮我们创建好了执行器,进入这个方法:

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)this.interceptorChain.pluginAll(executor); return executor; }

Mybatis深入源码分析之SQLSession一级缓存原理分析

Mybatis深入源码分析之SQLSession一级缓存原理分析

Mybatis深入源码分析之SQLSession一级缓存原理分析

由此我们知道开启了缓存执行器,executor传递的是简单执行器,我们就明白了,先有简单执行器(SimpleExecutor),判断是否开启了二级缓存,开启了就创建缓存执行器(CacheExecutor)

最后返回executor

Mybatis深入源码分析之SQLSession一级缓存原理分析

下面我们总结下这几个执行器的作用:

  • SimpleExecutor: 默认的 Executor,每个 SQL 执行时都会创建新的 Statement
  • CachingExecutor: 可缓存数据的 Executor,用代理模式包装了其它类型的 Executor
  • ReuseExecutor: 相同的 SQL 会复用 Statement
  • BatchExecutor: 用于批处理的 Executor

public List 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); }

进入这行代码:

CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { return this.delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql); }

Mybatis深入源码分析之SQLSession一级缓存原理分析

由此我们知道,这里使用了多态的思想,没有SimpleExecutor执行器,说明走的是SimpleExecutor的父类BaseExecutor执行器的方法

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (this.closed) { throw new ExecutorException("Executor was closed."); } else { CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); List parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();

    for(int i = 0; i < parameterMappings.size(); ++i) {
        ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
            String propertyName = parameterMapping.getProperty();
            Object value;
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
                value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
            } else {
                MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            cacheKey.update(value);
        }
    }
    if (this.configuration.getEnvironment() != null) {
        cacheKey.update(this.configuration.getEnvironment().getId());
    }
    return cacheKey;    //返回了cacheKey为:-978696406:1452564227:com.mayikt.mapper.UserMapper.getUser:0:2147483647:select \* from user where id=?:2:development
}

}

Mybatis深入源码分析之SQLSession一级缓存原理分析

通过上面得到了缓存key,得到key后,再调用query方法去查询:

public List 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 List 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 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); }

通过上述代码:我们可知,如果二级缓存没有,走简单执行器

Mybatis深入源码分析之SQLSession一级缓存原理分析

二级缓存没用,则进入else分支:

return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

Mybatis深入源码分析之SQLSession一级缓存原理分析

思考:为什么CachingExecutor要找SimpleExecutor创建缓存key?

答案是为了复用,实现缓存key代码复用。mybatis缓存控制:先查找二级缓存(需要自己配置),二级缓存没有的情况下,再去找一级缓存(默认都有)

一级缓存是绝对有的,二级缓存(硬盘、Redis、EHCache)是可以没有的(表示没用使用,配置存储介质,就不会缓存,相当于空壳的)。

public List 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 i$ = this.deferredLoads.iterator(); while(i$.hasNext()) { BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next(); deferredLoad.load(); } this.deferredLoads.clear(); if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { this.clearLocalCache(); } } return list; } }

执行从HashMap中查找缓存

list = resultHandler == null ? (List)this.localCache.getObject(key) : null;

public Object getObject(Object key) { return this.cache.get(key); }

Mybatis深入源码分析之SQLSession一级缓存原理分析

Mybatis深入源码分析之SQLSession一级缓存原理分析

所以,我们知道了PerpetualCache指的是我们的一级缓存,一级缓存指的是本地缓存,存放在内存中的。使用Map集合存放的。

我们知道,我们一级缓存现在也没有,所以会先往数据库中查询一次

Mybatis深入源码分析之SQLSession一级缓存原理分析

private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);    //这里缓存中先放个占位符,表示要去查询数据库了 List list; try { list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);    //这里去数据库查询结果 } finally { this.localCache.removeObject(key);    //先删除占位key }

this.localCache.putObject(key, list);    //再存到缓存中
if (ms.getStatementType() == StatementType.CALLABLE) {
    this.localOutputParameterCache.putObject(key, parameter);
}
return list;

}

最后回到前面:

Mybatis深入源码分析之SQLSession一级缓存原理分析

protected int queryStack = 0;

这里queryStack为全局的,存在线程安全问题。

三:Mybatis缓存源码分析

下面我们开启日志,来验证下本地一级缓存作用:在Mybatis配置文件加入下面配置,开启打印日志:

Mybatis深入源码分析之SQLSession一级缓存原理分析

结果:说明只发出一条SQL语句去数据库查询一次,第一次去查询数据库,将查询结果集存放在缓存中,第二次查询就直接走本地缓存查询。

第一次调用....
Opening JDBC Connection
Created connection 1076835071.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@402f32ff]
==>  Preparing: select * from user where id=? 
==> Parameters: 1(Integer)
<==    Columns: id, name, update_time
<==        Row: 1, xuyu, 2019-03-13 14:27:49.0
<==      Total: 1
xuyu
第二次调用....
xuyu

加入在中间加入一条update语句,结果是怎样?

//中间执行一条update语句 sqlSession.update("com.mayikt.mapper.UserMapper.updateUser",1);

结果:发出了三条SQL语句。

第一次调用....
Opening JDBC Connection
Created connection 1076835071.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@402f32ff]
==>  Preparing: select * from user where id=? 
==> Parameters: 1(Integer)
<==    Columns: id, name, update_time
<==        Row: 1, xuyu, 2019-03-13 14:27:49.0
<==      Total: 1
xuyu
==>  Preparing: update user set name ='xiaoxu' where id=? 
==> Parameters: 1(Integer)
<==    Updates: 1
第二次调用....
==>  Preparing: select * from user where id=? 
==> Parameters: 1(Integer)
<==    Columns: id, name, update_time
<==        Row: 1, xiaoxu, 2019-03-13 14:27:49.0
<==      Total: 1
xiaoxu

为什么是三条SQL语句?不是有缓存吗?

我们的注意:sqlSession缓存为了防止脏数据,在我们进行增加、修改、删除的时候,都会清除一级缓存。下面我们看下源码。

public int update(String statement, Object parameter) { int var4; try { this.dirty = true; MappedStatement ms = this.configuration.getMappedStatement(statement); var4 = this.executor.update(ms, this.wrapCollection(parameter)); } catch (Exception var8) { throw ExceptionFactory.wrapException("Error updating database. Cause: " + var8, var8); } finally { ErrorContext.instance().reset(); } return var4; }

var4 = this.executor.update(ms, this.wrapCollection(parameter));

public int update(MappedStatement ms, Object parameter) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); if (this.closed) { throw new ExecutorException("Executor was closed."); } else { this.clearLocalCache();    //清除所有一级缓存 return this.doUpdate(ms, parameter); } }

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

protected PerpetualCache localCache; protected PerpetualCache localOutputParameterCache;

我们就明白了,PerpetualCache都会被清除掉了。

四:最后,我们来分析下:一级缓存存在哪些问题?

1、线程安全问题

2、集群会产生问题(主要的)

Mybatis深入源码分析之SQLSession一级缓存原理分析

如上图,会存在脏读问题。

所有我们怎么解决呢?集群的情况下,我们可以不去使用一级缓存,是不是可以直接关闭一级缓存?

答案是:不可用直接关闭一级缓存,Mybatis默认走SimpleExecutor,不能直接关闭一级缓存。

那么如何去关闭一级缓存?

方案1  在sql语句上 随机生成 不同的参数 存在缺点:map集合可能爆 内存溢出的问题

方案2  开启二级缓存

方案3  使用sqlSession强制清除缓存

方案4  创建新的sqlSession连接。

方案1:案例演示:

System.out.println("第一次调用...."); Map randomMap=new HashMap(); randomMap.put("randomString",new Random().nextInt()); randomMap.put("id",1); UserEntity o = sqlSession.selectOne("com.mayikt.mapper.UserMapper.getUser", randomMap); System.out.println(o.getName()); //中间执行一条update语句 //sqlSession.update("com.mayikt.mapper.UserMapper.updateUser",1); System.out.println("第二次调用...."); randomMap.put("randomString",new Random().nextInt()); UserEntity o2 = sqlSession.selectOne("com.mayikt.mapper.UserMapper.getUser", randomMap); System.out.println(o2.getName());

输出结果

第一次调用....
Opening JDBC Connection
Created connection 1463757745.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@573f2bb1]
==>  Preparing: select * from user where id=? and ?=? 
==> Parameters: 1(Integer), 1705142735(Integer), 1705142735(Integer)
<==    Columns: id, name, update_time
<==        Row: 1, xuyu, 2019-03-13 14:27:49.0
<==      Total: 1
xuyu
第二次调用....
==>  Preparing: select * from user where id=? and ?=? 
==> Parameters: 1(Integer), -13684383(Integer), -13684383(Integer)
<==    Columns: id, name, update_time
<==        Row: 1, xuyu, 2019-03-13 14:27:49.0
<==      Total: 1
xuyu

五:总结

SQLSession一级缓存原理分析流程

1、调用getMapper方法时候,会执行invoke方法,将我们的代理方法缓存起来

2、调用execute方法,最终执行selectOne方法

3、进入selectOne方法可知,sqlSession这个接口帮我们封装了CRUD的方法,便于我们操作SQL语句。

4、selectOne方法底层还是执行selectList方法查询所有,但取第一个

5、进入selectList方法,通过configuration得到SQL语句,再执行query方法

6、进入query方法,先执行CacheExecutor二级缓存执行器,发现没用配置二级缓存介质,则走SimpleExecutor简单执行器(一级缓存)

7、从HashMap中查找数据,一级缓存也没用数据,则会去查询数据库,查询到了数据,缓存到一级缓存

8、此时再去查询,就直接查询一级缓存数据(本地缓存)不会去查询数据库

后面解决方案我们下次再分析

本文参考

蚂蚁课堂

http://www.mayikt.com/

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Mybatis深入源码分析之Mapper与接口绑定原理源码分析
!(https://www.w3cschool.cn/attachments/image/20170807/1502093784622523.png)紧接上篇文章:Mybatis深入源码分析之SqlSessionFactoryBuilder源码分析(https://my.oschina.net/u/3995125/blog/3079296),这里
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Mybatis深入源码分析之SqlSessionFactory二级缓存原理分析
!(https://www.w3cschool.cn/attachments/image/20170807/1502093784622523.png)上篇内容回顾可以参考;Mybatis深入源码分析之SQLSession一级缓存原理分析(https://my.oschina.net/u/3995125/blog/3079788)这里再概括下上
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这