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

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

相关推荐

评论
点赞
3
分享

创作者周榜

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