MyBatis 缓存机制详解
一级缓存、二级缓存、缓存配置、分布式缓存
概述
MyBatis 提供两级缓存机制,用于减少数据库访问次数,提升性能。
┌─────────────────────────────────────────────────┐
│ SqlSession │
│ ┌───────────────────────────────────────────┐ │
│ │ 一级缓存(会话级) │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ 二级缓存(Mapper 级) │ │ │
│ │ │ ┌─────────────┬─────────────┐ │ │ │
│ │ │ │ UserMapper │ OrderMapper │ │ │ │
│ │ │ │ (PerpetualCache) │ │ │ │
│ │ │ └─────────────┴─────────────┘ │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘一级缓存
作用域
SqlSession 级别,同一个 SqlSession 的查询会被缓存。
工作原理
查询请求
↓
检查 SqlSession 缓存(Map)
↓
有 → 返回缓存结果
无 → 查询数据库 → 写入缓存 → 返回结果示例
java
try (SqlSession session = sqlSessionFactory.openSession()) {
// 第一次查询,走数据库
User user1 = session.selectOne("mapper.UserMapper.selectById", 1L);
// 第二次查询,走一级缓存
User user2 = session.selectOne("mapper.UserMapper.selectById", 1L);
// true:同一个对象(同一个 SqlSession)
System.out.println(user1 == user2);
}缓存失效情况
java
try (SqlSession session = sqlSessionFactory.openSession()) {
// 查询
User user1 = session.selectOne("mapper.UserMapper.selectById", 1L);
// 执行 DML 操作(增删改),缓存被清空
session.update("mapper.UserMapper.update", user);
session.commit(); // 提交事务,缓存失效
// 再次查询,走数据库
User user2 = session.selectOne("mapper.UserMapper.selectById", 1L);
System.out.println(user1 == user2); // false
}| 操作 | 一级缓存状态 |
|---|---|
commit() | 清空缓存 |
close() | 清空缓存 |
| DML 操作(增删改) | 清空缓存 |
clearCache() | 清空缓存 |
禁用一级缓存
java
// 每次查询都强制从数据库读取
try (SqlSession session = sqlSessionFactory.openSession()) {
// 方式一:flushCache=true
User user = session.selectOne("mapper.UserMapper.selectById",
1L, RowBounds.DEFAULT, new CacheKey(), true);
// 方式二:XML 配置禁用
}二级缓存
作用域
Mapper 级别,跨 SqlSession 共享,只要 Mapper 相同即可共享缓存。
配置步骤
1. 开启二级缓存(全局)
xml
<!-- mybatis-config.xml -->
<settings>
<!-- 开启二级缓存(默认 true,可不配) -->
<setting name="cacheEnabled" value="true"/>
</settings>2. Mapper 中启用缓存
xml
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启当前 Mapper 的二级缓存 -->
<cache/>
</mapper>3. 实体类实现 Serializable
java
public class User implements Serializable {
private Long id;
private String name;
// getters, setters
}缓存属性配置
xml
<cache eviction="LRU" <!-- 淘汰策略:FIFO/LRU/SOFT/WEAK -->
flushInterval="60000" <!-- 刷新间隔:60秒 -->
size="512" <!-- 缓存条目数 -->
readOnly="false"> <!-- 只读 -->
</cache>| 属性 | 可选值 | 说明 |
|---|---|---|
| eviction | FIFO, LRU, SOFT, WEAK | 缓存淘汰算法 |
| flushInterval | 毫秒 | 定时刷新缓存 |
| size | 正整数 | 缓存最大数量 |
| readOnly | true, false | true=返回同一对象,false=返回拷贝 |
淘汰策略
| 策略 | 说明 |
|---|---|
| LRU(默认) | 最近最少使用 |
| FIFO | 先进先出 |
| SOFT | 软引用,基于 GC |
| WEAK | 弱引用,基于 GC |
二级缓存工作流程
SqlSession 1 查询
↓
一级缓存未命中
↓
检查二级缓存
↓
有 → 返回缓存结果
无 → 查询数据库 → 写入一级缓存 + 二级缓存
↓
SqlSession 1 关闭
↓
一级缓存数据写入二级缓存
↓
SqlSession 2 查询
↓
一级缓存未命中
↓
二级缓存命中 → 返回结果示例
java
// SqlSession 1
try (SqlSession session1 = sqlSessionFactory.openSession()) {
User user = session1.selectOne("mapper.UserMapper.selectById", 1L);
// 查询数据库,存入一级/二级缓存
System.out.println(user); // 查 DB
}
// session1 关闭,一级缓存写入二级缓存
// SqlSession 2(不同会话)
try (SqlSession session2 = sqlSessionFactory.openSession()) {
User user = session2.selectOne("mapper.UserMapper.selectById", 1L);
// 二级缓存命中,不查数据库
System.out.println(user); // 命中缓存
}readOnly 模式
xml
<!-- readOnly=true:返回同一对象引用,效率高但线程不安全 -->
<cache readOnly="true"/>
<!-- readOnly=false(默认):返回对象拷贝,安全但效率低 -->
<cache readOnly="false"/>java
// readOnly=true
User user1 = session1.selectOne("selectById", 1L);
User user2 = session2.selectOne("selectById", 1L);
System.out.println(user1 == user2); // true
// readOnly=false
System.out.println(user1 == user2); // false缓存查询设置
useCache
xml
<!-- 禁用当前查询的二级缓存 -->
<select id="selectAll" resultMap="BaseResultMap" useCache="false">
SELECT * FROM users ORDER BY id
</select>flushCache
xml
<!-- 执行后清空缓存 -->
<select id="selectById" resultMap="BaseResultMap" flushCache="true">
SELECT * FROM users WHERE id = #{id}
</select>
<!-- DML 操作默认 flushCache="true" -->
<insert id="insert" flushCache="true">
INSERT INTO users (name) VALUES (#{name})
</insert>缓存局限性
1. 脏数据问题
java
// 线程 A 查询数据(缓存)
User user = sessionA.selectOne("selectById", 1L);
// 线程 B 直接修改数据库(未通过 MyBatis)
// UPDATE user SET name = 'New Name' WHERE id = 1;
// 线程 A 再次查询,读到的是缓存中的旧数据
User user2 = sessionA.selectOne("selectById", 1L);
// user2.getName() 仍是旧值!解决方案
| 方案 | 说明 |
|---|---|
设置 flushInterval | 定时过期 |
| Mapper 粒度缓存 | 按业务拆分 |
| 使用分布式缓存 | Redis 替代 |
| 事务内读写一致性 | 确保同事务内读写同 session |
分布式缓存(Redis)
使用场景
单实例 MyBatis 缓存只对本 JVM 有效,集群环境下需分布式缓存。
集成 Redis
xml
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>2.0.0</version>
</dependency>properties
# redis.properties
redis.host=localhost
redis.port=6379
redis.database=0xml
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="LRU"
flushInterval="1800000"
size="1024"/>缓存最佳实践
推荐配置
xml
<!-- 通用缓存配置 -->
<cache eviction="LRU"
flushInterval="1800000"
size="512"
readOnly="false"/>
<!-- 查询级配置 -->
<select id="selectById" resultMap="ResultMap" useCache="true">
SELECT * FROM users WHERE id = #{id}
</select>
<select id="selectAll" resultMap="ResultMap" useCache="false">
<!-- 不常变化的表禁用缓存 -->
SELECT * FROM sys_config
</select>缓存使用场景
| 场景 | 是否使用缓存 |
|---|---|
| 读多写少 | ✅ 使用 |
| 实时性要求高 | ❌ 禁用 |
| 大数据量查询 | ❌ 禁用 |
| 关联查询 | ⚠️ 谨慎使用 |
| 分布式环境 | ❌ 使用 Redis |
相关页面
- **Java\MyBatis\MyBatis-完全指南** - 返回总览
- **Java\MyBatis\MyBatis-进阶应用** - 分页插件、性能优化
- **Java\MyBatis\MyBatis-关联查询** - 一对多缓存问题
#标签 #mybatis #缓存 #一级缓存 #二级缓存 #cache #redis