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); } 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 { if (required) { // 事务回滚时,清空待提交的缓存 tcm.rollback(); } } } } // TransactionalCacheManager - 事务缓存管理器 public class TransactionalCacheManager { private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>(); public void clear(Cache cache) { getTransactionalCache(cache).clear(); } public Object getObject(Cache cache, CacheKey key) { return getTransactionalCache(cache).getObject(key); } public void putObject(Cache cache, CacheKey key, Object value) { getTransactionalCache(cache).putObject(key, value); } public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); } } public void rollback() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.rollback(); } } private TransactionalCache getTransactionalCache(Cache cache) { return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new); } }
二级缓存测试示例:
@Test public void testSecondLevelCache() { // 第一个SqlSession try (SqlSession sqlSession1 = sqlSessionFactory.openSession()) { UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class); System.out.println("=== SqlSession1 第一次查询 ==="); User user1 = mapper1.selectById(1L); System.out.println("查询结果: " + user1); // 必须提交事务,数据才会进入二级缓存 sqlSession1.commit(); } // 第二个SqlSession try (SqlSession sqlSession2 = sqlSessionFactory.openSession()) { UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); System.out.println("=== SqlSession2 查询(应该命中二级缓存)==="); User user2 = mapper2.selectById(1L); System.out.println("查询结果: " + user2); } // 第三个SqlSession执行更新 try (SqlSession sqlSession3 = sqlSessionFactory.openSession()) { UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class); System.out.println("=== SqlSession3 执行更新 ==="); mapper3.updateById(new User(1L, "更新后的名称", 30)); sqlSession3.commit(); // 提交后二级缓存被清空 } // 第四个SqlSession再次查询 try (SqlSession sqlSession4 = sqlSessionFactory.openSession()) { UserMapper mapper4 = sqlSession4.getMapper(UserMapper.class); System.out.println("=== SqlSession4 查询(缓存已清空,重新查询)==="); User user4 = mapper4.selectById(1L); System.out.println("查询结果: " + user4); } }
🔄 缓存失效策略
缓存回收策略
面试考点:
面试官:"MyBatis的缓存回收策略有哪些?LRU是如何实现的?"
四种回收策略:
// 1. LRU - 最近最少使用(默认) public class LruCache implements Cache { private final Cache delegate; private Map<Object, Object> keyMap; private Object eldestKey; public LruCache(Cache delegate) { this.delegate = delegate; setSize(1024); } @Override public void putObject(Object key, Object value) { delegate.putObject(key, value); cycleKeyList(key); } @Override public Object getObject(Object key) { keyMap.get(key); // 更新访问顺序 return delegate.getObject(key); } private void cycleKeyList(Object key) { keyMap.put(key, key); if (eldestKey != null) { delegate.removeObject(eldestKey); } } public void setSize(final int size) { keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) { private static final long serialVersionUID = 4267176411845948333L; @Override protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) { boolean tooBig = size() > size; if (tooBig) { eldestKey = eldest.getKey(); } return tooBig; } }; } } // 2. FIFO - 先进先出 public class FifoCache implements Cache { private final Cache delegate; private final Deque<Object> keyList; private int size; public FifoCache(Cache delegate) { this.delegate = delegate; this.keyList = new LinkedList<>(); this.size = 1024; } @Override public void putObject(Object key, Object value) { cycleKeyList(key); delegate.putObject(key, value); } private void cycleKeyList(Object key) { keyList.addLast(key); if (keyList.size() > size) { Object oldestKey = keyList.removeFirst(); delegate.removeObject(oldestKey); } } } // 3. SOFT - 软引用 public class SoftCache implements Cache { private final Deque<Object> hardLinksToAvoidGarbageCollection; private final ReferenceQueue<Object> queueOfGarbageCollectedEntries; private final Cache delegate; private int numberOfHardLinks; public SoftCache(Cache delegate) { this.delegate = delegate; this.numberOfHardLinks = 256; this.hardLinksToAvoidGarbageCollection = new LinkedList<>(); this.queueOfGarbageCollectedEntries = new ReferenceQueue<>(); } @Override public void putObject(Object key, Object value) { removeGarbageCollectedItems(); delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries)); } @Override public Object getObject(Object key) { Object result = null; @SuppressWarnings("unchecked") SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key); if (softReference != null) { result = softReference.get(); if (result == null) { delegate.removeObject(key); } else { // 保持强引用,避免被GC hardLinksToAvoidGarbageCollection.addFirst(result); if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { hardLinksToAvoidGarbageCollection.removeLast(); } } } return result; } } // 4. WEAK - 弱引用 public class WeakCache implements Cache { private final ReferenceQueue<Object> queueOfGarbageCollectedEntries; private final Cache delegate; public WeakCache(Cache delegate) { this.delegate = delegate; this.queueOfGarbageCollectedEntries = new ReferenceQueue<>(); } @Override public void putObject(Object key, Object value) { removeGarbageCollectedItems(); delegate.putObject(key, new WeakEntry(key, value, queueOfGarbageCollectedEntries)); } @Override public Object getObject(Object key) { Object result = null; @SuppressWarnings("unchecked") WeakReference<Object> weakReference = (WeakReference<Object>) delegate.getObject(key); if (weakReference != null) { result = weakReference.get(); if (result == null) { delegate.removeObject(key); } } return result; } }
自定义缓存实现
Redis缓存实现:
// 自定义Redis缓存 public class RedisCache implements Cache { private final String id; private final RedisTemplate<String, Object> redisTemplate; private final int timeout = 30 * 60; // 30分钟 public RedisCache(String id) { if (id == null) { throw new IllegalArgumentException("Cache instances require an ID"); } this.id = id; this.redisTemplate = SpringContextHolder.getBean("redisTemplate", RedisTemplate.class); } @Override public String getId() { return id; } @Override public void putObject(Object key, Object value) { if (value != null) { String redisKey = getRedisKey(key); redisTemplate.opsForValue().set(redisKey, value, timeout, TimeUnit.SECONDS); } } @Override public Object getObject(Object key) { try { String redisKey = getRedisKey(key); return redisTemplate.opsForValue().get(redisKey); } catch (Exception e) { logger.error("Redis缓存获取失败", e); return null; } } @Override public Object removeObject(Object key) { try { String redisKey = getRedisKey(key); Object value = redisTemplate.opsForValue().get(redisKey); redisTemplate.delete(redisKey); return value; } catch (Exception e) { logger.error("Redis缓存删除失败", e); return null; } } @Override public void clear() { try { Set<String> keys = redisTemplate.keys(id + ":*"); if (keys != null && !keys.isEmpty()) { redisTemplate.delete(keys); } } catch (Exception e) { logger.error("Redis缓存清空失败", e); } } @Override public int getSize() { try { Set<String> keys = redisTemplate.keys(id + ":*"); return keys != null ? keys.size() : 0; } catch (Exception e) { logger.error("Redis缓存大小获取失败", e); return 0; } } private String getRedisKey(Object key) { return id + ":" + DigestUtils.md5DigestAsHex(key.toString().getBytes()); } } // 在Mapper.xml中使用自定义缓存 <cache type="com.example.cache.RedisCache"> <property name="timeout" value="1800"/> </cache>
缓存使用最佳实践
缓存配置建议:
@Configuration public class MyBatisCacheConfig { // 一级缓存配置 @Bean public ConfigurationCustomizer configurationCustomizer() { return configuration -> { // 设置一级缓存作用域 configuration.setLocalCacheScope(LocalCacheScope.SESSION); // SESSION或STATEMENT // 开启二级缓存 configuration.setCacheEnabled(true); // 开启懒加载 configuration.setLazyLoadingEnabled(true); configuration.setAggressiveLazyLoading(false); }; } } // 缓存使用场景判断 @Service public class CacheUsageService { // 适合使用缓存的场景 public List<User> getActiveUsers() { // 1. 查询频繁 // 2. 数据变化不频繁 // 3. 对数据一致性要求不高 return userMapper.selectActiveUsers(); } // 不适合使用缓存的场景 @CacheEvict(value = "userCache", key = "#user.id") public void updateUserBalance(User user) { // 1. 数据变化频繁 // 2. 对数据一致性要求高 // 3. 实时性要求高 userMapper.updateBalance(user); } // 缓存穿透防护 public User getUserById(Long id) { if (id == null || id <= 0) { return null; } User user = userMapper.selectById(id); if (user == null) { // 缓存空对象,防止缓存穿透 // 可以设置较短的过期时间 } return user; } }
💡 面试重点总结
高频面试题
1. 一级缓存vs二级缓存
必答要点: - 一级缓存:SqlSession级别,默认开启,线程不安全 - 二级缓存:Mapper级别,需要配置,跨SqlSession共享 - 一级缓存在增删改后自动清空 - 二级缓存在事务提交后才生效
2. 缓存失效机制
必答要点: - 执行增删改操作自动清空缓存 - 手动调用clearCache()清空一级缓存 - flushCache="true"清空二级缓存 - 缓存回收策略:LRU、FIFO、SOFT、WEAK
3. 缓存使用注意事项
必答要点: - 二级缓存需要实体类实现Serializable - 跨SqlSession的数据一致性问题 - 分布式环境下的缓存同步问题 - 缓存穿透、击穿、雪崩的防护
答题技巧
技巧1:结合实际场景
"在我们的项目中,用户基础信息查询频繁但变化不多,所以开启了二级缓存..."
技巧2:对比其他缓存方案
"相比Redis这种外部缓存,MyBatis的本地缓存减少了网络开销..."
技巧3:提及性能优化
"通过合理配置缓存,我们的查询性能提升了60%..."
本节核心要点:
- ✅ 一级缓存和二级缓存的实现原理和区别
- ✅ 缓存失效的各种场景和触发条件
- ✅ 四种缓存回收策略的实现机制
- ✅ 自定义缓存实现(Redis缓存示例)
- ✅ 缓存使用的最佳实践和注意事项
下一节预告: 动态SQL与性能优化 - 灵活构建SQL和批量操作优化
#java秋招面试#