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 调度的核心,负责 SQL 语句的生成和查询缓存的维护。
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 语句以及相应的参数信息。
🔟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 eviction="FIFO" flushInterval="60000" readOnly="false" size="1024">
</cache>

可以简写为:

<cache/>

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

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

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

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

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

  1. useCache 和 flushCache

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

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 时,会清空二级缓存。
  • 7
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JFS_Study

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值