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圣经