mybatis

meituan

聊聊MyBatis缓存机制

Mybatis 架构设计与实例分析

MyBatis 的框架设计

  1. 接口层,和数据库交互的方式

    • 使用传统的MyBatis 提供的API(SqlSession)
    • 使用 Mapper 接口
      根据 MyBatis 的配置规范配置好后,通过 SqlSession.getMapper(XXXMapper.class) 方法,MyBatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper 实例,我们使用 Mapper 接口的某一个方法时,MyBatis 会根据这个方法的方法名和参数类型,确定 Statement Id,底层还是通过 SqlSession.select(“statementId”,parameterObject);或者 SqlSession.update(“statementId”,parameterObject); 等等来实现对数据库的操作。
  2. 数据处理层可以说是MyBatis 的核心,从大的方面上讲,它要完成三个功能:

    • 通过传入参数构建动态SQL语句;
    • SQL语句的执行;
    • 封装查询结果集成List
  3. 框架支撑层
    事务管理机制, 连接池管理机制,缓存机制, SQL语句的配置方式

  4. 引导层

MyBatis 的主要构件及其相互关系

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

Mybatis 初始化机制详解

初始化做了什么

可以这么说,MyBatis 初始化的过程,就是创建 Configuration 对象的过程。而 Configuration 对象包含 MyBatis 初始化所需的全部信息,大致包含:settings 设置,typeAliases 类型命名,typeHandlers 类型处理器,objectFactory 对象工厂,plugins 插件,environments 环境(environment 环境变量,transactionManager 事务管理器,dataSource 数据源 )等。

XML 文件创建 Configuration 对象的过程

  1. 调用 SqlSessionFactoryBuilder 对象的 build(inputStream) 方法;
  2. SqlSessionFactoryBuilder 会根据输入流 inputStream 等信息创建 XMLConfigBuilder 对象;
  3. SqlSessionFactoryBuilder 调用 XMLConfigBuilder 对象的 parse() 方法;
  4. XMLConfigBuilder 对象返回 Configuration 对象;
  5. SqlSessionFactoryBuilder 根据 Configuration 对象创建一个 DefaultSessionFactory 对象;
  6. SqlSessionFactoryBuilder 返回 DefaultSessionFactory 对象给 Client,供 Client 使用。

Mybatis 数据源与连接池

MyBatis 数据源 DataSource 分类

UnpooledDataSource,PooledDataSource,JNDI。PooledDataSource 内部持有一个 UnpooledDataSource 的引用,当 PooledDataSource 需要创建 java.sql.Connection 对象时,还是通过 UnpooledDataSource 来创建,PooledDataSource 只是提供一种缓存连接池机制。

为什么要使用连接池?

创建一个 Connection 对象,要用 250 毫秒左右,对频繁地跟数据库交互的应用程序浪费性能。

Mybatis 事务管理机制

MyBatis 的事务管理分为两种形式:

  1. 使用 JDBC 的事务管理机制(实现为 JdbcTransaction):即利用 java.sql.Connection 对象完成对事务的提交(commit())、回滚(rollback())、关闭(close())等
  2. 使用 MANAGED 的事务管理机制(实现为 ManagedTransaction):这种机制 MyBatis 自身不会去实现事务管理,而是让程序的容器如(JBOSS,Weblogic)来实现对事务的管理

Mybatis 缓存机制的设计与实现

MyBatis 将数据缓存设计成两级结构,分为一级缓存、二级缓存:

  • 一级缓存是 Session 会话级别的缓存,位于表示一次数据库会话的 SqlSession 对象之中,又被称之为本地缓存。一级缓存是 MyBatis 内部实现的一个特性,用户不能配置,默认情况下自动支持的缓存,用户没有定制它的权利(不过这也不是绝对的,可以通过开发插件对它进行修改);
  • 二级缓存是 Application 应用级别的缓存,它的是生命周期很长,跟 Application 的声明周期一样,也就是说它的作用范围是整个 Application 应用。

mybatis 缓存整体结构图

Mybatis 一级缓存实现详解 及使用注意事项

什么是一级缓存? 为什么使用一级缓存?

在对数据库的一次会话中(或者说使用同一个 SqlSession 对象查询时?),我们有可能会反复地执行完全相同的查询语句,如果不采取一些措施的话,每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。
为了解决这一问题,减少资源的浪费,MyBatis 会在表示会话的 SqlSession 对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

SqlSession 中的缓存是怎样组织的?

当创建了一个 SqlSession 对象时,MyBatis 会为这个 SqlSession 对象创建一个新的 Executor 执行器,而缓存信息就被维护在这个Executor执行器中,MyBatis 将缓存和对缓存相关的操作封装成了 Cache 接口中。
Executor 接口的实现类 BaseExecutor 中拥有一个 Cache 接口的实现类 PerpetualCache,则对于 BaseExecutor 对象而言,它将使用 PerpetualCache 对象维护缓存。
PerpetualCache 实现原理其实很简单,其内部就是通过一个简单的 HashMap<k,v> 来实现的,没有其他的任何限制。

