Mybatis的Executor和缓存体系

《Mybatis入门到精通 一》中,我们了解了Mybatis启动过程:配置解析、创建SqlSession。本文介绍了SQL执行引擎Executor,以及和Executor密切相关的一二级缓存实现。

注:本文中源码来自mybatis 3.4.x版本,地址https://github.com/mybatis/mybatis-3.git

一 Executor接口

1.1 作用

Executor接口中,是MyBatis的SQL执行引擎,通过精巧的设计模式组合,实现了SQL执行的高效协调和灵活扩展。该接口有以下关键职责:

  1. SQL执行协调
java 复制代码
// 查询操作
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler);
// 更新操作  
int update(MappedStatement ms, Object parameter);
  1. 缓存管理
java 复制代码
// 一级缓存管理
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
boolean isCached(MappedStatement ms, CacheKey key);
void clearLocalCache();
  1. 事务控制
java 复制代码
void commit(boolean required);
void rollback(boolean required);
Transaction getTransaction();
  1. 延迟加载
java 复制代码
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

1.2 接口体系

BaseExecutor是顶级父抽象类,用模版方法实现公共逻辑,如维护一级缓存。

java 复制代码
public abstract class BaseExecutor implements Executor {
    // 模板方法:定义执行流程
    public <E> List<E> query(...) {
        // 1. 检查一级缓存
        list = localCache.getObject(key);
        if (list != null) {
            return list;
        }
        // 2. 从数据库查询
        return queryFromDatabase(...);
    }
    
    // 抽象方法:子类实现具体策略
    protected abstract <E> List<E> doQuery(...);
}

使用策略模式,BaseExecutor有以下实现类

  • SimpleExecutor,每次执行完都关闭Statement
java 复制代码
public int doUpdate(MappedStatement ms, Object parameter) {
    Statement stmt = null;
    try {
        StatementHandler handler = configuration.newStatementHandler(...);
        stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.update(stmt);
    } finally {
        // 每次都关闭Statement
        closeStatement(stmt);
    }
}
  • ReuseExecutor ,缓存Statement对象,实现重用
  • BatchExecutor,批量执行,提升性能

CachingExecutor采用了装饰器模式,在BaseExecutor基础上,提供了二级缓存能力。

java 复制代码
public class CachingExecutor implements Executor {
    // 被装饰的BaseExecutor子类对象
    private final Executor delegate; 
    
    public <E> List<E> query(...) {
        Cache cache = ms.getCache();
        if (cache != null) {
            // 二级缓存逻辑
            List<E> list = tcm.getObject(cache, key);
            // 未命中缓存,查询数据库并放入缓存池
            if (list == null) {
                list = delegate.query(...); // 委托给原执行器
                tcm.putObject(cache, key, list);
            }
            return list;
        }
        //未启用二级缓存,直接查询数据库 
        return delegate.query(...);
    }
}

1.3 创建Executor

Configuration#newExecutor方法会根据executorType来创建相应的实现类对象,并使用插件增强。

默认得executorType = ExecutorType.SIMPLE,即使用SimpleExecutor。

java 复制代码
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    // 二级缓存装饰
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    // 代理增强
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

二 缓存体系

2.1 一级缓存

2.1.1 特点和启用

一级缓存有如下特点:

  • 作用域:SqlSession级别,不同Session不共享
  • 生命周期:SqlSession创建到关闭
  • 存储结构:HashMap(PerpetualCache)
  • 默认状态:始终开启,无法关闭
  • 清理时机:commit、rollback、update操作

**一级缓存默认开启,默认作用域是SESSION。**启用方式如下:

XML 复制代码
<!-- mybatis-config.xml -->
<settings>
    <!-- SESSION: 会话级别(默认) -->
    <!-- STATEMENT: 语句级别(执行完立即清空) -->
    <setting name="localCacheScope" value="SESSION"/>
</settings>

来看一个命中一级缓存的示例

java 复制代码
SqlSession session = sqlSessionFactory.openSession();
try {
    UserMapper mapper = session.getMapper(UserMapper.class);
    
    // 第一次查询,走数据库
    User user1 = mapper.selectById(1);
    
    // 第二次查询,命中一级缓存,不走数据库
    User user2 = mapper.selectById(1);
    
    System.out.println(user1 == user2); // true,同一对象
} finally {
    session.close();
}

2.1.2 作用域

localCacheScope决定了一级缓存的有效范围。取值SESSIONSTATEMENT有如下区别:

SESSION

  1. 一级缓存在整个 SqlSession 会话期间都有效
  2. 只有当SqlSession执行insert/update/delete操作(会自动清空当前 SqlSession 的一级缓存)、调用sqlSession.clearCache()手动清空缓存,或SqlSession关闭(sqlSession.close())时,该会话的一级缓存才会失效或被销毁。

STATEMENT

  1. 一级缓存仅对当前查询语句有效,查询结束后缓存立即失效 ,相当于 "禁用了一级缓存";
  2. 查询执行完成后,缓存会被立即清空 / 失效,第二次查询不会复用第一次的缓存结果,仍然会去数据库重新查询;
  3. 用于解决嵌套查询的重复数据加载问题,如关联查询中的相同数据避免重复查询。

