这里有新鲜出炉的 MyBatis 教程,程序狗速度看过来!
MyBatis 本是 apache 的一个开源项目 iBatis, 2010 年这个项目由 apache software foundation 迁移到了 google code,并且改名为 MyBatis 。
这篇文章主要介绍了 MyBatis 一二级缓存的相关知识,非常不错,具有参考借鉴价值, 需要的朋友可以参考下
MyBatis 缓存
我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于 DB 而言,数据是持久化在磁盘中的,因此查询操作需要通过 IO,IO 操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相同的查询语句,完全可以把查询结果存储起来,下次查询同样的内容的时候直接从内存中获取数据即可,这样在某些场景下可以大大提升查询效率。
MyBatis 的缓存分为两种:
一级缓存,一级缓存是 SqlSession 级别的缓存,对于相同的查询,会从缓存中返回结果而不是查询数据库
二级缓存,二级缓存是 Mapper 级别的缓存,定义在 Mapper 文件的 <cache> 标签中并需要开启此缓存,多个 Mapper 文件可以共用一个缓存,依赖 <cache-ref> 标签配置
下面来详细看一下 MyBatis 的一二级缓存。
MyBatis 一级缓存工作流程
接着看一下 MyBatis 一级缓存工作流程。前面说了,MyBatis 的一级缓存是 SqlSession 级别的缓存,当 openSession() 的方法运行完毕或者主动调用了 SqlSession 的 close 方法,SqlSession 就被回收了,一级缓存与此同时也一起被回收掉了。前面的文章有说过,在 MyBatis 中,无论 selectOne 还是 selectList 方法,最终都被转换为了 selectList 方法来执行,那么看一下 SqlSession 的 selectList 方法的实现:
- 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();
- }
- }
继续跟踪第 4 行的代码,到 BaseExeccutor 的 query 方法:
- public < E > List < E > query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
- BoundSql boundSql = ms.getBoundSql(parameter);
- CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
- return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
- }
第 3 行构建缓存条件 CacheKey,这里涉及到怎么样条件算是和上一次查询是同一个条件的一个问题,因为同一个条件就可以返回上一次的结果回去,这部分代码留在下一部分分析。
接着看第 4 行的 query 方法的实现,代码位于 CachingExecutor 中:
- 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, parameterObject, boundSql);
- @SuppressWarnings("unchecked")
- List<E> list = (List<E>) tcm.getObject(cache, key);
- if (list == null) {
- list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- tcm.putObject(cache, key, list); // issue #578 and #116
- }
- return list;
- }
- }
- return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- }
第 3 行~ 第 16 行的代码先不管,继续跟第 17 行的 query 方法,代码位于 BaseExecutor 中:
- 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--;
- }...
- }
看 12 行,query 的时候会尝试从 localCache 中去获取查询结果,如果获取到的查询结果为 null,那么执行 16 行的代码从 DB 中捞数据,捞完之后会把 CacheKey 作为 key,把查询结果作为 value 放到 localCache 中。
MyBatis 一级缓存存储流程看完了,接着我们从这段代码中可以得到三个结论:
MyBatis 的一级缓存是 SqlSession 级别的,但是它并不定义在 SqlSessio 接口的实现类 DefaultSqlSession 中,而是定义在 DefaultSqlSession 的成员变量 Executor 中,Executor 是在 openSession 的时候被实例化出来的,它的默认实现为 SimpleExecutor
MyBatis 中的一级缓存,与有没有配置无关,只要 SqlSession 存在,MyBastis 一级缓存就存在,localCache 的类型是 PerpetualCache,它其实很简单,一个 id 属性 + 一个 HashMap 属性而已,id 是一个名为 "localCache" 的字符串,HashMap 用于存储数据,Key 为 CacheKey,Value 为查询结果
MyBatis 的一级缓存查询的时候默认都是会先尝试从一级缓存中获取数据的,但是我们看第 6 行的代码做了一个判断,ms.isFlushCacheRequired(),即想每次查询都走 DB 也行,将
从 MyBatis 一级缓存来看,它以单纯的 HashMap 做缓存,没有容量控制,而一次 SqlSession 中通常来说并不会有大量的查询操作,因此只适用于一次 SqlSession,如果用到二级缓存的 Mapper 级别的场景,有可能缓存数据不断碰到而导致内存溢出。
还有一点,差点忘了写了,<insert>、<delete>、<update> 最终都会转换为 update 方法,看一下 BaseExecutor 的 update 方法:
- public int update(MappedStatement ms, Object parameter) throws SQLException {
- ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
- if (closed) {
- throw new ExecutorException("Executor was closed.");
- }
- clearLocalCache();
- return doUpdate(ms, parameter);
- }
第 6 行 clearLocalCache() 方法,这意味着所有的增、删、改都会清空本地缓存,这和是否配置了 flushCache=true 是无关的。
这很好理解,因为增、删、改这三种操作都可能会导致查询出来的结果并不是原来的结果,如果增、删、改不清理缓存,那么可能导致读取出来的数据是脏数据。
一级缓存的 CacheKey
接着我们看下一个问题:怎么样的查询条件算和上一次查询是一样的查询,从而返回同样的结果回去?这个问题,得从 CacheKey 说起。
我们先看一下 CacheKey 的数据结构:
- public class CacheKey implements Cloneable,
- Serializable {
- private static final long serialVersionUID = 1146682552656046210L;
- public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
- private static final int DEFAULT_MULTIPLYER = 37;
- private static final int DEFAULT_HASHCODE = 17;
- private int multiplier;
- private int hashcode;
- private long checksum;
- private int count;
- private List < Object > updateList;...
- }
其中最重要的是第 14 行的 updateList 这个两个属性,为什么这么说,因为 HashMap 的 Key 是 CacheKey,而 HashMap 的 get 方法是先判断 hashCode,在 hashCode 冲突的情况下再进行 equals 判断,因此最终无论如何都会进行一次 equals 的判断,看下 equals 方法的实现:
- public boolean equals(Object object) {
- if (this == object) {
- return true;
- }
- if (! (object instanceof CacheKey)) {
- return false;
- }
- final CacheKey cacheKey = (CacheKey) object;
- if (hashcode != cacheKey.hashcode) {
- return false;
- }
- if (checksum != cacheKey.checksum) {
- return false;
- }
- if (count != cacheKey.count) {
- return false;
- }
- for (int i = 0; i < updateList.size(); i++) {
- Object thisObject = updateList.get(i);
- Object thatObject = cacheKey.updateList.get(i);
- if (thisObject == null) {
- if (thatObject != null) {
- return false;
- }
- } else {
- if (!thisObject.equals(thatObject)) {
- return false;
- }
- }
- }
- return true;
- }
看到整个方法的流程都是围绕着 updateList 中的每个属性进行逐一比较,因此再进一步的,我们要看一下 updateList 中到底存储了什么。
关于 updateList 里面存储的数据我们可以看下哪里使用了 updateList 的 add 方法,然后一步一步反推回去即可。updateList 中数据的添加是在 doUpdate 方法中:
- private void doUpdate(Object object) {
- int baseHashCode = object == null ? 1 : object.hashCode();
- count++;
- checksum += baseHashCode;
- baseHashCode *= count;
- hashcode = multiplier * hashcode + baseHashCode;
- updateList.add(object);
- }
它的调用方为 update 方法:
- public void update(Object object) {
- if (object != null && object.getClass().isArray()) {
- int length = Array.getLength(object);
- for (int i = 0; i < length; i++) {
- Object element = Array.get(object, i);
- doUpdate(element);
- }
- } else {
- doUpdate(object);
- }
- }
这里主要是对输入参数是数组类型进行了一次判断,是数组就遍历逐一做 doUpdate,否则就直接做 doUpdate。再看 update 方法的调用方,其实 update 方法的调用方有挺多处,但是这里我们要看的是 Executor 中的,看一下 BaseExecutor 中的 createCacheKey 方法实现:
- ...
- CacheKey cacheKey = new CacheKey();
- cacheKey.update(ms.getId());
- cacheKey.update(rowBounds.getOffset());
- cacheKey.update(rowBounds.getLimit());
- cacheKey.update(boundSql.getSql());
- ...
到了这里应当一目了然了,MyBastis 从三个维度判断两次查询是相同的:
<select> 标签所在的 Mapper 的 Namespace+<select> 标签的 id 属性
RowBounds 的 offset 和 limit 属性,RowBounds 是 MyBatis 用于处理分页的一个类,offset 默认为 0,limit 默认为 Integer.MAX_VALUE
<select> 标签中定义的 sql 语句
即只要两次查询满足以上三个条件且没有定义 flushCache="true",那么第二次查询会直接从 MyBatis 一级缓存 PerpetualCache 中返回数据,而不会走 DB。
MyBatis 二级缓存
上面说完了 MyBatis,接着看一下 MyBatis 二级缓存,还是从二级缓存工作流程开始。还是从 DefaultSqlSession 的 selectList 方法进去:
- 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();
- }
- }
执行 query 方法,方法位于 CachingExecutor 中:
- 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);
- }
继续跟第 4 行的 query 方法,同样位于 CachingExecutor 中:
- 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, parameterObject, boundSql);
- @SuppressWarnings("unchecked")
- List<E> list = (List<E>) tcm.getObject(cache, key);
- if (list == null) {
- list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- tcm.putObject(cache, key, list); // issue #578 and #116
- }
- return list;
- }
- }
- return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- }
从这里看到,执行第 17 行的 BaseExecutor 的 query 方法之前,会先拿 Mybatis 二级缓存,而 BaseExecutor 的 query 方法会优先读取 MyBatis 一级缓存,由此可以得出一个重要结论:假如定义了 MyBatis 二级缓存,那么 MyBatis 二级缓存读取优先级高于 MyBatis 一级缓存。
而第 3 行~ 第 16 行的逻辑:
第 5 行的方法很好理解,根据 flushCache=true 或者 flushCache=false 判断是否要清理二级缓存
第 7 行的方法是保证 MyBatis 二级缓存不会存储存储过程的结果
第 9 行的方法先尝试从 tcm 中获取查询结果,这个 tcm 解释一下,这又是一个装饰器模式(数数 MyBatis 用到了多少装饰器模式了),创建一个事物缓存 TranactionalCache,持有 Cache 接口,Cache 接口的实现类就是根据我们在 Mapper 文件中配置的
第 10 行~ 第 12 行,如果没有从 MyBatis 二级缓存中拿到数据,那么就会查一次数据库,然后放到 MyBatis 二级缓存中去
至于如何判定上次查询和这次查询是一次查询?由于这里的 CacheKey 和 MyBatis 一级缓存使用的是同一个 CacheKey,因此它的判定条件和前文写过的 MyBatis 一级缓存三个维度的判定条件是一致的。
最后再来谈一点,"Cache cache = ms.getCache()" 这句代码十分重要,这意味着 Cache 是从 MappedStatement 中获取到的,而 MappedStatement 又和每一个 < insert>、<delete>、<update>、<select> 绑定并在 MyBatis 启动的时候存入 Configuration 中:
- protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
因此 MyBatis 二级缓存的生命周期即整个应用的生命周期,应用不结束,定义的二级缓存都会存在在内存中。
从这个角度考虑,为了避免 MyBatis 二级缓存中数据量过大导致内存溢出,MyBatis 在配置文件中给我们增加了很多配置例如 size(缓存大小)、flushInterval(缓存清理时间间隔)、eviction(数据淘汰算法)来保证缓存中存储的数据不至于太过庞大。
MyBatis 二级缓存实例化过程
接着看一下 MyBatis 二级缓存 <cache> 实例化的过程,代码位于 XmlMapperBuilder 的 cacheElement 方法中:
- private void cacheElement(XNode context) throws Exception {
- if (context != null) {
- String type = context.getStringAttribute("type", "PERPETUAL");
- Class < ?extends Cache > typeClass = typeAliasRegistry.resolveAlias(type);
- String eviction = context.getStringAttribute("eviction", "LRU");
- Class < ?extends Cache > evictionClass = typeAliasRegistry.resolveAlias(eviction);
- Long flushInterval = context.getLongAttribute("flushInterval");
- Integer size = context.getIntAttribute("size");
- boolean readWrite = !context.getBooleanAttribute("readOnly", false);
- boolean blocking = context.getBooleanAttribute("blocking", false);
- Properties props = context.getChildrenAsProperties();
- builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
- }
- }
这里分别取 <cache> 中配置的各个属性,关注一下两个默认值:
type 表示缓存实现,默认是 PERPETUAL,根据 typeAliasRegistry 中注册的,PERPETUAL 实际对应 PerpetualCache,这和 MyBatis 一级缓存是一致的
eviction 表示淘汰算法,默认是 LRU 算法
第 3 行~ 第 11 行拿到了所有属性,那么调用 12 行的 useNewCache 方法创建缓存:
- public Cache useNewCache(Class<? extends Cache> typeClass,
- Class<? extends Cache> evictionClass,
- Long flushInterval,
- Integer size,
- boolean readWrite,
- boolean blocking,
- Properties props) {
- Cache cache = new CacheBuilder(currentNamespace)
- .implementation(valueOrDefault(typeClass, PerpetualCache.class))
- .addDecorator(valueOrDefault(evictionClass, LruCache.class))
- .clearInterval(flushInterval)
- .size(size)
- .readWrite(readWrite)
- .blocking(blocking)
- .properties(props)
- .build();
- configuration.addCache(cache);
- currentCache = cache;
- return cache;
- }
这里又使用了建造者模式,跟一下第 16 行的 build() 方法,在此之前该传入的参数都已经传入了 CacheBuilder:
- public Cache build() {
- setDefaultImplementations();
- Cache cache = newBaseCacheInstance(implementation, id);
- setCacheProperties(cache);
- // issue #352, do not apply decorators to custom caches
- if (PerpetualCache.class.equals(cache.getClass())) {
- for (Class < ?extends Cache > decorator: decorators) {
- cache = newCacheDecoratorInstance(decorator, cache);
- setCacheProperties(cache);
- }
- cache = setStandardDecorators(cache);
- } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
- cache = new LoggingCache(cache);
- }
- return cache;
- }
第 3 行的代码,构建基础的缓存,implementation 指的是 type 配置的值,这里是默认的 PerpetualCache。
第 6 行的代码,如果是 PerpetualCache,那么继续装饰(又是装饰器模式,可以数数这几篇 MyBatis 源码解析的文章里面出现了多少次装饰器模式了),这里的装饰是根据 eviction 进行装饰,到这一步,给 PerpetualCache 加上了 LRU 的功能。
第 11 行的代码,继续装饰,这次 MyBatis 将它命名为标准装饰,setStandardDecorators 方法实现为:
- private Cache setStandardDecorators(Cache cache) {
- try {
- MetaObject metaCache = SystemMetaObject.forObject(cache);
- if (size != null && metaCache.hasSetter("size")) {
- metaCache.setValue("size", size);
- }
- if (clearInterval != null) {
- cache = new ScheduledCache(cache); ((ScheduledCache) cache).setClearInterval(clearInterval);
- }
- if (readWrite) {
- cache = new SerializedCache(cache);
- }
- cache = new LoggingCache(cache);
- cache = new SynchronizedCache(cache);
- if (blocking) {
- cache = new BlockingCache(cache);
- }
- return cache;
- } catch(Exception e) {
- throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
- }
- }
这次是根据其它的配置参数来:
Cache 全部装饰完毕,返回,至此 MyBatis 二级缓存生成完毕。
最后说一下,MyBatis 支持三种类型的二级缓存:
从 build() 方法来看,后两种场景的 Cache,MyBatis 只会将其装饰为 LoggingCache,理由很简单,这些缓存的定期清除功能、淘汰过期数据功能开发者自己或者第三方缓存都已经实现好了,根本不需要依赖 MyBatis 本身的装饰。
来源: http://www.phperz.com/article/17/0819/338297.html