MyBatis 的一级缓存与二级缓存

一、简述

MyBatis 的一级缓存是基于数据库会话(SqlSession 对象)的,默认开启。二级缓存是基于全局(nameSpace)的,开启需要配置。

对于 JDBC 操作,如果需要连续请求 id=1 的用户数据,那么就要进行两次的数据库连接,获取数据库中的数据。相同的数据,进行两次数据库连接,这肯定会造成资源的浪费。基于面向对象,可以把第一次获取的数据保存到一个对象中,下一次直接从对象中获取就行了,如图:

获取的内容保存在对象中,在一个请求期间,直接使用或者传递对象就可以了。对于 JDBC 的操作,可以自己定义类或者集合来保存数据库中的数据,来避免连续请求数据库的问题。这里用来保存数据的对象或者集合,也能称之为缓存。

但是使用了三层架构之后,Dao 层和 Dao 层之间有可能互相是不清楚的。如果有一个复杂的业务要在 Service 层中进行处理,需要分别调用不同 Dao 层中的数据,那这样简单的缓存还是不够看。

此时,要再去处理缓存问题,就会花费过多的精力,得不偿失。在这种层面上的缓存处理 MyBatis 框架已经做好了,就叫做一级缓存。

二、MyBatis 的主要层次结构

使用 MyBatis 对数据库操作的代码,能够看见的就是这个 SqlSession 对象。实际上,这只是 MyBatis 对外暴露的接口,整个 MyBatis 核心部件是下面的这么一堆接口和类:

1️⃣SqlSession:MyBatis 工作的主要顶层 API,表示和数据库交互的会话,完成必要数据库增删改查功能。
2️⃣Executor:MyBatis 执行器,整个 MyBatis 调度的核心,负责 QL 语句的生成和查询缓存的维护。
3️⃣StatementHandler:封装了 JDBC Statement 操作,负责对 JDBC statement 的操作,如设置参数、将 Statement 结果集转换成 List 集合。
4️⃣ParameterHandler:负责对用户传递的参数转换成 JDBC Statement 所需要的参数。
5️⃣ResultSetHandler:负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合。
6️⃣TypeHandler:负责 Java 数据类型和 jdbc 数据类型之间的映射和转换。
7️⃣MappedStatement:MappedStatement 维护了一条节点的封装。
8️⃣SqlSource:负责根据用户传递的 parameterObject,动态地生成 SQL 语句,将信息封装到 BoundSql 对象中,并返回。
9️⃣BoundSql:表示动态生成的 SQL 语句以及相应的参数信息。
1️⃣0️⃣Configuration:MyBatis 所有的配置信息都维持在 Configuration 对象之中。

上面这堆接口和类的层次关系如图:

MyBatis 对外暴露的接口是 SqlSession,而最重要的是 Executor 接口。Executor 的实现类 BaseExecutor 中拥有一个 Cache 接口的实现类 PerpetualCache:

PerpetualCache 中则有一个 HashMap 属性:

总结:

MyBatis 封装了 JDBC 操作,对外暴露了 SqlSession 接口进行数据库的操作。但是实际 MyBatis 最核心的接口是 Executor,它负责 SQL 语句的生成和查询缓存的维护。如果没有缓存就查数据库,有缓存就使用的是 PerpetualCache 中的 HashMap 保存的数据缓存。MyBatis 的一级缓存其实就保存在一个 HashMap 中。HashMap 如何判断查询方法是否相同?其实主要是通过 HashMap 的 key 值。

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<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
            Iterator var8 = parameterMappings.iterator();
            while(var8.hasNext()) {
                ParameterMapping parameterMapping = (ParameterMapping)var8.next();
                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;
        }
    }
...

从代码中可以看出,如果下面条件一样,就可以判断为两个查询相同:

1️⃣statementId
2️⃣RowBounds 的 offset、limit 的结果集分页属性
3️⃣SQL 语句
4️⃣传给 JDBC 的参数值

三、MyBatis 的一级缓存

1️⃣一级缓存最简单的组织形式

MyBatis 在一次会话的表示——一个 SqlSession 对象中创建一个本地缓存(local cache),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。

类似于最开始保存的方式,只是从一个简单的对象,换成了封装好了的更加复杂的 Local Cache 对象。

实际上,SqlSession 只是一个 MyBatis 对外的接口,SqlSession 将它的工作交给了 Executor 执行器这个角色来完成,负责完成对数据库的各种操作。当创建了一个 SqlSession 对象时,MyBatis 会为这个 SqlSession 对象创建一个新的 Executor 执行器,而缓存信息就被维护在这个 Executor 执行器中,MyBatis 将缓存和对缓存相关的操作封装在 Cache 接口中。它们之间的组织关系,大概如下图:

2️⃣一级缓存的生命周期

  1. MyBatis 在开启一个数据库会话时,会创建一个新的 SqlSession 对象,SqlSession 对象中会有一个新的 Executor 对象,Executor 对象中持有一个新的 PerpetualCache 对象(Cache 接口的实现类);当会话结束时,SqlSession 对象及其内部的 Executor 对象还有 PerpetualCache 对象也一并释放掉。
  2. 如果 SqlSession 调用了 close(),会释放掉一级缓存 PerpetualCache 对象,一级缓存将不可用。
  3. 如果 SqlSession 调用了 clearCache(),会清空 PerpetualCache 对象中的数据,但是 SqlSession 对象仍可使用。
  4. SqlSession 中执行任何一个增/删/改操作之后执行事务提交 commit() ,都会清空PerpetualCache 对象的数据,但是 SqlSession 对象可以继续使用。

