MyBatis一级缓存解密:深入探究缓存机制与应用场景

简介: MyBatis一级缓存解密:深入探究缓存机制与应用场景

在上文《探秘MyBatis缓存原理:Cache接口与实现类源码分析》中,我们已经介绍了 MyBatis 的 Cache 接口以及对应的实现类。其中的 PerpetualCache 是 MyBatis 缓存的最基础的实现类,底层通过 HashMap 存储数据,其他的实现类都属于装饰器,基于 PerpetualCache 的各个方面进行增强,各个实现类的理论和实现我们学习过后,本文我们就来探究一下,MyBatis 真正的缓存机制是怎么样的!

MyBatis 缓存机制

MyBatis 为提高其数据库查询性能,提供了两层缓存机制(只针对查询做缓存),包括一级缓存和二级缓存。

  1. ⼀级缓存:将查询到的数据存储到 SqlSession 中,所以只对本 SqlSession 有效。范围比较小,只对于一次 SQL 会话。
  2. ⼆级缓存:将查询到的数据存储到 SqlSessionFactory 中。范围比较大,针对于整个数据库级别。

MyBatis 也可以集成其它第三方缓存:比如基于 Java 开发的 EhCache、基于 C 语言开发的 Memcache等。

一级缓存

一级缓存也叫本地缓存(LocalCache)。一级缓存默认开启,当 MyBatis 在一次 SqlSession 数据库查询之后,会将查询结果以键值对形式存储到内存中,当前 SqlSession 后续以相同 SQL 查询时,会直接去查询内存缓存,避免数据库查询,提高查询性能。

  1. 作用范围:一级缓存的作用范围是在同一个 SqlSession 中。当你在一个 SqlSession 中执行了一次查询操作后,查询的结果会被缓存在内存中,下次执行相同的查询时,MyBatis 会首先检查缓存中是否存在该查询的结果,如果存在,则直接返回缓存中的结果,而不会再次查询数据库。
  2. 生命周期:一级缓存的生命周期与 SqlSession 相关联。当 SqlSession 关闭时,一级缓存也会被清空,这意味着一级缓存只在 SqlSession 的生命周期内有效。
  3. 缓存键:MyBatis 默认使用 SQL 语句、输入参数和 RowBounds 作为缓存的键值。这意味着如果两次查询的 SQL 语句相同、输入参数相同且分页参数 RowBounds 相同,则会命中缓存。
  4. 缓存清除:MyBatis 提供了多种方式来清除一级缓存,包括调用 SqlSession 的 clearCache() 方法手动清除缓存、执行 update、insert 或 delete 操作时自动清除缓存等。
  5. 缓存失效:当执行 update、insert 或 delete 操作时,MyBatis 会自动清除一级缓存,以避免缓存中的数据与数据库中的数据不一致。
  6. 线程安全性:由于一级缓存是与 SqlSession 相关联的,因此它是线程安全的。每个 SqlSession 都有自己的一级缓存,不同线程的操作不会相互影响。

验证一级缓存

MyBatis 配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    <typeAliases>
        <typeAlias type="world.xuewei.mybatis.entity.Account" alias="Account"/>
    </typeAliases>
    <environments default="default">
        <environment id="default">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://**.**.**/learn?useSSL=false&amp;characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mappers/AccountMapper.xml"/>
    </mappers>
</configuration>
测试类
package world.xuewei;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import world.xuewei.mybatis.dao.AccountDao;
import java.io.IOException;
import java.io.InputStream;
/**
 * @author 薛伟
 * @since 2023/9/14 20:51
 */
public class CacheTest {
    private SqlSession sqlSession;
    @Before
    public void before() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sessionFactory.openSession();
    }
    @After
    public void after() {
        sqlSession.commit();
    }
    /**
     * 验证一级缓存默认开启,但是一级缓存只对同一个 SqlSession 有效
     */
    @Test
    public void testGetAll() {
        AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
        accountDao.getAll().forEach(System.out::println);
        System.out.println("======================================");
        accountDao.getAll().forEach(System.out::println);
    }
}

通过观察日志的打印情况,可以看出,在第二次执行相同的查询方法时,就已经是从缓存中直接拿到的数据了。

清理一级缓存

手动清理
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
accountDao.getAll().forEach(System.out::println);
sqlSession.clearCache();
accountDao.getAll().forEach(System.out::println);
执行 update、insert 或 delete 操作

不管你是操作哪张表的,都会清空一级缓存。

AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
accountDao.getAll().forEach(System.out::println);
accountDao.delete(1);
accountDao.getAll().forEach(System.out::println);

关闭一级缓存

一级缓存默认开启,但是如果想关闭一级缓存,可以使用localCacheScopde=statement来关闭。即添加在 MyBatis 的配置文件中的 标签下:

<settings>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

通过观察 Configuration 配置类的源码可以看到,localCacheScope 的默认值为 Session。

public class Configuration {
    // ...
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
    //...
}

关闭后再次执行上面的测试代码可以看到,每次执行的时候都是去查询了数据库。

为什么可以通过指定localCacheScopde=statement来关闭一级缓存呢?这里先剧透一下,接下来我们会讲到 MyBatis 的一级缓存主要是通过 BaseExecutor 类来实现的,核心的方法如下:

实现原理

我们带着答案去找原因,为什么一级缓存时绑定在 SqlSession 的?通过我们原有的认知,我们要联想到这个图:

