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

举报
薛伟同学 发表于 2024/12/20 12:09:59 2024/12/20
【摘要】 MyBatis的一级缓存是提高数据库访问性能的重要组成部分,也是默认开启的缓存机制。本文将深入研究MyBatis一级缓存,详细解析其工作原理、优势和应用场景。我们将探讨一级缓存的生命周期、作用范围、失效策略等关键特性,以及如何利用一级缓存提高系统的性能和可维护性。

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>

    <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);
    }
}

image.png

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

清理一级缓存

手动清理

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> 标签下:

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

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

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

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

image.png

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

image.png

实现原理

我们带着答案去找原因,为什么一级缓存时绑定在 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 就相同。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。