副标题:为什么同一个查询有时快有时慢?缓存的秘密!🎯
🎬 开场:缓存的作用
没有缓存 vs 有缓存
没有缓存:
getUserById(1) → 查询数据库 ⏱️ 10ms
getUserById(1) → 查询数据库 ⏱️ 10ms ← 重复查询
getUserById(1) → 查询数据库 ⏱️ 10ms ← 重复查询
总耗时:30ms ❌
有缓存:
getUserById(1) → 查询数据库 ⏱️ 10ms → 放入缓存
getUserById(1) → 读取缓存 ⏱️ 0.01ms ✅
getUserById(1) → 读取缓存 ⏱️ 0.01ms ✅
总耗时:10.02ms ✅ 快了近3倍!
📚 MyBatis缓存体系
两级缓存
MyBatis缓存结构:
┌─────────────────────────────────────┐
│ SqlSession 1 │
│ ┌──────────────────────────────┐ │
│ │ 一级缓存(默认开启) │ │
│ │ - SqlSession级别 │ │
│ │ - 生命周期:SqlSession │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ SqlSession 2 │
│ ┌──────────────────────────────┐ │
│ │ 一级缓存(默认开启) │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
↓
↓ 共享
↓
┌─────────────────────────────────────┐
│ 二级缓存(需要手动开启) │
│ - Mapper级别 │
│ - 生命周期:Application │
│ - 跨SqlSession共享 │
└─────────────────────────────────────┘
🎯 一级缓存(SqlSession级别)
原理
一级缓存(默认开启):
特点:
1. SqlSession级别
2. 默认开启,无法关闭
3. 生命周期:SqlSession
4. 数据结构:HashMap
工作流程:
1. 执行查询
2. 查询结果放入一级缓存
3. 相同查询从缓存读取
4. SqlSession关闭,缓存清空
代码示例
/**
* 一级缓存示例
*/
@Test
public void testFirstLevelCache() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询:查数据库
System.out.println("第一次查询");
User user1 = mapper.selectById(1L);
System.out.println("查询结果:" + user1);
// 第二次查询:走缓存
System.out.println("第二次查询");
User user2 = mapper.selectById(1L);
System.out.println("查询结果:" + user2);
// 验证:两次查询返回的是同一个对象
System.out.println("是否同一个对象:" + (user1 == user2)); // true ✅
sqlSession.close();
}
/**
* 输出:
* 第一次查询
* ==> Preparing: SELECT * FROM users WHERE id = ?
* ==> Parameters: 1(Long)
* <== Total: 1
* 查询结果:User(id=1, name=张三)
*
* 第二次查询
* 查询结果:User(id=1, name=张三) ← 没有SQL,直接从缓存读取
*
* 是否同一个对象:true
*/
缓存失效场景
/**
* 场景1:不同的SqlSession
*/
@Test
public void testDifferentSqlSession() {
// SqlSession 1
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1L); // 查数据库
System.out.println("Session1: " + user1);
sqlSession1.close();
// SqlSession 2(新的)
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1L); // 再次查数据库(一级缓存不共享)
System.out.println("Session2: " + user2);
sqlSession2.close();
}
/**
* 场景2:执行了增删改操作
*/
@Test
public void testCacheInvalidation() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询
User user1 = mapper.selectById(1L); // 查数据库
System.out.println("第一次查询:" + user1);
// 执行更新操作
mapper.updateUser(new User(1L, "李四")); // 缓存被清空!
// 第二次查询
User user2 = mapper.selectById(1L); // 再次查数据库
System.out.println("第二次查询:" + user2);
sqlSession.close();
}
/**
* 场景3:手动清空缓存
*/
@Test
public void testClearCache() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询
User user1 = mapper.selectById(1L);
// 手动清空缓存
sqlSession.clearCache();
// 第二次查询
User user2 = mapper.selectById(1L); // 再次查数据库
sqlSession.close();
}
/**
* 场景4:查询参数不同
*/
@Test
public void testDifferentParams() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.selectById(1L); // 查id=1
User user2 = mapper.selectById(2L); // 查id=2(不同参数,查数据库)
sqlSession.close();
}
一级缓存源码
/**
* 一级缓存实现
*
* 源码位置:BaseExecutor
*/
public abstract class BaseExecutor implements Executor {
// 一级缓存(HashMap)
protected PerpetualCache localCache;
/**
* 查询方法
*/
@Override
public <E> List<E> query(
MappedStatement ms,
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException {
// 1. 生成缓存Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 2. 从缓存查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(
MappedStatement ms,
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler,
CacheKey key,
BoundSql boundSql) throws SQLException {
// 从一级缓存获取
list = resultHandler == null ?
(List<E>) localCache.getObject(key) : null;
if (list != null) {
// 缓存命中
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 缓存未命中,查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
return list;
}
/**
* 从数据库查询
*/
private <E> List<E> queryFromDatabase(
MappedStatement ms,
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler,
CacheKey key,
BoundSql boundSql) throws SQLException {
List<E> list;
// 先在缓存中占位
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 查询数据库
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 移除占位符
localCache.removeObject(key);
}
// 放入缓存
localCache.putObject(key, list);
return list;
}
/**
* 更新操作:清空缓存
*/
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
// 清空一级缓存
clearLocalCache();
return doUpdate(ms, parameter);
}
/**
* 清空缓存
*/
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
}
}
/**
* 生成缓存Key
*
* Key组成:
* statementId + rowBounds + sql + 参数值
*/
@Override
public CacheKey createCacheKey(
MappedStatement ms,
Object parameterObject,
RowBounds rowBounds,
BoundSql boundSql) {
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId()); // statementId
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql()); // SQL语句
// 参数值
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
for (ParameterMapping parameterMapping : parameterMappings) {
Object value = parameterObject; // 获取参数值
cacheKey.update(value);
}
return cacheKey;
}
}
🎯 二级缓存(Mapper级别)
原理
二级缓存(需要手动开启):
特点:
1. Mapper级别(namespace)
2. 默认关闭,需要手动开启
3. 生命周期:Application
4. 跨SqlSession共享
5. SqlSession关闭后才会写入二级缓存
工作流程:
1. SqlSession查询数据
2. 数据先放入一级缓存
3. SqlSession关闭/提交
4. 一级缓存数据写入二级缓存
5. 新的SqlSession可以从二级缓存读取
开启二级缓存
<!-- 1. 全局配置(mybatis-config.xml) -->
<settings>
<setting name="cacheEnabled" value="true"/> <!-- 默认就是true -->
</settings>
<!-- 2. Mapper.xml中开启 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache/>
<!-- 或者自定义配置 -->
<cache
eviction="LRU" <!-- 缓存淘汰策略:LRU/FIFO/SOFT/WEAK -->
flushInterval="60000" <!-- 刷新间隔:60秒 -->
size="512" <!-- 缓存大小:512个对象 -->
readOnly="false"/> <!-- 只读:false表示可读写 -->
<select id="selectById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
/**
* 3. 实体类实现Serializable(readOnly=false时需要)
*/
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
// getter/setter...
}
代码示例
/**
* 二级缓存示例
*/
@Test
public void testSecondLevelCache() {
// SqlSession 1
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
// 第一次查询
System.out.println("第一次查询");
User user1 = mapper1.selectById(1L); // 查数据库
System.out.println(user1);
sqlSession1.close(); // 关闭后,一级缓存数据写入二级缓存
// SqlSession 2(新的)
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
// 第二次查询
System.out.println("第二次查询");
User user2 = mapper2.selectById(1L); // 从二级缓存读取 ✅
System.out.println(user2);
sqlSession2.close();
}
/**
* 输出:
* 第一次查询
* ==> Preparing: SELECT * FROM users WHERE id = ?
* ==> Parameters: 1(Long)
* <== Total: 1
* User(id=1, name=张三)
*
* 第二次查询
* Cache Hit Ratio [com.example.mapper.UserMapper]: 0.5 ← 缓存命中
* User(id=1, name=张三) ← 没有SQL,从二级缓存读取
*/
缓存淘汰策略
/**
* 缓存淘汰策略
*/
// 1. LRU(Least Recently Used)最近最少使用
// 移除最长时间不被使用的对象
<cache eviction="LRU"/>
// 2. FIFO(First In First Out)先进先出
// 按对象进入缓存的顺序来移除
<cache eviction="FIFO"/>
// 3. SOFT(软引用)
// 基于垃圾回收器状态和软引用规则移除对象
<cache eviction="SOFT"/>
// 4. WEAK(弱引用)
// 更积极地基于垃圾收集器状态和弱引用规则移除对象
<cache eviction="WEAK"/>
readOnly属性
<!-- readOnly=true:只读缓存 -->
<cache readOnly="true"/>
<!--
特点:
- 返回缓存对象的引用(同一个对象)
- 性能更好(不需要序列化/反序列化)
- 不安全(所有调用者共享同一个对象)
- 实体类不需要实现Serializable
-->
<!-- readOnly=false:读写缓存(默认) -->
<cache readOnly="false"/>
<!--
特点:
- 返回缓存对象的拷贝(序列化/反序列化)
- 性能稍差
- 安全(每个调用者获得独立的对象)
- 实体类必须实现Serializable
-->
二级缓存失效
/**
* 二级缓存失效场景
*/
// 1. 执行增删改操作
@Test
public void testCacheInvalidation() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1L);
sqlSession1.close(); // 写入二级缓存
// 执行更新
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
mapper2.updateUser(new User(1L, "李四")); // 二级缓存被清空!
sqlSession2.commit();
sqlSession2.close();
// 再次查询
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
User user3 = mapper3.selectById(1L); // 查数据库
sqlSession3.close();
}
// 2. 手动清空(flushCache=true)
<select id="selectById" resultType="User" flushCache="true">
SELECT * FROM users WHERE id = #{id}
</select>
// 3. 不同的namespace
// UserMapper的缓存和OrderMapper的缓存是独立的
二级缓存源码
/**
* 二级缓存实现
*
* 源码位置:CachingExecutor
*/
public class CachingExecutor implements Executor {
private final Executor delegate; // 被装饰的Executor
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
/**
* 查询方法
*/
@Override
public <E> List<E> query(
MappedStatement ms,
Object parameterObject,
RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(
MappedStatement ms,
Object parameterObject,
RowBounds rowBounds,
ResultHandler resultHandler,
CacheKey key,
BoundSql boundSql) throws SQLException {
// 获取二级缓存
Cache cache = ms.getCache();
if (cache != null) {
// 有二级缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 从二级缓存获取
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 缓存未命中,查询数据库
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 放入二级缓存(暂存)
tcm.putObject(cache, key, list);
}
return list;
}
}
// 没有二级缓存,直接查询
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
/**
* 提交:将暂存的数据真正写入二级缓存
*/
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit(); // 提交二级缓存
}
/**
* 回滚:清空暂存的数据
*/
@Override
public void rollback(boolean required) throws SQLException {
try {
delegate.rollback(required);
} finally {
tcm.rollback(); // 回滚二级缓存
}
}
}
🆚 一级缓存 vs 二级缓存
对比表
| 维度 | 一级缓存 | 二级缓存 |
|---|---|---|
| 级别 | SqlSession | Mapper(namespace) |
| 默认状态 | 开启(无法关闭) | 关闭(需手动开启) |
| 生命周期 | SqlSession | Application |
| 共享范围 | 单个SqlSession | 跨SqlSession |
| 失效时机 | SqlSession关闭/增删改/手动清空 | 增删改/flushInterval |
| 数据结构 | HashMap | 可配置(LRU/FIFO等) |
| 序列化 | 不需要 | readOnly=false时需要 |
查询流程
完整的查询流程:
1. 查询请求
↓
2. 查询二级缓存
├─ 命中 → 返回结果 ✅
└─ 未命中 ↓
3. 查询一级缓存
├─ 命中 → 返回结果 ✅
└─ 未命中 ↓
4. 查询数据库
↓
5. 结果放入一级缓存
↓
6. SqlSession关闭/提交
↓
7. 一级缓存数据写入二级缓存
🎯 缓存最佳实践
什么时候使用缓存
✅ 适合使用缓存:
- 查询频繁
- 数据不经常变化
- 对实时性要求不高
- 如:字典数据、配置数据
❌ 不适合使用缓存:
- 数据经常变化
- 对实时性要求高
- 分布式环境
- 如:订单数据、库存数据
缓存雪崩问题
/**
* 问题:分布式环境下的缓存不一致
*/
// 场景:两个应用共享数据库
Application1: 更新用户数据 → 清空缓存
Application2: 缓存未清空 → 读到旧数据 ❌
/**
* 解决方案:
* 1. 不使用MyBatis二级缓存
* 2. 使用Redis等集中式缓存
* 3. 使用消息队列同步缓存
*/
🎉 总结
核心要点
1. 一级缓存
- SqlSession级别
- 默认开启
- 不能跨SqlSession
2. 二级缓存
- Mapper级别
- 需要手动开启
- 可以跨SqlSession
3. 使用建议
- 一级缓存:保留默认
- 二级缓存:谨慎使用
- 分布式:使用Redis
记忆口诀
MyBatis缓存有两级,
一级二级不一样。
一级缓存Session级,
默认开启不能关。
HashMap来存储,
同一Session可共享。
关闭清空或增删改,
缓存立即就失效。
二级缓存Mapper级,
默认关闭要手动开。
namespace来隔离,
跨Session可共享。
LRU FIFO可选择,
淘汰策略很灵活。
readOnly要注意,
true性能好不安全,
false安全要序列化,
Serializable要实现。
查询先查二级缓存,
未命中再查一级缓存,
还没有就查数据库,
结果放入一级缓存。
Session关闭或提交,
一级数据写二级。
分布式环境别用它,
缓存不一致有问题。
Redis集中式缓存,
才是分布式的选择!
愿你的查询性能飞快,缓存使用得当! 💾✨