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秋招面试#
全部评论

相关推荐

评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务