# 体系结构与工作原理

作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)


# 工作流程分析

  1. 解析配置文件

    在MyBatis启动要去解析配置文件, 包括全局配置文件和映射器配置文件, 会将它解析为 Configuration 对象。

  2. 提供操作接口

    解析完配置文件后, 会提供 SqlSession 对象, 它可以代表应用程序和数据库之间的一次连接, MyBatis提供了一个 SqlSessionFactory 工厂, 用来获取一个 SqlSession

  3. 执行SQL操作

    SqlSession 会持有一个 Executor 对象, 该对象用于封装对数据库的操作。Executor 在执行query 或 update等操作时, 会生成一系列对象用来处理结果集, 可以统一为 StatementHandler 即对JDBC Statement 的封装。

# 总架构分层与模块划分

  1. 接口层

    核心对象是 SqlSession, 它是上层应用和 MyBatis 打交道的桥梁, SqlSession 上定义了非常多的对数据库的操作方法。接口层在接收到调用请求的时候,会调用核心处理层的相应模块来完成具体的数据库操作。

  2. 核心处理层

    和数据库操作相关的动作是在这一层完成的。大致步骤如下

    1. 把接口中传入的参数解析并且映射成JDBC 类型:
    2. 解析 xml 文件中的 SQL语句, 包括插入参数, 和动态 SQL 的生成;
    3. 执行 SQL语句;
    4. 处理结果集, 并映射成 Java 对象。
  3. 基础支持层

    基础支持层主要是一些抽取出来的通用的功能为了复用, 用来支持核心处理层的功能。

# MyBatis 缓存详解

# 1. MyBatis 缓存体系结构

MyBatis 跟缓存相关的类都在cache 包里面,其中有一个 Cache 接口, 其有一个默认实现 PerpetualCache, 该缓存由HashMap实现。使用了装饰器模式对Cache 接口进行包装, 从而实现一系列额外功能的缓存。

# 2. 一级缓存(Local Cache)

MyBatis 的一级缓存是作用在 SqlSession 层级的本地缓存,默认开启,不需要任何配置。其主要作用是避免在同一个会话中对相同 SQL 重复访问数据库。

# 开关配置

若希望关闭一级缓存,可以通过设置 LocalCacheScope.STATEMENT 实现。此配置可在 MyBatis 全局配置文件或 Java 配置类中设置:

configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);
1

或者配置

mybatis:
  configuration:
    local-cache-scope: STATEMENT
1
2
3

底层实现位于 BaseExecutor:

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    // issue #482
    clearLocalCache();
  }
1
2
3
4

# 缓存结构与位置

一级缓存由 Executor 管理。DefaultSqlSession 包含 ConfigurationExecutor 两个核心成员,其中:

  • Configuration 是全局配置,多个会话共享;
  • Executor 是每个会话(SqlSession)独立持有;
  • 缓存由 BaseExecutor 抽象类中的 PerpetualCache 实现。

如下为其构造器中的部分代码:

protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }
1
2
3
4
5
6
7
8
9

# 缓存生效条件

当在 同一个 SqlSession 中多次执行相同 SQL(包括参数和分页条件),且未显式清除缓存时,将直接命中缓存,避免重复访问数据库。

注意:不同的 SqlSession 实例不共享一级缓存,每个 SqlSession 都会维护自己的 Executor 和缓存。

对于分库或多数据源配置场景:

# Spring 多数据源或分库配置
jdbc:mysql://localhost:3306/user_db_0
jdbc:mysql://localhost:3306/user_db_1
1
2
3

在一次请求中访问多个库时,MyBatis 会创建多个 SqlSession,每个库维护独立的缓存。

