7.2 缓存机制深入
面试重要程度:⭐⭐⭐⭐⭐
常见提问方式:一级缓存与二级缓存区别、缓存失效策略
预计阅读时间:25分钟
开场白
兄弟,MyBatis的缓存机制绝对是面试官最爱问的!特别是一级缓存和二级缓存的区别,缓存什么时候失效,这些都是考察你对MyBatis深度理解的关键点。
很多人只知道MyBatis有缓存,但不知道底层是怎么实现的,今天我们就把这个机制彻底搞透!
🗄️ 一级缓存与二级缓存
一级缓存(SqlSession级别)
面试高频:
面试官:"MyBatis的一级缓存是什么?什么时候会失效?"
一级缓存原理:
// BaseExecutor中的一级缓存实现
public abstract class BaseExecutor implements Executor {
// 一级缓存:PerpetualCache实现
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 1. 先从一级缓存中查找
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 2. 缓存中没有,从数据库查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
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);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
}
一级缓存测试示例:
@Test
public void testFirstLevelCache() {
try (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
System.out.println("对象地址1: " + System.identityHashCode(user1));
System.out.println("对象地址2: " + System.identityHashCode(user2));
System.out.println("=== 执行更新操作 ===");
mapper.updateById(new User(1L, "新名称", 25));
System.out.println("=== 更新后再次查询 ===");
User user3 = mapper.selectById(1L);
System.out.println("查询结果: " + user3);
System.out.println("是否为同一对象: " + (user1 == user3)); // false,缓存已清空
}
}
// 输出结果:
// === 第一次查询 ===
// DEBUG - ==> Preparing: SELECT * FROM user WHERE id = ?
// DEBUG - ==> Parameters: 1(Long)
// DEBUG - <== Total: 1
// 查询结果: User(id=1, name=张三, age=20)
// === 第二次查询(相同参数)===
// 查询结果: User(id=1, name=张三, age=20) // 没有SQL日志,说明走了缓存
// 是否为同一对象: true
// === 执行更新操作 ===
// DEBUG - ==> Preparing: UPDATE user SET name = ?, age = ? WHERE id = ?
// DEBUG - ==> Parameters: 新名称(String), 25(Integer), 1(Long)
// DEBUG - <== Updates: 1
// === 更新后再次查询 ===
// DEBUG - ==> Preparing: SELECT * FROM user WHERE id = ? // 重新执行SQL
// DEBUG - ==> Parameters: 1(Long)
// DEBUG - <== Total: 1
// 查询结果: User(id=1, name=新名称, age=25)
// 是否为同一对象: false
一级缓存失效场景:
public class FirstLevelCacheInvalidation {
@Test
public void testCacheInvalidation() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 1. 执行增删改操作,缓存自动清空
mapper.selectById(1L); // 缓存
mapper.insert(new User("新用户", 30)); // 清空缓存
mapper.selectById(1L); // 重新查询数据库
// 2. 手动清除缓存
mapper.selectById(2L); // 缓存
sqlSession.clearCache(); // 手动清空
mapper.selectById(2L); // 重新查询数据库
// 3. 不同的SqlSession,缓存不共享
}
try (SqlSession sqlSession2 = sqlSessionFactory.openSession()) {
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
mapper2.selectById(1L); // 新的SqlSession,重新查询
}
}
}
二级缓存(Mapper级别)
面试重点:
面试官:"二级缓存是什么?如何配置?什么时候使用?"
二级缓存配置:
<!-- 1. 在mybatis-config.xml中开启二级缓存 -->
<configuration>
<settings>
<!-- 开启二级缓存,默认为true -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
<!-- 2. 在Mapper.xml中配置缓存 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 基础缓存配置 -->
<cache
eviction="LRU" <!-- 缓存回收策略:LRU、FIFO、SOFT、WEAK -->
flushInterval="60000" <!-- 刷新间隔:60秒 -->
size="512" <!-- 缓存对象数量 -->
readOnly="false"/> <!-- 是否只读 -->
<!-- 查询语句配置 -->
<select id="selectById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 不使用二级缓存的查询 -->
<select id="selectSensitiveData" resultType="User" useCache="false">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 更新操作会清空二级缓存 -->
<update id="updateById" flushCache="true">
UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}
</update>
</mapper>
二级缓存实现原理:
// CachingExecutor - 二级缓存执行器
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
@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) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
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);
}
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经 文章被收录于专栏
Java面试圣经,带你练透java圣经
查看2道真题和解析