2.1.3 非线程安全

一级缓存实现是PerpetualCache,不是线程安全的,因为底层使用了HashMap,而且putObject方法也不加锁。

那么为什么不使用线程安全的Map呢?基于以下几点原因:

  1. MyBatis的设计原则:SqlSession不应该被多线程共享
  2. 使用ConcurrentHashMap会带来性能开销

因此,每个线程应当使用独立的SqlSession(如使用SqlSessionManager ),或使用Spring等框架提供的线程安全封装(如SqlSessionTemplate )。

2.2 二级缓存

2.2.1 特点和启用

二级缓存有以下特点:

  • 作用域:Mapper(Namespace)级别,跨Session共享
  • 生命周期:应用启动到关闭
  • 存储结构:可配置(LRU、FIFO等)
  • 默认状态:关闭,需手动开启
  • 事务性:commit后才生效,rollback会清空

启用二级缓存,需要满足三个条件:

  • 全局开启(默认启用
XML 复制代码
<!-- mybatis-config.xml -->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

只有当Configuration中cacheEnabled=true时,才会创建CachingExecutor。

  • Mapper局部配置(必须手动配置
XML 复制代码
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 基本配置,激活二级缓存 -->
    <cache/>
    
    <!-- 完整配置 -->
    <cache
        eviction="LRU"           <!-- 淘汰策略:LRU/FIFO/SOFT/WEAK -->
        flushInterval="60000"    <!-- 刷新间隔:60秒 -->
        size="512"               <!-- 缓存对象数量 -->
        readOnly="false"/>       <!-- 是否只读 -->
</mapper>
  • 提供实体类序列化(推荐)
java 复制代码
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long id;
    private String name;
    // ...
}

Mapper 中的 select****查询语句useCache默认为true。

当执行 insert**、** update**、** delete****操作时( flushCache****默认为true),MyBatis 会自动清空当前 Mapper 命名空间下的二级缓存,确保缓存数据与数据库数据一致性。

来看跨Session共享二级缓存的示例

java 复制代码
// Session A
SqlSession session1 = sqlSessionFactory.openSession();
try {
    UserMapper mapper1 = session1.getMapper(UserMapper.class);
    User user1 = mapper1.selectById(1);  // 查询数据库
    session1.commit();  // 提交后才放入二级缓存
} finally {
    session1.close();
}

// Session B
SqlSession session2 = sqlSessionFactory.openSession();
try {
    UserMapper mapper2 = session2.getMapper(UserMapper.class);
    User user2 = mapper2.selectById(1);  // 命中二级缓存
} finally {
    session2.close();
}

2.2.2 其他特性

此外,二级缓存支持语句级控制、自定义缓存实现、启用缓存日志

XML 复制代码
<mapper namespace="com.example.mapper.UserMapper">
    <cache/>
    
    <!-- select默认使用缓存,可不配置useCache -->
    <select id="selectById" useCache="true">
        SELECT * FROM user WHERE id = #{id}
    </select>
    
    <!-- 不使用缓存 -->
    <select id="selectRealtime" useCache="false">
        SELECT * FROM user WHERE id = #{id}
    </select>
    
    <!-- 更新操作清空缓存(默认) -->
    <update id="updateUser" flushCache="true">
        UPDATE user SET name = #{name} WHERE id = #{id}
    </update>
</mapper>
XML 复制代码
<!-- 使用Redis作为二级缓存 -->
<cache type="org.mybatis.caches.redis.RedisCache">
    <property name="host" value="localhost"/>
    <property name="port" value="6379"/>
</cache>
XML 复制代码
<!-- 开启缓存日志 -->
<cache type="org.apache.ibatis.cache.decorators.LoggingCache">
    <property name="logImpl" value="STDOUT_LOGGING"/>
</cache>

2.3 总结

|------|------------|--------|
| 特性 | 一级缓存 | 二级缓存 |
| 作用域 | SqlSession | Mapper |
| 默认状态 | 开启 | 关闭 |
| 线程安全 | 否 | 是 |
| 序列化 | 不需要 | 需要 |
| 适用场景 | 单次会话优化 | 跨会话共享 |

  • 一级缓存是SqlSession级别,无法关闭。但是当设置LocalCacheScope为STATEMENT时,查询语句执行结束时都会清空一级缓存,相当于 "禁用了一级缓存的跨查询复用能力";
  • 二级缓存,适合读多写少场景,需谨慎使用;
  • 在分布式系统下,需要禁用Mybatis的一二级缓存,因为它们都是本地缓存,会引起数据不一致问题。应当使用Redis等分布式缓存。
相关推荐
涡能增压发动积1 天前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
云烟成雨TD1 天前
Spring AI Alibaba 1.x 系列【6】ReactAgent 同步执行 & 流式执行
java·人工智能·spring
Wenweno0o1 天前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
行乾1 天前
鸿蒙端 IMSDK 架构探索
架构·harmonyos
于慨1 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz1 天前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg3213211 天前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
tyung1 天前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald1 天前
SpringBoot - 自动配置原理
java·spring boot·后端
殷紫川1 天前
深入理解 AQS:从架构到实现,解锁 Java 并发编程的核心密钥
java