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

全部评论
有还在为简历上没有拿的出手的实习项目困扰的同学,可以来找我包装个适合你的大厂项目,这个项目保证不会烂大街,已经很多同学在我这包装完后成功上岸的,马上秋招春招了别错过。需要的可以直接进我主页看简介
点赞 回复 分享
发布于 2025-08-26 14:51 江苏

相关推荐

2025-12-08 09:20
门头沟学院 Java
1.项目介绍2.分布式系统中的幂等性问题在设计一个电商系统的订单支付接口时,如何保证幂等性,防止重复支付?3.消息队列的消息积压处理若Kafka消息队列出现大量消息积压,你会从哪些方面排查原因?请详细说明对应的解决方案。4.微服务接口的熔断降级设计在基于SpringCloud的微服务架构中,如何设计一个依赖外部第三方服务的接口的熔断降级策略,确保系统高可用?5.Redis&nbsp;缓存雪崩应对当Redis缓存发生雪崩:导致大量请求直接打到数据库,如何快速恢复系统并预防此类问题再次发生?6.数据库分库分表后的事务处理在电商系统数据库分库分表后,跨库的订单创建与库存扣减操作,怎样保证事务的一致性?7.JVM垃圾回收器的选择与调优针对不同的业务场景,如何选择合适的JVM垃圾回收器?8.高并发下的接口限流实现在秒杀活动场景中,如何基于Guava的&nbsp;RateLimiter或Sentinel实现接口限流,避免系统被流量击垮?9.Java线程池的动态调整在一个实时日志处理系统中,如何根据任务队列长度和系统负载动态调整线程池的核心线程数与最大线程数?10.Spring&nbsp;Boot&nbsp;应用的启动性能优化对于一个包含大量自动配置和第三方依赖的SpringBoot应用,怎样优化其启动时间?11.分布式系统中的全局唯一ID生成在分布式订单系统中,如何设计一个高效且全局唯一的订单&nbsp;ID生成方案?12.JVM内存碎片的分析与解决当Java应用频繁出现FullGC且存在内存碎片问题,如何利用工具定位并解决?13.分布式锁的性能优化使用Redis实现分布式锁时,如何提高锁的获取和释放效率,减少线程等待时间?14.MySQL慢查询优化实战在项目中发现一条执行缓慢的SOL语句,你会通过哪些步骤和方法进行优化?15.Spring事务失效场景分析在Spring项目中,哪些情况会导致事务注解@Transactional失效?如何排查和解决?16.Kafka消息重复消费处理在使用Kafka作为消息队列时,若出现消息重复消费该如何设计解决方案?17.分布式系统中的服务注册与发现基于Nacos实现服务注册与发现,如何保证服务实例的健康检查准确性和高可用性?18.Java对象的序列化与反序列化在分布式RPC调用中,为什么需要对象序列化?常见的序列化框架有哪些,如何选择?19.数据库读写分离的实现与问题在电商系统中实现MySOL读写分离后,可能会遇到哪些数据一致性问题?如何解决?
查看18道真题和解析
点赞 评论 收藏
分享
2025-12-03 09:24
复旦大学 Java
性格问题,面试之前非常紧张,没有录音,说一下记得的内容8.28一面(95min)1.子类与父类的加载过程,静态方法重写,静态代码块2.Java中sync阻塞与可重入是怎么实现的,轻量级锁这么可重入,重量级锁维护了wait&nbsp;set3.sync锁升级,AQS4.用户态,内核态5.wait与sleep,调用后操作系统发生了什么,sleep是怎样唤醒的6.concurrenthashmap介绍,size()怎么个流程7.单例模式,双重校验锁,volatile机制,可见性原理(MESI),直接将sync加方法上的锁粒度问题,类加载机制的懒汉8.MySQL中select语句执行流程9.事务与并发MVCC10.rr隔离级别下的间隙锁导致的死锁问题11.索引的注意事项12.uuid与自增id,顺序写与随机写,空间占用,可能导致极端情况varchar数据存不下导致的行溢出问题,同时网络消耗(其实这都不重要,能消耗多少)11.mysql能否保证数据不丢失12.事务注解失效的情况,你是怎么避免的,主要说了代理类,吃异常的一些问题13.spring循环依赖解决,两级缓存能否解决,可以,只是需要实例化后都暴露且暴露代理类,三级缓存提供了一个延迟以及按需加载的思想(不知道理解的对不对),判断是否需要加载代理类,以及需要时才提前暴露14.mybatis执行流程,缓存有了解吗,说一下优缺点15.实习问题手撕:链表加法9.3二面(65min)1.聊一下hashmap,负载因子与len&nbsp;=&nbsp;8锁涉及到的概率问题,树化的优缺点2.介绍concurrenthashmap&nbsp;-&amp;gt;&nbsp;CAS&nbsp;+&nbsp;sync&nbsp;+&nbsp;volatile,主要说一下尽量使用用户态的CAS去尝试,这点再AQS中也有体现(不知道我的理解有没有问题)3.你说了concurrenthashmap&nbsp;这么多优点,那有没有缺点&nbsp;-&amp;gt;&nbsp;主要提了一下并发机制,常见的缓存,MVCC中rc,rr允许读旧数据,尽管volatile无需加锁,但cpu缓存一致性时也存在消耗,可能在读写并发量较高时性能有所缺陷4.MySQLselect语句流程(我真绷不住了,一面问了我没复习)5.MySQL如何解析到select语法错误的,客户端连接时,用户名密码存在错误时MySQL如何解决的6.介绍B+树,主要从多叉有序平衡,树高来说7.你说了B+树有这么多优点,那有没有缺点,这边主要讲了并发量高的insert/delete场景下页分裂时锁整棵树,讲了B-link树的一些优化点,谈到了跳表设计机制8.说一下常用的jdk,1.8的新特性,lambda实现原理,功能性接口9.实习拷打手撕:山峰数组二分找变化点下标补充:简历上的实习内容有些不清楚,还好找同事咨询了,不然也是难绷有保底心态确实会不一样9.6&nbsp;oc
点赞 评论 收藏
分享
评论
点赞
2
分享

创作者周榜

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