mybatis执行器
在mybatis
中包含四大件是指:executor, statementHandler,parameterHandler,resultHandler对象。
它们都是sqlSession的底层类实现,本篇的重点是了解一下executor
执行器的作用和一些原理。
在mybatis
中有三类执行器:
- 简单执行器:
SimpleExecutor
,是执行器的默认实现,继承了BaseExecutor
抽象类,利用StatementHandler
完成。每次调用执行方法都会构建一个StatementHandler
,并预设参数,然后执行。 - 可重用执行器:
ReuseExecutor
,可重复使用JDBC
中Statement
,减少预编译次数。该执行器在执行SQL
时会把Statement
缓存起来,如果下次碰到相同的SQL,就会取出来使用。 - 批处理执行器:
BatchExecutor
,每次的执行操作 不会立即提交到数据库,而是把对应的Statement
对象填充好参数之后暂存起来。调用doFlushStatements
的时候一次性提交到数据库,可用于批处理插入的场景。
执行器的顶层接口为Executor
,它定义了数据修改、数据查询、缓存维护等基本功能,还有一些辅助的API接口,比如提交事务、回滚事务等。
1 | // 简单列几个接口,还有一些没列出来 |
抽象接口BaseExecutor
实现Executor
并提供一些通用能力,比如有一级缓存,获取连接等。比如针对查询来说,它实现了顶层接口的query
方法,并实现一级缓存的相关逻辑,若缓存查询不同,通过doQuery
方法,下放给具体实现子类,也就是说,子类只需要实现具体的doQuery
方法。
在mybatis
中有三种执行器,分别为简单执行器、可重用执行器、批处理执行器。在这种结构下,若希望添加一级缓存应该如何处理?可以在BaseExecutor
上层抽象一层接口,这个抽象接口实现二级缓存的逻辑。但是mybatis
不采用这种方式,它通过装饰者模式,构建CachingExecutor
类实现于Executor
接口,并将Executor
作为他的一个属性,当自身的二级缓存获取不到值时,调用delegate
的同名方法,进入具体的执行器中。
一级缓存
一级缓存实际上就是一个HashMap
,根据一些相关的参数信息生成一个key
,结果集为值的hashMap
。它的CacheKey
的主要参数为statementID
,SQL
,执行参数等,所以有时候尽管是一模一样的SQL
也不会命中mybatis
的一级缓存。
mybatis
的代码是极其精简的,这是仅是使用HashMap
来存储一级缓存的内容,它甚至都不使用concurrentHashMap
,这是因为SqlSession
本身就不是线程安全的,对于SqlSession
来说他需要创建一个执行器,执行器对应于一条statementID
,如果出现并发,两个线程获取同一个SqlSession
,那么就有可能导致执行器中绑定的statementId
不一致,导致得到一级缓存的返回类型值不一样,从而报错。并且SqlSession
获取对应的JDBCConnection
连接同时得到事务,如果两个线程获取同一个线程,那么就表示两个线程拥有同一个事务,所以SqlSession
不是线程安全了,在Spring
中通过很多的手段去保证并发情况下SqlSession
的线程安全。
所以一级缓存是与会话相关,如果会话被关闭,那么一级缓存就失效。
由mybatis
生成对应的mapper
的动态代理,在执行对应的SQL
方法时,开启一个SqlSession
会话,通过会话和相关信息开启一个执行器,在执行器中先调用BaseExecutor#query()
,具体的一级缓存逻辑就是在query()
方法中,如果一级缓存中获取不到,调用执行器实现子类的doQuery()
方法。
1 |
|
看一段结合Spring
之后的缓存问题,测试结果为false
,说明一级缓存没有生效,这是由于Spring
在每次执行具体的调用时都自动进行了事务的提交,也就是说每执行一次selectById
就会开启一次会话。所以很多人会说在Spring
集成了mybatis
之后,mybatis
的一级缓存失效了。
那么如果就是想在Spring
中使用mybatis
的一级缓存。可以通过手动开启事务的方式。
1 |
|
在Spring
中对Mapper
做了一些动态代理的处理,通过getBean()
获取到的UserMapper
对象实际上已经是被Spring
动态代理过的对象,
Mapper
:是被spring
修改过的动态代理对象,做一些statementID
相关预处理等操作。SqlSessionTemplate
:它实现了SqlSession
接口,并将数据库的相关操作,比如query
、update
、insert
转发给具体的sqlSession
。而它的能力为拦截之后获取对应的sqlSession
会话对象。SqlSessionFactory
:去创建对应的事务、执行器,然后返回DefaultSqlSession
对象。
那么现在来看一些spring
使得mybatis
一级缓存没有生效的具体源码:
1 | public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { |
可以看出在sessionHolder
处如果获取到对应的session
则直接返回,如果获取不到则创建一个新的session
,所以这里实际上事务决定了会话,如果事务被提交,则事务被删除,也就导致 session
会话被删除,如果一个事务一直不提交,那么在这个事务内的所有数据库操作就会触发一级缓存的相关逻辑。
二级缓存
二级缓存也称作是应用级缓存,与一级缓存不同的是它的作用范围是整个应用,而且可以跨线程调用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据。由于二级缓存的作用范围是整个应用,所以需要为二级缓存考虑溢出淘汰的机制,而一级缓存的生命周期是一个会话,所以并没有特别为一级缓存考虑淘汰机制。
- 过期清理:清理一些存放时间过久的数据,设置一个有效期,对超过有效期的缓存进行清理。
- 线程安全:二级缓存是跨线程使用的,所以需要考虑到线程安全的问题。
- 命中率统计:根据命中率统计来给用户提供反馈,告诉用户某次查询是否命中了缓存。
通过装饰者模式结合责任链的方式来实现二级缓存。每一种类型的缓存都是单一职责,当做完自己本身需要做的事情之后,就将需要缓存的相关参数等信息传递给责任链的下一个位置。
1 | public interface Cache { |
这是缓存的顶层接口,非常简单,它通过接口屏蔽了复杂的底层调用,在使用的过程中,只需要通过这个接口Cache
进行相关缓存的操作。
1 |
|
1.chcheEnabled
:全局缓存开关,只要这个参数配置了false
,整个二级缓存就关闭。
2.useCache
:表示当前的statement
要不要使用缓存。
1 | public interface UserMapper { |
3.flushCache
:在查询时,将整个二级缓存清空。注意是整个。
1 | public interface UserMapper { |
4.<cache/>
或@CacheNamespace
是标记在xml
文件或者对应的mapper
文件,默认都是使用全限定类名作为缓存的命名空间,如果两个文件同时标记时会报错。
1 |
|
5.在第四点中说到,对应的mapper
文件和xml
不能同时标记,那么如果就是想共用,那么就需要使用到缓存空间引用。<cache-ref/>
或CacheNamespaceRef
1 |
|
由于二级缓存是跨线程使用,所以需要在事务提交之后的数据库操作,才能命中缓存。
每一个会话都有一个事务缓存管理器,暂存区的个数取决于访问了多少个mapper
。下方的缓存空间就是对应的mapper
的缓存命名空间。会话一旦关闭,它所对应的事务缓存管理器就会被清理,从而导致暂存区数据被清理。在数据的操作过程,都是先操作暂存区,只有在事务提交或者事务回滚之后才会将暂存区数据提交到对应的缓存空间。
![image-20221127115857163](/Users/jiangjiamin/Library/Application Support/typora-user-images/image-20221127115857163.png)
1 |
|
二级缓存的执行流程:
根据上图可知:查询是直接查询缓存空间,其他操作是对暂存区数据进行操作,当进行commit
操作时将暂存区的操作数据添加到缓存空间。
1 | // TransactionalCache#commit |