[MyBatis学习笔记] 三、Mybatis缓存

本文深入探讨了MyBatis的一级和二级缓存机制。一级缓存是SqlSession级别的,使用HashMap存储,查询时首先检查一级缓存,未命中再查询数据库。增删改操作会清空一级缓存。二级缓存是Mapper级别,跨SqlSession共享,基于LRU算法,可配置。二级缓存执行流程涉及CachingExecutor,查询时先查二级缓存,再查一级缓存,最后查询数据库。更新时数据先暂存,提交时写入二级缓存。同时介绍了如何开启和使用二级缓存,以及Mybatis-RedisCache的分布式缓存方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、Mybatis缓存简介

以下内容来自mybatis官网对于缓存的介绍

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行1

<cache/>

这个简单语句的效果如下:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insertupdatedelete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

提示 缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java APIXML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。

提示 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=trueinsert/delete/update 语句时,缓存会获得更新。

二、一级缓存

一级缓存是SqlSession级别的缓存,其底层存储结构是HashMap

1、简单的源码分析

(1)存储结构

可通过SqlSession#clearCache()方法定位一级缓存的具体存储位置:
在这里插入图片描述
上述图示说明一级缓存的存储结构为HashMap

(2)读操作(查询)工作过程

以下借助Executor#query(MappedStatement, Object, RowBounds, ResultHandler)来查看在查询时一级缓存是如何工作的:

// Executor#query(MappedStatement, Object, RowBounds, ResultHandler)
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

其实现类BaseExecutor的相关实现代码:

// BaseExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

其中,CacheKeyMappedStatement, Object, RowBounds以及BoundSql四个部分构成:

// BaseExecutor#createCacheKey
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
	......
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    ......
}

继续执行query方法:

// BaseExecutor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
      ......
      // 首先从localCache即一级缓存中查询
      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);
      }
      ......
  }

BaseExecutor#queryFromDatabase()方法:

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ......
    try {
      // 查询数据库,获取结果
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    // 将数据库查询的结果写入缓存
    localCache.putObject(key, list);
	......
    return list;
  }

(3)写操作(增删改)工作过程

先说结论:进行增删改操作时,mybatis会刷新一级缓存
以下以新增操作为例进行简单分析:
首先找到入口SqlSession.insert(String)及其实现类DefaultSqlSession.insert(String),调用情况如下:
在这里插入图片描述
接力棒交给了DefaultSqlSession#update(String,Object)SqlSession中的insertupdatedelete操作,最终都会进入此处,执行本方法):

public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 最终调用的Executor#update方法
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

接下来进入BaseExecutor#update(MappedStatement, Object)方法:

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

由上述示例代码可见,mybatis在执行写入操作(增删改)前,会清空一级缓存。

2、测试代码

SqlSessionFactory sqlSessionFactory;
        try (
                InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
                InputStreamReader isr = new InputStreamReader(Objects.requireNonNull(in), StandardCharsets.UTF_8)
        ) {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(isr);
        }

        try (
                SqlSession session = sqlSessionFactory.openSession()
        ) {
            UserMapper userMapper = session.getMapper(UserMapper.class);

            User user1 = userMapper.findByIdNameAnn(new User(1, "lucy", null, null));

            User user2 = userMapper.findByIdNameAnn(new User(1, "lucy", null, null));

            System.out.println(user1 == user2);
        }

输出结果为true,说明第二次查询的结果是从缓存中获取的。

三、二级缓存

二级缓存是Mapper级别的缓存。多个SqlSession操作同一个Mapper中的sql语句时,可共用二级缓存。二级缓存的存储介质有多种,其可存储在内存中,也可存储在硬盘中

1、源码分析

由前文第一部分可知,在*Mapper.xml中添加<cache></cache>标签可开启二级缓存,故可以此处为突破口进行源码分析。

(1)二级缓存的加载过程

以下以SqlSessionFactory#build(Reader, String, Properties)方法作为入口查看映射文件*Mapper.xml是如何加载cache的:

SqlSessionFactoryBuilder:

STEP1:
在这里插入图片描述
XMLConfigBuilder:

STEP1:
在这里插入图片描述
STEP2:
在这里插入图片描述
STEP3:
在这里插入图片描述
XMLMapperBuilder:
STEP1(加载当前的Mapper.xml):
在这里插入图片描述
STEP2:
在这里插入图片描述

1)XMLMapperBuilder#cacheElement方法

STEP3:
在这里插入图片描述
MapperBuilderAssistant:

STEP1:
在这里插入图片描述
至此,当前Mapper所属的二级缓存加载完成。

2)XMLMapperBuilder#buildStatementFromContext方法

在这里插入图片描述
XMLStatementBuilder:

在这里插入图片描述
在这里插入图片描述
MapperBuilderAssistant#addMappedStatement:
在这里插入图片描述
由此可见,同一Mapper.xml中的所有statement共享一个二级缓存。

(2)二级缓存的执行过程

以下将通过SqlSession#selectList()作为入口,来分析二级缓存的执行过程。不过在此之前,我们先探究一下SqlSession是如何被创建的。

1)SqlSesssion中Executor的类型

可通过SqlSessionFactory.openSession()方法来创建SqlSession对象,openSession方法的具体实现(DefaultSqlSessionFactory.openSession())如下:
DefaultSqlSessionFactory:

STEP1:
在这里插入图片描述
STEP2(主要查看Executor是如何获取的):
在这里插入图片描述
Configuration:
在这里插入图片描述
由此可见,当缓存开启(mybatis-configsetting标签的cacheEnabled属性被设置为true——默认为true)时,实际的Executor对象时CachingExecutor

2)selectList中二级缓存的执行过程

DefaultSqlSession#selectList(String, Object, RowBounds)方法作为开始。
DefaultSqlSession:

STEP1:
在这里插入图片描述
由上一小节的分析可知,cacheEnabled属性被设置为true的情况下,此处的executor对象的具体实现为CachingExecutor类。
CachingExecutor:

query:
在这里插入图片描述
由此可见,mybatis中的查询步骤为:二级缓存(CachingExecutor中) -> 一级缓存(BaseExecutor中) -> 数据库
commit:
在这里插入图片描述

TransactionalCacheManager:
在这里插入图片描述
TransactionalCache:

getObject:
在这里插入图片描述
putObject:
在这里插入图片描述
commit:
在这里插入图片描述
flushPendingEntries:
在这里插入图片描述
由上述putObjectcommit可见,查询数据库所得的结果,并未立即放入二级缓存中,而是先存入entriesToAddOnCommit集合中,直到commit方法被执行时,才由flushPendingEntries方法将结果写入二级缓存中。
clear:
在这里插入图片描述

2、简单的使用

(1)开启二级缓存

*Mapper.xml中添加<cache></cache>
或者在*Mapper接口中添加注解:@CacheNamespace

3、分布式缓存:Mybatis-RedisCache

使用redis作为二级缓存。
需添加依赖:

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

参考:


  1. Mybatis缓存简介↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值