一级缓存的生命周期有多长?

  1. MyBatis 在开启一个数据库会话时,会 创建一个新的 SqlSession 对象,SqlSession 对象中会有一个新的 Executor 对象,Executor 对象中持有一个新的 PerpetualCache 对象;当会话结束时,SqlSession 对象及其内部的 Executor 对象还有 PerpetualCache 对象也一并释放掉。
  2. 如果 SqlSession 调用了 close() 方法,会释放掉一级缓存 PerpetualCache 对象,一级缓存将不可用;
  3. 如果 SqlSession调用了 clearCache(),会清空 PerpetualCache 对象中的数据,但是该对象仍可使用;
  4. SqlSession 中执行了任何一个 update 操作(update()、delete()、insert()) ,都会清空 PerpetualCache 对象的数据,但是该对象可以继续使用;

Cache 接口的设计以及 CacheKey 的定义(非常重要)

Cache 最核心的实现其实就是一个 Map,将本次查询使用的特征值作为 key,将查询结果作为 value 存储到Map 中。
MyBatis 认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:

  1. 传入的 statementId
  2. 查询时要求的结果集中的结果范围 (结果的范围通过 rowBounds.offset 和 rowBounds.limit 表示);
  3. 这次查询所产生的最终要传递给 JDBC java.sql.Preparedstatement 的 Sql 语句字符串(boundSql.getSql() )
  4. 传递给 java.sql.Statement 要设置的参数值

综上所述, CacheKey 由以下条件决定:
statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值

对于每次的查询请求,Executor 都会根据传递的参数信息以及动态生成的SQL语句,将上面的条件根据一定的计算规则,创建一个对应的 CacheKey 对象。
我们知道创建 CacheKey 的目的,就两个:
1. 根据 CacheKey 作为 key,去 Cache 缓存中查找缓存结果;
2. 如果查找缓存命中失败,则通过此 CacheKey 作为 key,将从数据库查询到的结果作为 value,组成key,value 对存储到 Cache 缓存中。
CacheKey 的构建被放置到了 Executor 接口的实现类 BaseExecutor 中,

根据一级缓存的特性,在使用的过程中,应该注意:

  1. 对于数据变化频率很大,并且需要高时效准确性的数据要求,我们使用 SqlSession 查询的时候,要控制好 SqlSession 的生存时间,SqlSession 的生存时间越长,它其中缓存的数据有可能就越旧,从而造成和真实数据库的误差;同时对于这种情况,用户也可以手动地适时清空 SqlSession 中的缓存;
  2. 对于只执行、并且频繁执行大范围的 select 操作的 SqlSession 对象,SqlSession对象的生存时间不应过长。

Mybatis 二级缓存的设计原理

MyBatis 二级缓存的工作模式

MyBatis 的二级缓存是 Application 级别的缓存,如果用户配置了 “cacheEnabled=true”,那么MyBatis在为 SqlSession 对象创建 Executor 对象时,会对 Executor对象加上一个装饰者:CachingExecutor,这时 SqlSession 使用 CachingExecutor 对象来完成操作请求。CachingExecutor 对于查询请求,会先判断该查询请求在 Application 级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的 Executor 对象来完成查询操作,之后 CachingExecutor 会将真正 Executor 返回的查询结果放置到缓存中,然后在返回给用户。

MyBatis二级缓存的划分

MyBatis 并不是简单地对整个 Application 就只有一个 Cache 缓存对象,它将缓存划分的更细,即是 Mapper 级别的,即每一个 Mapper 都可以拥有一个 Cache 对象,具体如下:

  1. 为每一个 Mapper 分配一个 Cache 缓存对象(使用 cache 节点配置);
  2. 多个 Mapper 共用一个 Cache 缓存对象(使用 cache-ref 节点配置);

使用二级缓存,必须要具备的条件

MyBatis 对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。
虽然在 Mapper 中配置了 cache,并且为此 Mapper 分配了 Cache 对象,这并不表示我们使用 Mapper 中定义的查询语句查到的结果都会放置到Cache对象之中,我们必须指定 Mapper 中的某条选择语句是否支持缓存,即如下所示,在 select 节点中配置 useCache=”true”,Mapper 才会对此 Select 的查询支持缓存特性,否则,不会对此 Select 查询,不会经过 Cache 缓存。

一级缓存和二级缓存的使用顺序

请注意,如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库

二级缓存实现的选择

使用 MyBatis 的二级缓存有三个选择:

  1. MyBatis 自身提供的缓存实现;
  2. 用户自定义的 Cache 接口实现;
  3. 跟第三方内存缓存库的集成;

MyBatis自身提供的二级缓存的实现

MyBatis主要提供了以下几个刷新和置换策略:

  • LRU:(Least Recently Used),最近最少使用算法,即如果缓存中容量已经满了,会将缓存中最近做少被使用的缓存记录清除掉,然后添加新的记录;
  • FIFO:(First in first out),先进先出算法,如果缓存中的容量已经满了,那么会将最先进入缓存中的数据清除掉;
  • Scheduled:指定时间间隔清空算法,该算法会以指定的某一个时间间隔将Cache缓存中的数据清空;

双数据源关键代码

  1. mapper.java,mapper.xml 按库分,存在两个文件夹下
  2. @MapperScan(basePackages = “org.sang.mybatis.mapper1”,sqlSessionFactoryRef = “sqlSessionFactory1”,sqlSessionTemplateRef = “sqlSessionTemplate1”)