前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Mybatis深入源码分析之SQLSession一级缓存原理分析

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

作者头像
须臾之余
发布2019-07-30 14:44:34
4670
发布2019-07-30 14:44:34
举报
文章被收录于专栏:须臾之余须臾之余

一:invoke()方法源码分析

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

代码语言:javascript
复制
// 5.操作Mapper接口
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制
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()方法

代码语言:javascript
复制
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()方法。所以下面的两行代码是等效的,效果一样

代码语言:javascript
复制
UserEntity user = mapper.getUser(2);
UserEntity o = sqlSession.selectOne("com.mayikt.mapper.UserMapper.getUser", 2);

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

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

代码语言:javascript
复制
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

下面会执行这段代码

代码语言:javascript
复制
public SqlSession openSession() {
    return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);//进入这里
}

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

代码语言:javascript
复制
public <T> T selectOne(String statement, Object parameter) {
    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;
    }
}

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

代码语言:javascript
复制
public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
代码语言:javascript
复制
public <E> List<E> 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;
}

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

下面进入这个方法

代码语言:javascript
复制
MappedStatement ms = this.configuration.getMappedStatement(statement);

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

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

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

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

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

二:OpenSession()方法源码分析

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

代码语言:javascript
复制
// 4.获取Session
SqlSession sqlSession = sqlSessionFactory.openSession();
代码语言:javascript
复制
public SqlSession openSession() {
    return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}
代码语言:javascript
复制
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;
}
代码语言:javascript
复制
Executor executor = this.configuration.newExecutor(tx, execType);

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

代码语言:javascript
复制
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;
}

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

最后返回executor

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

  • SimpleExecutor: 默认的 Executor,每个 SQL 执行时都会创建新的 Statement
  • CachingExecutor: 可缓存数据的 Executor,用代理模式包装了其它类型的 Executor
  • ReuseExecutor: 相同的 SQL 会复用 Statement
  • BatchExecutor: 用于批处理的 Executor
代码语言:javascript
复制
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);
}

进入这行代码:

代码语言:javascript
复制
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
代码语言:javascript
复制
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    return this.delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}

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

代码语言:javascript
复制
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<ParameterMapping> 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
    }
}

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

代码语言:javascript
复制
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);
}
代码语言:javascript
复制
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);
}

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

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

代码语言:javascript
复制
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

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

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

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

代码语言:javascript
复制
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 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中查找缓存

代码语言:javascript
复制
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
代码语言:javascript
复制
public Object getObject(Object key) {
    return this.cache.get(key);
}

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

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

代码语言:javascript
复制
private <E> List<E> 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;
}

最后回到前面:

代码语言:javascript
复制
protected int queryStack = 0;

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

三:Mybatis缓存源码分析

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

代码语言:javascript
复制
<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

结果:说明只发出一条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语句,结果是怎样?

代码语言:javascript
复制
//中间执行一条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缓存为了防止脏数据,在我们进行增加、修改、删除的时候,都会清除一级缓存。下面我们看下源码。

代码语言:javascript
复制
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;
}
代码语言:javascript
复制
var4 = this.executor.update(ms, this.wrapCollection(parameter));
代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制
public void clearLocalCache() {
    if (!this.closed) {
        this.localCache.clear();
        this.localOutputParameterCache.clear();
    }
}
代码语言:javascript
复制
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;

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

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

1、线程安全问题 2、集群会产生问题(主要的)

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

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

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

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

方案1 在sql语句上 随机生成 不同的参数 存在缺点:map集合可能爆 内存溢出的问题 方案2 开启二级缓存 方案3 使用sqlSession强制清除缓存 方案4 创建新的sqlSession连接。

方案1:案例演示:

代码语言:javascript
复制
<select id="getUser" parameterType="map"
        resultType="com.mayikt.entity.UserEntity">
    select * from user where id=#{id} and #{randomString}=#{randomString}
</select>
代码语言:javascript
复制
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、此时再去查询,就直接查询一级缓存数据(本地缓存)不会去查询数据库

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一:invoke()方法源码分析
  • 二:OpenSession()方法源码分析
  • 三:Mybatis缓存源码分析
  • 四:最后,我们来分析下:一级缓存存在哪些问题?
  • 五:总结
    • SQLSession一级缓存原理分析流程
    相关产品与服务
    云数据库 SQL Server
    腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档