# 一级缓存相关机制分析

  1. 缓存的读取与写入时机

    查询方法中,MyBatis 会优先尝试从本地缓存获取结果,如果获取不到则会访问数据库并将结果写入缓存。

    关键代码如下:

    // BaseExecutor.query() 尝试从一级缓存获取
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    
    if (list == null) {
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    
    // queryFromDatabase 内部实现
    localCache.putObject(key, EXECUTION_PLACEHOLDER); // 占位符防止缓存穿透
    try {
      list = doQuery(...); // 执行 SQL 查询
    } finally {
      localCache.removeObject(key); // 清除占位符
    }
    localCache.putObject(key, list); // 写入一级缓存
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  2. 缓存的清除时机

    • 执行 update 操作时自动清除缓存

      @Override
      public int update(MappedStatement ms, Object parameter) throws SQLException {
        clearLocalCache(); // 更新操作时清除缓存
        return doUpdate(ms, parameter);
      }
      
      1
      2
      3
      4
      5
    • 查询时若配置了 flushCache=true,也会清除缓存

      if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache(); // 主查询时根据配置决定是否清除缓存
      }
      
      1
      2
      3
  3. 缓存作用域为 SqlSession,会话之间不共享

    由于一级缓存是基于 SqlSession 存储的,不同会话之间不共享缓存。如果某个会话中更新了数据,而另一个会话中仍存在缓存数据,则可能导致脏读或过期读。

    这也是一级缓存在实际分布式或高并发场景中存在的局限性之一,通常通过启用 二级缓存 来扩大作用范围并保持一致性。

# 3. 二级缓存

二级缓存的范围是 namespace 级别的, 可以被多个 SqlSession 共享(同一个接口里的相同方法都可以共享), 生命周期和应用同步。

既然二级缓存的作用范围比一级缓存大, 那么肯定是放在BaseExecutor外层, MyBatis通过CachingExecutor装饰器类来维护, 如果启用了二级缓存, 在创建Executor对象后会对Executor进行装饰, 在查询时会先判断二级缓存中是否有缓存结果, 如果没有会委派给真正的查询器Executor实现类来执行, 比如SimpleExecutor, 查询后将结果缓存并返回

# 开关配置

  1. 全局开关

    MyBatis 默认启用二级缓存,通过 cacheEnabled 控制是否启用全局二级缓存功能:

    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    
    1
    2
    3

    若设置为 false,所有 Mapper 的二级缓存功能都将被禁用。

  2. Mapper 层开关(必须显式配置)

    默认情况下,只有在 Mapper.xml 中添加 <cache/> 标签,才会为该 Mapper 创建对应的缓存实例:

    <cache eviction="LRU" flushInterval="60000" size="512" readOnly="false"/>
    
    1
  3. SQL 级别控制

    useCache="false":禁用当前 select 语句的二级缓存功能

    flushCache="true":默认用于 insert/update/delete,表示操作后清空该 namespace 下的二级缓存 可用于 select 强制刷新缓存

    <select id="query" useCache="true" flushCache="false">
        SELECT * FROM user WHERE id = #{id}
    </select>
    
    1
    2
    3

# 缓存结构与位置

缓存作用域

  • 一级缓存(localCache):基于 SqlSession 作用域,每次请求结束后即失效
  • 二级缓存:基于 Mapper(namespace)级别,所有 SqlSession 实例共享缓存数据

缓存位置

  • 一级缓存存储在 BaseExecutor 的成员变量 localCache 中(本地内存)
  • 二级缓存并不直接存在于 Executor 中,而是通过 CachingExecutor 来代理操作 Mapper 对应的 Cache 实例(装饰器)

缓存的持久化结构

  • 默认使用 PerpetualCache 作为存储容器(Map 结构)
  • 可通过组合如 LRUCacheFIFOSoftCacheWeakCache 等实现策略装饰
  • 支持自定义实现 org.apache.ibatis.cache.Cache 接口,集成 Redis、Ehcache 等第三方缓存中间件

一般建议自定义二级缓存, mybatis的二级缓存容量不可控, 并且存储的数据一般都是查询语句的结果, 常用的字典等缓存生产中多是Map<key,value>结构, 并且在多节点难以维护一致性, 需要结合redis等组件使用。