Mybatis原理——执行原理详解
总结于B站鲁班大叔视频:https://www.bilibili.com/video/BV1Tp4y1X7FM?p=13&spm_id_from=pageDriver
概述
JDBC的执行流程可以大致分为:
- 获得连接
- 预编译sql
- 设置参数
- 执行sql
Mybatis执行原理大致分为:
- 动态代理MapperProxy
- sql会话Sqlsession
- 执行器Executor
- JDBC处理器StatementHandler
图中JDBC圈起来的部分就对应了sql具体的执行过程,属于Mybatis执行器范围内。本文主要针对sql会话与执行器来展开讨论。
Mybatis的执行过程
Mybatis使用门面模式提供了统一的门面接口API:
- 基本的API:增、删、改、查等
- 辅助的API:提交、关闭会话等
这些统一的API就是Sqlsession提供的,具体的API实现交由执行器来完成。
Executor的实现
- SimpleExecutor(默认实现)
- 其每次执行sql,无论sql是否一样,都会进行sql的预处理。
- ReuseExecutor(可复用MappedStatement,MappedStatement包装的是sql信息)
- 如果多次执行的sql一致,只会进行一次sql预处理。
- sql的重用,之和sql有关,即sql和参数一致,就可以重用,这个很重要。
- BatchExecutor(批处理)
- 只针对增、删、改操作,也就是修改操作。如果是查询语句,则和SimpleExecutor没有区别。
- 在批处理的情况下,对相同的sql,只会预处理一次,多次设置sql的参数值。
- 必须手动调用BatchExecutor.doFlushStatement()方法提交事务。
- BaseExecutor(执行器抽象类)
- 实现SimpleExecutor、BatchExecutor、ReuseExecutor三者的重复操作:一级缓存、获取连接等。
- SimpleExecutor、BatchExecutor、ReuseExecutor继承BaseExecutor,BaseExecutor实现Executor。
- 定义query和doUpdate方法供SimpleExecutor、BatchExecutor、ReuseExecutor使用。
一级缓存
由于一级缓存、获取连接的实现在BaseExecutor中,单独使用SimpleExecutor、BatchExecutor、ReuseExecutor就无法得到BaseExecutor的支持:
- BaseExecutor中query方法会创建缓存的key,并调用其重载的方法query方法,通过localcache(key)去获取一级缓存,如果没有缓存,就会去调用doQuery方法,这个【doQuery】方法就是子类(SimpleExecutor、BatchExecutor、ReuseExecutor)中实现的数据库操作方法。
二级缓存
- CachingExecutor
- 实现Executor接口
- 只专注实现二级缓存的逻辑。
- CachingExecutor二级缓存的逻辑执行完成之后,将会把业务交由下一个执行器处理。下一个执行器(SimpleExecutor或BatchExecutor或ReuseExecutor)由构造方法指定。装饰者模式。
需要注意的是:
- 一级缓存的数据是开始执行的时候就生成了,二级缓存的数据是提交执行过后才会生成。
- 之后的查询会先访问二级缓存,再访问一级缓存。
缓存命中场景
一级缓存
一级缓存是key-value形式的,实质底层就是一个HashMap。作用范围是sqlsession,当数据库会话结束之后,随之消亡。
命中条件:
- sql和参数相同。
- 相同的statementID(需要调用相同mapper中的查询方法相同)
- 同一个sqlsession(这就是为什么一级缓存也叫会话级缓存)
- 如果是分页查询,使用的RowBounds也要相同(也就是查询起始行、查询数据大小都要相同)
影响命中一级缓存的设置:
- 手动清空sqlsession.clearCache()
- Mapper层(DAO层)查询方法使用了@Options(fluashCache = Opions.FlushCachePolicy.TRUE)
- 该参数设置在每次查询后都会清空一级缓存
- 执行了update操作
- rollback也会清空相应的缓存
Spring集成mybatis一级缓存失效问题
spring集成Mybatis之后,不满足同一个sqlsession,导致一级缓存失效。在没有配置事务的情况下,每次执行sql都会构造一个新的会话。是由于srping的动态代理导致的。
解决办法就是将sql执行放在同一个事务中,就会使用同一个sqlsession,同一个sqlsession的情况下,一级缓存就不会失效。
二级缓存
为什么已经有了一级缓存,还需要二级缓存呢?
二级缓存也称作是应用级缓存,与一级缓存不同的是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据。
- 二级缓存可以使用内存和磁盘进行存储。
- 溢出淘汰:在有限的内存中缓存数据,始终会面临缓存承载满的情况,就会存在缓存淘汰机制
- FIFO,先进先出的淘汰算法。
- LRU,最近最少使用的淘汰机制。
- 缓存设置过期时间,会进行过期清理。
- 线程安全
- 命中率统计
- 序列化
Mybatis二级缓存设计、
接口定义:
实现类
Mybatis使用装饰器+责任链模式构建了完整的二级缓存结构:
- SynchronizedCache:线程同步
- LoggingCache:记录命中率
- LruCache:防止溢出
- ScheduledCache:过期清理
- BlockingCache:防止内存穿透
- PerpetualCache:内存存储
调用Cache实现类的任何方法都会沿着上面的xxxCache完成装饰,并执行。
二级缓存命中条件:
- 必须提交了事务
- 即使是设置了autocommite也不能命中缓存,除非是手动调用了commite方法,或者是关闭当前的事务。
- sql和参数相同。
- 如果是分页查询,使用的RowBounds也要相同(也就是查询起始行、查询数据大小都要相同)
影响二级缓存命中的设置:
- 缓存的开关userCache = true。
- Mapper层(DAO层)查询方法使用了@Options(fluashCache = Opions.FlushCachePolicy.TRUE)
- 该参数设置在每次查询后都会清空一级缓存
- 声明缓存空间
- <cache><cache/>或者@CacheNamespace
- 同时必须引用缓存空间:<cache-ref>或@CacehNamespaceRef
为什么要提交之后才能命中二级缓存?
因为二级缓存是跨线程使用的:
例如sqlsessionA和sqlsessionB去查询同样的数据
- A先修改了数据,再对该数据进行查询
- B去查询该数据将结果填充进入二级缓存
- A去查询该数据的时候,B进行的回滚
- A这时候如果从二级缓存读取数据,就会产生脏读。
所有执行结果在commit之前,都会放入事务缓存管理器的暂存区,只有被commit之后才会放入缓存区,这里的缓存区指的就是装饰器+责任链模式构建了完整的二级缓存结构。
StatementHandler
JDBC处理器,基于JDBC构建的Statement并设置参数,然后执行sql,没调用会话当中的一次sql,就有与之相对应的且唯一的Statement实例。
主要功能
- 声明(创建)Statement
- prepare方法基于Connection创建Statement。
- 设置参数
- 查询
- 修改
主要是实现:
- BaseStatementHandler(从子类处理器中抽象出公共的处理逻辑)
- SimpleStatementHandler(简单处理器)
- PreparedStatementHandler(预处理器)
- 大多数情况下都是使用该处理器,预处理性能更高,会进行参数转义,防止sql注入。
- CallableStatementHandler(存储过程处理器)
PreparedStatementHandler执行流程
这个过程包括从 执行器 -> StatementHandler -> 参数处理 -> 结果集处理
- 执行器
- StatementHandler
- 预编译
- 设置参数
- 执行sql
- 结果集映射成Java bean
声明或者是创建Statement
执行查询的开始代码,是在具体Executor的实现类中的doQuery方法:
@Overridepublic <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();//创建StatementStatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);stmt = prepareStatement(handler, ms.getStatementLog());return handler.query(stmt, resultHandler);} finally {closeStatement(stmt);}}public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {//创建Statement实例StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;}//RoutingStatementHandler构造方法根据Statement类型创建具体的Statementpublic RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {switch (ms.getStatementType()) {case STATEMENT:delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case PREPARED:delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case CALLABLE:delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;default:throw new ExecutorException("Unknown statement type: " + ms.getStatementType());}}
创建完Statement之后,回到doQuery方法,调用prepareStatement()方法
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;Connection connection = getConnection(statementLog);//创建Statementstmt = handler.prepare(connection, transaction.getTimeout());//设置参数handler.parameterize(stmt);return stmt;}@Overridepublic Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {ErrorContext.instance().sql(boundSql.getSql());Statement statement = null;try {//instantiateStatement抽象方法由具体的子类实现(PreparedStatementHandler、SimpleStatementHandler、CallableStatementHandler)statement = instantiateStatement(connection);//设置超时时间setStatementTimeout(statement, transactionTimeout);//设置返回行数setFetchSize(statement);return statement;} catch (SQLException e) {closeStatement(statement);throw e;} catch (Exception e) {closeStatement(statement);throw new ExecutorException("Error preparing statement. Cause: " + e, e);}}
回到prepareStatement方法,handler.parameterize(stmt);设置参数。parameterize方法是各个StatementHandler实现类的具体实现。最后回到doQuery方法通过具体的handler的query方法执行真正的sql操作。
Mybatis的运行原理
获取SqlSessionFactory对象
- 创建SqlSessionFactory实际上就是加载配置信息,包括Mybatis的配置文件以及加载Mapper文件。
- 将Mapper文件中的sql解析为MappedStatement(一个MappedStatement对应一个mapper文件中的sql) 以及 配置信息封装为Configuration。
- Configuration注入DefaultSqlSessionFactory返回。
OpenSession
DefaultSqlSessionFactory的openSession
@Overridepublic SqlSession openSession() {return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {//获取配置信息final Environment environment = configuration.getEnvironment();//创建事务final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);//根据执行器类型创建执行器final Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);} finally {ErrorContext.instance().reset();}}
获取查询的代理对象
openSession执行之后返回了DefaultSqlSession,DefaultSqlSession用来获取查询的代理对象。其实DefaultSqlSession已经可以根据之前加载的配置信息以及MappeStatement进行查询了。
DefaultSqlSession的getMapper
@Overridepublic <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);}
Configuration
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);public <T> T getMapper(Class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession);}
MapperRegistry
private final Configuration config;private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();public <T> T getMapper(Class<T> type, SqlSession sqlSession) {final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);if (mapperProxyFactory == null) {throw new BindingException("Type " + type + " is not known to the MapperRegistry.");}try {//根据Mapper的代理工厂构建Mapper的代理对象return mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new BindingException("Error getting mapper instance. Cause: " + e, e);}}
MapperProxyFactory
public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}protected T newInstance(MapperProxy<T> mapperProxy) {//JDK动态代理return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}
最终返回的Mapper
执行sql
执行流程就是执行器Executor的原理的,中间穿插了一级缓存和二级缓存的逻辑。
- 代理对象包含了DefaultSqlSession,实际执行查询也是通过DefaultSqlSession
- DefaultSqlSession的创建中也包含了具体的执行器,通过执行器的doQuery进行实际查询
- 具体的Executor创建其对应的StatementHandler,进行参数的设置,预编译(ParameterHandler),执行查询(StatementHandler),返回结果处理(ResultSetHandler)
Mybatis原理——执行原理详解相关推荐
- DeepLearning tutorial(4)CNN卷积神经网络原理简介+代码详解
FROM: http://blog.csdn.net/u012162613/article/details/43225445 DeepLearning tutorial(4)CNN卷积神经网络原理简介 ...
- Nginx(二):反向代理原理 与 配置文件详解
相关阅读: Nginx(一):Nginx原理概述 与 安装步骤详解 Nginx(二):反向代理原理 与 配置文件详解 Nginx(三):负载均衡策略 与 Nginx静态服务器 Nginx(四):Ngi ...
- Spark SQL原理及常用方法详解(二)
Spark SQL 一.Spark SQL基础知识 1.Spark SQL简介 (1)简单介绍 (2)Datasets & DataFrames (3)Spark SQL架构 (4)Spark ...
- java的markword_【转帖】Java工具结构与锁实现原理及MarkWord详解
Java工具结构与锁实现原理及MarkWord详解 https://www.pianshen.com/article/2382167638/ 我们都知道,Java工具存储在堆(Heap)内存.那么一个 ...
- 【胖虎的逆向之路】04——脱壳(一代壳)原理脱壳相关概念详解
[胖虎的逆向之路]04--脱壳(一代壳)原理&脱壳相关概念详解 [胖虎的逆向之路]01--动态加载和类加载机制详解 [胖虎的逆向之路]02--Android整体加壳原理详解&实现 [胖 ...
- Cookie的工作原理和应用详解
Cookie的工作原理和应用详解 1. Cookie 原理 1.1 Cookie 背景信息 1.2 Cookie 工作原理 1.3 Cookie 创建.获取.修改 1.4 Cookie 共享范围 1. ...
- Pytorch | yolov3原理及代码详解(二)
阅前可看: Pytorch | yolov3原理及代码详解(一) https://blog.csdn.net/qq_24739717/article/details/92399359 分析代码: ht ...
- 晶振工作原理及参数详解
本文转载于:https://www.cnblogs.com/sunshine-jackie/p/8137293.html 晶振工作原理及参数详解(最透彻) 晶振工作原理及参数详解(最透彻) 原文链接点 ...
- Dubbo 原理和机制详解
Dubbo 是一款Java RPC框架,致力于提供高性能的 RPC 远程服务调用方案.作为主流的微服务框架之一,Dubbo 为开发人员带来了非常多的便利. 1. Dubbo核心功能 Dubbo主要提供 ...
- redis队列优先级java实现_Redis 实现队列原理的实例详解
Redis 实现队列原理的实例详解 场景说明: ·用于处理比较耗时的请求,例如批量发送邮件,如果直接在网页触发执行发送,程序会出现超时 ·高并发场景,当某个时刻请求瞬间增加时,可以把请求写入到队列,后 ...
最新文章
- 电子商务(电销)平台中用户模块(User)数据库设计明细
- 去哪编辑html5页面,h5页面 判断网页在哪打开
- 绝了!Pandas绘图功能
- [开源]Dapper Repository 一种实现方式
- 安装Windows Nano Server虚拟机
- 清明节特辑 |记忆存储、声音还原、性格模仿……AI可以让人类永生吗?
- 【Pre蓝桥杯嵌入式】【STM32】Unkown device
- 拿完年终奖换工作?频繁跳槽职场人工资低于同龄人平均水平
- 浅谈Rem 及其转换原理
- 使用windows2008R2自带磁盘管理进行分区
- 【负载观测】永磁同步电机的负载观测及前馈补偿
- 算法工程师必备技能(Python 优化提速小技巧)
- 微信小程序之设置背景图片
- FineReport帆软报表使用入门
- iOS开发-Xcode8兼容iOS7手记
- android twitter 分享代码,Twitter分享集成
- jeecg ajax验证,jeecg权限模块学习
- 一道关于SVM的机器学习作业题
- nacos 未读取到合法数据,请检查导入的数据文件
- 手机android系统问题怎么解决方案,4解决Android系统崩溃问题的解决方案
热门文章
- 彻底解决WPS右键没有新建文件的问题
- 页面生命周期:DOMContentLoaded, load, beforeunload, unload解析
- 电脑文件误删除如何恢复?可以快速找回
- mysql 1093 you can_mysql中错误:1093-You can’t specify target table for update in FROM clause的解决方法...
- ansible aws_如何使用Ansible管理您的AWS资源
- 解决网络丢包问题及故障判断方法
- 验证码之google的reCAPTCHA使用
- 据说很多搞软件的羡慕硬件工程师
- java3d关闭透视,3DMax怎么去除透视效果?3D新手请详解?
- was部署java项目_web工程was部署