四、MyBatis 的二级缓存

1️⃣二级缓存使用场景

类似于统计排行榜的查询,可能会涉及到多张表很多字段的查询统计排序,是非常费时费力的。如果每次都去数据库查询显示一次排行榜数据,那到排行榜这里,必定会卡顿很久,而且这种卡顿是用户不能忍受的。做成一级缓存也是不可行的,每次 SqlSession 请求,每个客户上来难道都要卡顿一次吗?所以,这种查询肯定要做成全局的缓存,当应用启动的时候就缓存这种查询数据,然后每一周刷新一次这种数据就可以了。

由此,简单总结二级缓存的特点和使用场景:二级缓存作用于全局,对于一些相当消耗性能的,并且对于时效性不敏感的查询可以使用二级缓存。注意,如果开启了二级缓存,查询的顺序是二级缓存 → 一级缓存 → 数据库

2️⃣MyBatis 二级缓存的配置

在 MyBatis 中使用二级缓存就必须要进行配置了,必须要有下面的步骤才能正常使用二级缓存:

  1. 在全局设置中开启二级缓存
<settings>...
  <!-- 开启二级缓存 -->
  <setting name="cacheEnabled" value="true"/>
  ...
</settings>
  1. 在 xxxMapper.xml 中开启 <cache> 标签
<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024">
</cache>

可以简写为:

<cache/>

这样就表示在 xxxMapper.xml 中开启二级缓存了,因为 <cache/> 标签的每个属性都有默认值。cache 标签属性:

eviction:缓存回收策略,这个属性又有下面几个值
LRU – 最近最少使用的。移除最长时间不被使用的对象。
FIFO – 先进先出。按对象进入缓存的顺序来移除它们。
SOFT – 软引用。移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用。更积极地移除基于垃圾收集器状态和弱引用规则的对象。
默认是LRU
flushInterval:刷新间隔,可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
size:引用数目,可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
readOnly:只读属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。

  1. 相关实体类需要序列化

放入二级缓存中保存的 JavaBean 需要实现 Serializable 接口。序列化的意思就是从内存中的数据传到硬盘中。反序列化意思相反。MyBatis 的二级缓存,实际上就是将数据放进了硬盘文件中去了。

如果要使用 MyBatis 的二级缓存,除了要在需要缓存的 mapper.xml 中开启以外,还需要目标实体类实现序列化的接口。当实体类有父类或级联属性,也必须实现序列化。

  1. useCache 和 flushCache

这一步不是必须的。这两个都是属于查询标签 <select> 的属性

userCache 是用来设置是否禁用二级缓存的,在 statement 中设置 useCache=false 可以禁用当前 select 语句的二级缓存,即每次查询都会发出 sql 去查询,默认情况是 true,即该 sql 使用二级缓存。

flushCache 属性,默认情况下为 true,即刷新缓存,如果改成 false 则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。

五、Mybatis 涉及的设计模式

1️⃣Builder模式,例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder
2️⃣工厂模式,例如 SqlSessionFactory、ObjectFactory、MapperProxyFactory
3️⃣单例模式,例如 ErrorContext 和 LogFactory
4️⃣代理模式,Mybatis 实现的核心,比如 MapperProxy、ConnectionLogger,用的 jdk 的动态代理;还有 executor.loader 包使用了 cglib 或者 javassist 达到延迟加载的效果
5️⃣组合模式,例如 SqlNode 和各个子类 ChooseSqlNode 等
6️⃣模板方法模式,例如 BaseExecutor 和 SimpleExecutor,还有 BaseTypeHandler 和所有的子类例如 IntegerTypeHandler
7️⃣适配器模式,例如 Log 的 Mybatis 接口和它对 jdbc、log4j 等各种日志框架的适配实现
8️⃣装饰者模式,例如 Cache 包中的 cache.decorators 子包中等各个装饰者的实现
9️⃣迭代器模式,例如迭代器模式 PropertyTokenizer

六、总结

  1. 进行 select 后,调用SqlSession.close(),会将其一级缓存的数据放进二级缓存中,此时一级缓存随着 SqlSession 的关闭也就不存在了。
  2. 进行 select 后,调用SqlSession.commit(),会将其一级缓存的数据放进二级缓存中,并清空一级缓存。
  3. 对 SqlSession 执行更新(insert、delete、update)后,同时不调用SqlSession.commit/SqlSession.close(),这时只会清空其自身的一级缓存,对二级缓存没有影响。
  4. 对 SqlSession 执行更新(insert、delete、update)后,执行SqlSession.commit(),不仅清空其自身的一级缓存(执行更新操作的结果),也清空二级缓存(执行 commit() 的效果)。
  5. 对 SqlSession 执行更新(insert、delete、update)后,执行SqlSession.close()(没有执行 SqlSession.commit()),需分两类情况。当 autoCommit 为 false 时,只会清空其自身的一级缓存(执行更新操作的效果),对二级缓存没有影响。当 autoCommit 为 true 时,会清空二级缓存。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345