MyBatis 缓存机制原理与数据一致性

MyBatis 的缓存机制是提升数据库访问性能的重要手段,它通过减少对数据库的查询次数,显著提高应用的响应速度。本文将深入分析 MyBatis 的一级缓存和二级缓存,包括它们的工作原理、配置方法以及如何避免缓存带来的数据一致性问题。

1.MyBatis 缓存机制概览

MyBatis 提供了两级缓存机制:

1. 一级缓存:也称为本地缓存,是 SqlSession 级别的缓存。每个 SqlSession 都有自己的缓存,不同的 SqlSession 之间的缓存相互隔离。

2. 二级缓存:也称为全局缓存,是 SqlSessionFactory 级别的缓存。所有由同一个 SqlSessionFactory 创建的 SqlSession 共享二级缓存。

缓存机制的基本工作流程如下:

查询请求 --> 二级缓存 --> 一级缓存 --> 数据库

当执行查询时,MyBatis 首先检查二级缓存,如果未命中则检查一级缓存,最后才会查询数据库。这种多级缓存的设计可以最大程度地减少数据库访问,提高性能。

2.一级缓存(SqlSession 级别缓存)

2.1 工作原理

一级缓存是 SqlSession 级别的缓存,它存在于一个 SqlSession 的生命周期内。当在同一个 SqlSession 中执行相同的 SQL 查询时,MyBatis 会直接从缓存中获取结果,而不是再次查询数据库。

一级缓存的核心实现是 PerpetualCache 类,它是一个简单的基于 HashMap 的缓存实现。

源码关键代码

// BaseExecutor.java
@Override
public  List 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 list;
    try {
        queryStack++;
        // 先从本地缓存中查询
        list = resultHandler == null ? (List) localCache.getObject(key) : null;
        if (list != null) {
            // 处理缓存结果
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 缓存未命中,查询数据库
            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) {
            // 如果是 STATEMENT 作用域,查询后清空缓存
            clearLocalCache();
        }
    }
    return list;
}

private  List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List 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;
}

2.2 缓存失效场景

一级缓存会在以下情况下失效:

  • SqlSession 关闭:当 SqlSession 关闭时,一级缓存会被清空。
  • 执行增删改操作:任何对数据库的增删改操作都会导致一级缓存失效。
  • 手动清空缓存:调用 SqlSession.clearCache() 方法可以手动清空一级缓存。

2.3 配置方法

一级缓存是默认开启的,无需额外配置。如果需要禁用一级缓存,可以将 localCacheScope 设置为 STATEMENT:


    

3.二级缓存(SqlSessionFactory 级别缓存)

3.1 工作原理

二级缓存是全局缓存,作用域是整个 SqlSessionFactory。多个 SqlSession 可以共享同一个二级缓存,这使得二级缓存的生命周期更长,缓存命中率更高。

二级缓存的默认实现也是 PerpetualCache,但 MyBatis 允许集成第三方缓存框架,如 Ehcache、Redis 等。

缓存层级结构

CachingExecutor (二级缓存)

BaseExecutor (一级缓存)

JDBC

源码关键代码

// CachingExecutor.java
@Override
public  List 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 list = (List) tcm.getObject(cache, key);
            if (list == null) {
                // 缓存未命中,委托给底层执行器查询
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 将结果放入缓存
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    // 没有启用二级缓存,直接委托给底层执行器
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

3.2 配置方法

二级缓存需要手动配置才能启用:

全局配置


    

映射文件配置

在映射文件中添加 标签:


    
    
    
    
    

自定义缓存配置


    eviction="LRU"
    flushInterval="60000"
    size="512"
    readOnly="true"/>

参数说明:

  • eviction:缓存淘汰策略,默认是 LRU(最近最少使用)。
  • flushInterval:刷新间隔,单位为毫秒。
  • size:缓存大小,最多可以存储的元素数量。
  • readOnly:是否只读,设为 true 可以提高性能。

3.3 集成第三方缓存

MyBatis 支持集成第三方缓存,例如 Ehcache:

添加依赖


    org.mybatis.caches
    mybatis-ehcache
    1.2.1

配置映射文件


    
    
    

4.缓存工作流程图

下面是一个完整的流程图,展示了 MyBatis 缓存机制的工作流程:

查询请求
    |
    v
CachingExecutor 是否启用二级缓存?
    |
    +--- 是 ---> 是否命中二级缓存?
    |           |
    |           +--- 是 ---> 返回缓存结果
    |           |
    |           +--- 否 ---> 委托给 BaseExecutor
    |
    +--- 否 ---> 直接委托给 BaseExecutor
                |
                v
BaseExecutor 查询一级缓存
    |
    +--- 命中 ---> 返回缓存结果
    |
    +--- 未命中 ---> 查询数据库
                    |
                    v
                将结果存入一级缓存
                    |
                    v
                返回结果给 CachingExecutor
                    |
                    v
                将结果存入二级缓存(如果启用)

5.缓存带来的数据一致性问题及解决方案

虽然缓存可以显著提高性能,但也可能带来数据一致性问题。以下是几种常见的问题及解决方案:

5.1 脏读问题

问题描述:当一个事务修改了数据,但另一个事务读取了旧的缓存数据时,就会发生脏读。

解决方案

  • 合理设置缓存刷新策略,确保在数据更新后及时刷新缓存。
  • 对于实时性要求高的数据,不使用缓存或缩短缓存刷新周期。

5.2 缓存与数据库数据不一致

问题描述:当数据在数据库中被修改,但缓存中的数据没有及时更新时,会导致数据不一致。

解决方案

  • MyBatis 在执行增删改操作时会自动刷新相关的缓存,确保数据一致性。
  • 对于复杂的业务场景,可以使用自定义缓存刷新策略。

5.3 分布式环境下的缓存一致性

问题描述:在分布式系统中,多个服务实例可能共享同一份数据,但各自维护自己的缓存,导致缓存不一致。

解决方案

  • 使用分布式缓存,如 Redis、Memcached 等,确保所有服务实例访问同一个缓存。
  • 在数据更新时,通过消息队列通知所有服务实例刷新缓存。

6.最佳实践

1. 合理使用一级缓存:一级缓存是 SqlSession 级别的,适合在同一个事务中多次查询相同数据的场景。

2. 谨慎使用二级缓存:二级缓存是全局的,适合缓存一些不经常变化的数据,如字典表、配置信息等。

3. 设置合理的缓存参数:根据业务需求设置缓存的大小、刷新间隔和淘汰策略。

4. 避免缓存大数据量:缓存大量数据会占用内存,降低系统性能。

5. 注意事务边界:确保在事务提交或回滚后,缓存状态与数据库一致。

7.总结

MyBatis 的缓存机制是一把双刃剑,合理使用可以显著提高系统性能,但如果使用不当,可能会导致数据一致性问题。通过深入理解一级缓存和二级缓存的工作原理,合理配置缓存参数,并采取有效的数据一致性保障措施,可以充分发挥缓存的优势,同时避免其带来的问题。

在实际开发中,需要根据业务场景权衡缓存的使用,对于实时性要求高、数据变化频繁的业务,应该谨慎使用缓存;而对于读多写少、数据变化不频繁的业务,缓存可以带来明显的性能提升。

你可能感兴趣的:(Mybatis,java,后端,mybatis)