Mybatis入门到精通 二

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

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

一 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(即Namespace
默认状态 开启 关闭
线程安全
序列化 不需要 需要
适用场景 单次会话优化 跨会话共享
  • 一级缓存是SqlSession级别,无法关闭。但是当设置LocalCacheScope为STATEMENT时,查询语句执行结束时都会清空一级缓存,相当于 "禁用了一级缓存的跨查询复用能力";
  • 二级缓存,适合读多写少场景,需谨慎使用;
  • 在分布式系统下,需要禁用Mybatis的一二级缓存,因为它们都是本地缓存,会引起数据不一致问题。应当使用Redis等分布式缓存。
相关推荐
2501_909800812 小时前
Java IO框架
java·学习·io框架
趣知岛3 小时前
初识Java
java·开发语言
步菲5 小时前
springboot canche 无法避免Null key错误, Null key returned for cache operation
java·开发语言·spring boot
毕设源码-朱学姐5 小时前
【开题答辩全过程】以 基于SpringBoot的中医理疗就诊系统为例,包含答辩的问题和答案
java·spring boot·后端
2201_757830879 小时前
全局异常处理器
java
小徐Chao努力10 小时前
【Langchain4j-Java AI开发】09-Agent智能体工作流
java·开发语言·人工智能
Coder_Boy_10 小时前
SpringAI与LangChain4j的智能应用-(理论篇3)
java·人工智能·spring boot·langchain
Coder_Boy_10 小时前
基于SpringAI的智能平台基座开发-(六)
java·数据库·人工智能·spring·langchain·langchain4j
伯明翰java11 小时前
Java数据类型与变量
java·开发语言