SqlSession 将 SQL 语句的处理和执行交给 Executor 去处理。联想前面学习的核心对象:《深度解析MyBatis核心:探寻其核心对象的精妙设计》,缓存的支持就是又 Executor 来搞的。而 Executor 是接口,查看他的几个实现类后不难发现,我们在 BaseExecutor 中发现了基于 HashMap 实现缓存的老熟人 PerpetualCache。

public abstract class BaseExecutor implements Executor {
    // 本地缓存
    protected PerpetualCache localCache;
    // 这个也是缓存,但是服务于存储过程
    protected PerpetualCache localOutputParameterCache;
    protected Configuration configuration;
    // 省略其他属性...
    protected int queryStack;
    private boolean closed;
    protected BaseExecutor(Configuration configuration, Transaction transaction) {
        this.transaction = transaction;
        this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
        // 为两个缓存初始化 ID
        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) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        // 创建缓存 Key
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    @SuppressWarnings("unchecked")
    @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++;
            // 如果缓存有数据,就从缓存拿
            list = resultHandler == null ? (List<E>) 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();
            // 注意这里,如果 localCacheScope 设置为 STATEMENT,那么每次查询完都会把缓存再清理...
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                clearLocalCache();
            }
        }
        return list;
    }
    @Override
    public void close(boolean forceRollback) {
        try {
            try {
                rollback(forceRollback);
            } finally {
                if (transaction != null) {
                    transaction.close();
                }
            }
        } catch (SQLException e) {
            log.warn("Unexpected exception on closing transaction.  Cause: " + e);
        } finally {
            // 关闭 Executor 的时候清空缓存
            transaction = null;
            deferredLoads = null;
            localCache = null;
            localOutputParameterCache = null;
            closed = true;
        }
    }
    @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        // 更新操作会清空缓存
        clearLocalCache();
        return doUpdate(ms, parameter);
    }
    @Override
    public void commit(boolean required) throws SQLException {
        if (closed) {
            throw new ExecutorException("Cannot commit, transaction is already closed");
        }
        // 提交操作会清空缓存
        clearLocalCache();
        flushStatements();
        if (required) {
            transaction.commit();
        }
    }
    @Override
    public void rollback(boolean required) throws SQLException {
        if (!closed) {
            try {
                // 回滚操作会清空缓存
                clearLocalCache();
                flushStatements(true);
            } finally {
                if (required) {
                    transaction.rollback();
                }
            }
        }
    }
    @Override
    public void clearLocalCache() {
        if (!closed) {
            localCache.clear();
            localOutputParameterCache.clear();
        }
    }
    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;
    }
    // 省略其他方法...
}

一级缓存的 Key 的结构,通过 createCacheKey 方法来创建缓存,源码如下:

通过源码我们不难发现,Key 的生成策略:MappedStatement.id + Offset + Limit + Sql + Param[*].value + Environment.id,这些值都相同,生成的 Key 就相同。


相关文章
|
Web App开发 移动开发 前端开发
Chrome各个版本小常识
Chrome各个版本小常识
|
JSON Linux 数据安全/隐私保护
|
8月前
|
缓存 NoSQL Java
Redis应用—6.热key探测设计与实践
热key问题在高并发系统中可能导致数据层和服务层的严重瓶颈,如Redis集群瘫痪和用户体验下降。为解决此问题,京东开发了JdHotkey热key探测框架,具备实时性、准确性、集群一致性和高性能等特点。该框架由etcd集群、Client端jar包、Worker端集群和Dashboard控制台组成,通过分布式计算快速识别热key并推送至应用内存,有效减轻数据层负载,提升服务性能。JdHotkey适用于多种场景,安装部署简便,支持毫秒级热key探测和集群一致性维护。
433 61
Redis应用—6.热key探测设计与实践
|
5月前
|
数据采集 Web App开发 JavaScript
Python爬虫解析动态网页:从渲染到数据提取
Python爬虫解析动态网页:从渲染到数据提取
|
机器学习/深度学习 人工智能 自然语言处理
比较Python和Java哪个更好
比较Python和Java哪个更好
393 5
|
Windows
DOS 批处理 setlocal命令、endlocal命令详解
setlocal这是一个命令,它开始局部化环境更改,通常在批处理文件中使用,以确保在脚本中所做的任何环境更改(例如设置或修改环境变量)不会影响到调用此批处理的上下文或其他批处理文件
643 14
|
NoSQL 容灾 MongoDB
MongoDB主备副本集方案:两台服务器使用非对称部署的方式实现高可用与容灾备份
在资源受限的情况下,为了实现MongoDB的高可用性,本文探讨了两种在两台服务器上部署MongoDB的方案。方案一是通过主备身份轮换,即一台服务器作为主节点,另一台同时部署备节点和仲裁节点;方案二是利用`priority`设置实现自动主备切换。两者相比,方案二自动化程度更高,适合追求快速故障恢复的场景,而方案一则提供了更多的手动控制选项。文章最后对比了这两种方案与标准三节点副本集的优缺点,指出三节点方案在高可用性和数据一致性方面表现更佳。
1074 5
|
机器学习/深度学习 自然语言处理 算法
ICML 2024:零阶优化器微调大模型,大幅降低内存
【7月更文挑战第14天】ICML 2024研究表明,零阶优化用于大模型微调能大幅降低内存需求。该论文通过避免反向传播,减少LLM(大型语言模型)微调的内存开销,提出新方法,适用于资源受限环境。虽然性能可能不及一阶优化器,但为高效NLP计算开辟了新途径。论文链接:[arxiv.org/abs/2402.11592](https://arxiv.org/abs/2402.11592)**
443 3
|
消息中间件 数据采集 监控
中间件数据集成
【7月更文挑战第7天】
330 4