MyBatis的缓存机制
查询缓存
MyBatis 支持查询缓存,将查询结果临时存储在内存中,避免重复执行相同 SQL 导致的数据库往返开销,用于减轻数据压力,提高数据库性能。
MyBatis 的查询缓存分为一级缓存和二级缓存,两者的大致关系如下
日常开发中,80% 的数据库操作是查询,其中大量查询是重复且无状态变化的。这时候缓存就能:
- 减少数据库 IO 次数,降低数据库压力;
- 提升查询响应速度
而且 MyBatis 的缓存和 Hibernate 的不一样,它的代码层缓存逻辑更简单,因为 MyBatis 内置缓存,无需手动写 Map 缓存,很容易加上 Spring Cache 和 Redis 形成一个二级缓存
SqlSession
SqlSession 是 MyBatis 中最核心的接口之一,很明显,MyBatis 通过这个接口与数据库建立连接的 Session 会话
所有对数据库的操作都必须通过 SqlSession 来执行,底层封装了 JDBC 的 Connection,同时整合了缓存、事务、Mapper 映射等核心能力。
MyBatis 中,SqlSession
替你封装了这些底层操作,你只需通过 SqlSession 获取 Mapper
接口,或直接调用 API 执行 SQL,无需关心底层连接的创建和资源释放。
尽是些这种方法
而 SqlSession 是轻量级对象,生命周期应尽可能短,一般来说,一个方法对应一个 SqlSession,绝对不能做成单例,否则会导致连接泄漏、缓存脏数据、线程安全问题。
而对于 SqlSession 原生 MyBatis 可以通过 XML 配置文件生命创建,但是
SpringBoot 整合后,SpringBoot 会自动管理 SqlSession 的生命周期,DI
会在容器启动时创建 SqlSessionFactory,每次 Mapper
方法调用时,Spring 会自动创建 和获取
SqlSession,绑定到当前线程,方法执行完成后,Spring
自动提交或回滚事务,并关闭 SqlSession。
所以说,我们使用框架开发,IoC 会让我们无需接触 SqlSession
SqlSession 是非线程安全的!
- 原因:SqlSession 内部持有 Connection、一级缓存等状态变量,多线程并发访问会导致数据错乱、连接泄漏;
- 正确做法:每个线程独立创建 / 使用 SqlSession,用完即关(SpringBoot 已自动处理);
为什么注入 Mapper 接口就能执行 SQL?这其实涉及到 MyBatis 的动态代理:
- 调用
sqlSession.getMapper(UserMapper.class)时,MyBatis 会为 UserMapper 生成一个动态代理对象; - 调用代理对象的方法时,会解析方法对应的 SQL ID(namespace + 方法名);
- 底层通过 SqlSession 执行对应的 SQL,并处理参数绑定、结果映射。
那么,Mapper 接口是 SqlSession 执行 SQL 的语法糖,这么说,也没什么问题
一级缓存
一级缓存是 SqlSession 级别的缓存。在操作数据库时需要构造 SqlSession 对象,在对象中有一个内存区域,用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的
一级缓存的作用域是同一个 SqlSession,在同一个 SqlSession 中两次执行相同的 Sql 语句,第一次执行完毕会将数据库中查询的数据写到内存缓存中,第二次会从缓存中获取数据将不再从数据库查询,而是从缓存拿,从而提高查询效率。
当一个 SqlSession 结束后该 SqlSession 中的一级缓存也就不存在了。Mybatis默认开启一级缓存。
一级缓存只是相对于同一个 SqlSession 而言。所以在参数和 SQL 完全一样的情况下,我们使用同一个 SqlSession 对象调用一个 Mapper 方法,往往只执行一次 SQL。因为使用 SqlSession 完成了第一次查询后,MyBatis 会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession 都会取出当前缓存的数据,而不会再次发送 SQL 到数据库。
那么,一级缓存的生命周期有多长?
MyBatis 在开启一个数据库会话时,会创建一个新的 SqlSession 对象,SqlSession 对象中会有一个新的 Executor 对象。Executor 对象中持有一个新的 PerpetualCache 对象;当会话结束时,SqlSession 对象及其内部的 Executor 对象还有 PerpetualCache 对象也一并释放掉。
如果 SqlSession 调用了 close() 方法关闭了这个数据库
Session,MyBatis 会释放掉一级缓存 PerpetualCache
对象,一级缓存将不可用。但是如果 SqlSession
调用了clearCache(),会清空 PerpetualCache
对象中的数据,但是该对象仍可使用。
而 SqlSession
中执行了任何一个update操作update()、delete()、insert())等,都会清空
PerpetualCache 对象的数据,但是该对象可以继续使用。
怎么判断某两次查询是完全相同的查询?
MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询。
- 传入的
StatementId - 这次查询所产生的最终要传递给 JDBC 的 Sql 语句字符串
- 传递给
java.sql.Statement要设置的参数值 - 查询时要求的结果集中的结果范围
而一级缓存返回的是 同一个对象的引用,因此在同一个 SqlSession 内,你操作的都是同一个 Java 对象。所以说,一级缓存无法在多个应用服务器之间共享,也就是分布式环境下是用不了的,因为它绑定在单个请求的 SqlSession 上。
二级缓存
MyBatis 的二级缓存是 Mapper 级别的缓存,它也可以提高对数据库查询的效率,以提高应用的性能。
MyBatis的缓存机制整体设计以及二级缓存的工作模式如下
至于为何是 Mapper 级别的缓存,是因为,多个 SqlSession 去操作同一个 Mapper 的 sql 语句的时候,多个 SqlSession 去操作数据库得到数据会存在二级缓存区域,多个 SqlSession 在 Mapper 级别下可以共用二级缓存,所以说二级缓存是跨 SqlSession 的。
二级缓存是多个 SqlSession 共享的,其作用域是 Mapper 的同一个 namespace,不同的 SqlSession 两次执行相同 namespace 下的 sql 语句且向 sql 中传递参数也相同的,即最终执行相同的 sql 语句下,第一次执行完毕会将数据库中查询的数据写到二级缓存中,第二次会从缓存中获取数据,不再从数据库查询,从而提高查询效率。
1 | SqlSession1 执行 UserMapper.selectUserById(1) → |
只有 SqlSession 提交 或 关闭后,一级缓存数据才会刷入二级缓存
Mybatis 默认没有开启二级缓存,需要在 setting 全局参数中配置开启二级缓存。二级缓存的开启需要进行配置,实现二级缓存的时候,MyBatis 要求返回的 POJO 必须是可序列化的。 也就是要求实现 Serializable 接口
配置方法很简单,只需要在映射XML文件配置就可以开启缓存了<cache/>:
1 | <!-- 原生MyBatis配置文件 mybatis-config.xml --> |
1 | # SpringBoot 配置 application.yml |
1 | <!-- 单个 Mapper 开启二级缓存 --> |
如果我们配置了二级缓存就意味着:
- 所有 SELECT 语句自动缓存,是
<cache/>配置后,当前Mapper下所有select默认useCache="true";若某条 select 不想缓存,可配置:<select useCache="false"> - 增删改刷新缓存:MyBatis 中增删改默认
flushCache="true",执行后会清空当前 Mapper 的二级缓存,避免脏数据 - 缓存会使用默认的 LRU 算法来收回:缓存容量默认 1024 个对象引用
- 缓存会存储列表集合或对象的1024个引用:可自定义:
<cache size="2048"> - 缓存会被视为是
read/write(可读/可写)的缓存:缓存返回的是对象的拷贝,而非引用,调用者修改对象不会影响缓存中的数据;若配置readOnly="true",返回只读副本(性能更高,但不能修改)
但二级缓存并非万能
不要在多表关联查询中使用二级缓存
1
2
3
4
5
6<!-- OrderMapper.xml 中关联查询用户和订单 -->
<select id="selectOrderWithUser" resultMap="OrderUserMap">
SELECT o.id, o.order_no, u.name
FROM order o LEFT JOIN user u ON o.user_id = u.id
WHERE o.id = #{id}
</select>此时缓存的是 订单 + 用户 的结果,但如果
UserMapper执行了update操作,而OrderMapper的二级缓存不会感知,会返回脏数据,因为二级缓存按 namespace 隔离。SpringBoot 中 SqlSession 由 Spring 管理,默认情况是 每个 Mapper 方法调用对应一个 SqlSession,因此:
- 同个 Service 方法内多次调用同一 Mapper 查询,先走一级缓存,方法结束后才刷入二级缓存;
- 跨 Service 方法调用,才会真正命中二级缓存。
MyBatis 的缓存工作原理
一级缓存工作原理
下图是根据 id 查询用户的一级缓存图解
MyBatis 一级缓存,二级缓存都是本地缓存,是其最基础、默认开启且无需额外配置的缓存机制
它的基础属性我们上面已经介绍了
- 作用域:SqlSession 级别
- 存储介质:内存,基于 HashMap 实现,键值对结构
- 默认状态:强制开启,无法通过配置关闭
- 命中条件:只有当 Key 完全一致时,一级缓存才会命中。
- 核心价值:避免同一 SqlSession 内重复执行相同 SQL,减少数据库 IO 开销
MyBatis 一级缓存的实现逻辑集中在
org.apache.ibatis.session.defaults.DefaultSqlSession 和
org.apache.ibatis.cache.impl.PerpetualCache
两个核心类中
每个 SqlSession 内部持有一个
Executor,这个Executor并非线程池的那个接口,这个的意思是执行器,而
Executor 中又包含一个 PerpetualCache
实例,它是一级缓存的具体实现
MyBatis 的
Executor(执行器)有两个核心实现:SimpleExecutor(简单执行器)、ReuseExecutor(复用执行器)
PerpetualCache可以看到是基于 HashMap
实现的键值对结构
从 DefaultSqlSession 的 selectList
源码看缓存触发逻辑
从 DefaultSqlSession 源码看缓存清空的触发点
增删改操作触发清空
然后,Executor
中的一级缓存会被PerpetualCache.clear()清空,避免增删改后缓存与数据库数据不一致。
二级缓存的工作原理
二级缓存的工作图如下
二级缓存虽然是本地缓存,但是缓存的 本地 是指单个应用实例(JVM 进程),若部署多实例也就是进行分布式,二级缓存无法跨进程共享;
而且二级缓存可通过扩展 MyBatis 缓存接口,接入 Redis 去做分布式缓存,此时二级缓存不再是本地缓存,但是这是扩展能力
一般情况下我们禁用 MyBatis 二级缓存
核心业务缓存用 Spring Cache + Redis,去对值得缓存的数据做业务层结果缓存
而 EhCache 是一个纯Java的进程内缓存框架,是一种广泛使用的开源 Java 分布式缓存,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。可以让 MyBatis 去整合 EhCache 来实现类似本地缓存的分布式缓存
MyBatis 的事务管理
MyBatis 进行的事务管理机制
MyBatis 的事务管理是保证数据库操作原子性、一致性的核心机制,它既封装了 JDBC 事务的底层逻辑,又提供了与 Spring 整合的灵活方式。
MyBatis 事务完全遵循数据库事务的 ACID 原则,这是理解事务管理的基础:
- 原子性(Atomicity):一个事务内的所有操作要么全执行,要么全回滚;
- 一致性(Consistency):事务执行前后,数据库数据保持合法状态(如转账后总金额不变);
- 隔离性(Isolation):多个事务并发执行时,互不干扰(MyBatis 复用数据库的隔离级别);
- 持久性(Durability):事务提交后,数据永久保存到数据库,不会因宕机丢失。
而 MyBatis 定义了 Transaction 和
TransactionFactory 两个核心接口,封装不同的事务实现:
Transaction接口封装事务的提交、回滚、关闭等操作,比较关键的实现类有JdbcTransaction和ManagedTransaction
TransactionFactory,生成 Transaction 实例的工厂类,同样,比较关键的实现类有JdbcTransactionFactory和ManagedTransactionFactory
只不过,这两者的区别还是比较明显的,至少在使用场景上
| 实现类 | 事务控制方式 | 适用场景 |
|---|---|---|
JdbcTransaction |
基于 JDBC 的 Connection
控制(setAutoCommit/commit/rollback) |
原生 MyBatis 开发、Spring 整合(默认) |
ManagedTransaction |
不主动控制事务,交由容器(如 Spring)管理(仅做连接关闭) | 完全依赖 Spring 事务管理 |
使用 JDBC
原生事务管理:JdbcTransaction其实是使用
JDBC
原生的事务管理方式,通过java.sql.Connection对象来控制事务。在这种方式下,MyBatis
会从数据源获取一个Connection对象,然后由开发者手动调用Connection的commit()方法来提交事务,调用rollback()方法来回滚事务。具体源码逻辑如下
1 | public class JdbcTransaction implements Transaction { |
使用 Spring
框架的事务管理:ManagedTransaction
含义为托管事务,即其内部不会对事物进行管理,而是将事务控制托管给其它框架。例如,在Mybatis整合Spring框架的项目中,通常会借助
Spring 的事务管理机制来管理事务。
1 | public class ManagedTransaction implements Transaction { |
可以看到,在ManagedTransaction中它既不会提交也不会回滚一个连接,而是将事务的整个生命周期管理工作交由容器处理,像Spring或
JEE 应用服务器的上下文环境就可以充当这样的容器。
而当使用 springBoot
项目中当引入mybatis依赖后其实我们无需手动配置ManagedTransaction。这是因为在springboot-start中会根据项目中配置的数据源自动创建合适的事务管理器并注册到
Spring 容器中,进而直接使用 @Transactional
注解来实现事务控制。
事实上,无论是 SqlSession,还是
Executor,它们的事务方法,最终都指向了
Transaction 的事务方法,即都是由
Transaction来完成事务提交、回滚的。
在
MyBatis里,虽然所有的sql执行都委托于Executor,但这不意味着Executor中的执行insert ()、update ()
等方法时,其会显示控制事务。换言之,在Executor中执行数据库操纵方法时,其并不会出现所谓的commit,rollback等操作。具体的事务内容需要你自己实现
关闭自动提交,但未执行
Commit会发生什么我们将
SqlSession中的autoCommit属性设定为false,即关闭SqlSession的自动提交我们在代码中并未手动
commit而仅执行close关闭当前会话,那如果不执行提交操作,仅调用会话的关闭方法,事务内部究竟会发生什么?
MyBatis在架构设计时已充分考虑到此类情况。当执行close方法时,MyBatis会进行一系列逻辑判断,并根据判断结果决定是否执行rollback操作。
在 MyBatis 进行事务管理
我们可以通过如下三种方式来管理事务:
- 编程式管理事务:在代码中显式开启、提交或回滚事务。
- 声明式管理事务:通过 AOP 代理实现事务管理,可以让代码更简洁,更容易维护。
- 注解式管理事务:通过注解方式管理事务,是声明式管理事务的一种扩展方式。
以上三种方式,最常用的是声明式管理事务,在 MyBatis 中,声明式事务管理需要借助 Spring 框架来实现,而且 MyBatis 无专属注解式事务。对于编程式事务,几乎不使用,除非你要自定义逻辑,但是通常也会自己封装成 AOP 的形式来避免代码冗余。
注意,Spring Data JPA 是基于 Hibernate 实现的,Spring 声明式事务是 Spring 框架本身提供的事务管理能力,也就是那个
@Transactional注解、TransactionManager等内容,它是一个 通用的事务管理工具,可以适配各种持久层框架。
MyBatis 结合 Spring 实现声明式事务的核心逻辑是:
- Spring
提供事务管理的核心能力,
DataSourceTransactionManager - MyBatis 提供
SqlSessionFactory、Mapper等组件,与 Spring 的事务管理器整合; - 你只需要在业务方法上加
@Transactional,Spring 就会接管 MyBatis 的数据库连接,实现事务的提交 / 回滚。
例如
1 | // 1. MyBatis 的 Mapper 接口,这里图意省事没用 XML 配置文件,用的注解 |
手动控制事务进行编程式事务的方式大致如下
1 |
|




