MyBatis缓存原理

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
简介: MyBatis缓存原理

前言

提示:自从上次发现mybatis缓存可被修改后,就一直想针对myBatis缓存单独做一期分析,包含其原理和运行方式,现在终于得空来详细写一篇了


一、MyBatis的两级缓存介绍

熟悉MyBatis的应该知道,MyBatis内置了两级缓存,会在查询数据库时,将查询结果缓存到内存中,以便下次查询时可以直接从缓存中获取数据,从而提高数据查询效率

MyBatis缓存一般分为一级缓存和二级缓存。


1c7cb1ca9bdf451db99aceccb4928530.png


一级缓存

是指MyBatis自身的缓存机制,是SqlSession级别的缓存。当同一个SqlSession执行相同的SQL语句时,MyBatis会将查询结果缓存到内存中。一级缓存的作用域是SqlSession,当当前的SqlSession关闭时,一级缓存也将被清空。


二级缓存

是指MyBatis全局的缓存机制,在多个SqlSession之间共享缓存数据。二级缓存的作用域是Mapper级别的,每个Mapper对应一个缓存。在同一应用程序中的多个SqlSession都可以共享同一个缓存,这是一种横向共享的缓存机制。但是需要注意的是,该缓存只有在Mapper映射文件中声明了缓存的情况下才能启用。


二、一级缓存

1. sqlSession的结构和目的

在说SqlSession级别的缓存前,我们需要介绍下sqlSession本身的一些内容。我们都知道,在早期项目中,与数据库的连接通常以JDBC 的 connection(即连接)为核心,通过jdbc的API获取数据库连接,并执行代码,其形式大体如下:

import java.sql.*;
public class MySqlConnectionExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydatabase";
        String user = "root";
        String password = "mypassword";
        String sql = "SELECT * FROM mytable";
        try {
            // 1. 加载 MySQL JDBC 驱动程序
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 2. 建立数据库连接(仅举例,实际项目一般都有连接池)
            Connection conn = DriverManager.getConnection(url, user, password);
            // 3. 创建 Statement 对象
            Statement stmt = conn.createStatement();
            // 4. 执行 SQL 查询语句,并返回结果集
            ResultSet rs = stmt.executeQuery(sql);
            // 5. 遍历结果集,输出查询结果
            while (rs.next()) {
                int id = rs.getInt("id");
                String name = rs.getString("name");
                System.out.println("id: " + id + ", name: " + name);
            }
            // 6. 关闭结果集、Statement 对象和数据库连接
            rs.close();
            stmt.close();
            conn.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

看起来,通过获取连接来执行sql似乎就够了,为什么myBatis还要加入一个sqlSqlsession,sqlSqlsession又是拿来做什么的呢?其实,使用 SqlSession 相比于 Connection 直接执行 SQL 的主要优点如下:


  • 封装了数据库连接的创建和释放,简化了代码编写,降低了出错风险。
  • 提供了多种执行 SQL 的方法,支持动态 SQL 和对象关系映射(ORM)等高级特性。
  • 支持缓存机制,可以缓存操作结果和查询结果,提高系统性能。

226d3532a13d4d87a1a86560b201461b.png

在应用程序启动时执行的进程。下述处理(1)至(4)对应于这种类型


  1. SqlSessionFactoryBean 请求SqlSessionFactory 构建SqlSessionFactoryBuilder
  2. SqlSessionFactoryBuilder 读取 MyBatis 配置文件生成SqlSessionFactory.
  3. SqlSessionFactoryBuilder 根据 MyBatis 配置文件的定义生成SqlSessionFactory ,SqlSessionFactory 生成的对象由 Spring 容器存储
  4. MapperFactoryBean 生成一个线程安全的SqlSession (SqlSessionTemplate)和一个线程安全的Mapper对象(Mapper接口的Proxy对象)。生成的Mapper对象存储在Spring 容器中。Mapper对象使用线程安全的SqlSession ( 即 SqlSessionTemplate) 提供线程安全的实现。
  5. 为每个请求执行的过程。下述处理(5)至(11)对应于这种类型

请求应用程序的进程

  1. Application(Service)调用容器注入的Mapper对象(实现Mapper接口的Proxy对象)的方法。
  2. Mapper对象调用与之对应的SqlSession (SqlSessionTemplate) 方法。
  3. SqlSession ( SqlSessionTemplate) 启用代理并调用其线程安全的SqlSession 方法
  4. 代理内的SqlSession 分配给事务。当事务没有对应的sqlSession时,将调用SqlSessionFactory 去获取一个SqlSession
  5. SqlSessionFactory 返回 SqlSession。返回的SqlSession 被分配给事务,如果它在同一个事务内,则使用该SqlSession而不创建新的
  6. SqlSession 从映射文件中获取要执行的 SQL 并执行 SQL。

2. sqlSession缓存

sqlSession缓存并非直接挂在sqlSession对象下,而是存储在执行器 BaseExecutor.class 中的 localCache(缓存查询结果) 和 localOutputParameterCache(缓存存储过程调用结果)实现,类名为 PerpetualCache.class

3d8e340b3b824e8bb8efb67bbbd5f798.png

cc3fd58a3fda4b15803771c4bec08e4e.png


而PerpetualCache类则内含一个普通的HashMap,这个HashMap即真正的缓存位置


f9647f394ee545c8b0f754853ed61790.png

这样的缓存键值对,值我们是能想到的,就是SQL查询后返回的结果列表,那么键是什么呢?

CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);

CacheKey 由以下几个因素影响


  • namespace.id
  • 用户传递给 SQL 语句的实际参数值
  • 指定查询结果集的范围(分页信息)
  • 查询所使用的 SQL 语句

而它的存储流程,其实很简单

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        // 省略部分代码
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            // 如果开启了flushCache,则清除缓存
          clearLocalCache();
      }
        List<E> list;
        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);
        }
        // 省略部分代码
      return list;
  }
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
        List list;
        try {
            list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            this.localCache.removeObject(key);
        }
        this.localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            this.localOutputParameterCache.putObject(key, parameter);
        }
        return list;
  }

3. 缓存生命周期

BaseExecutor不仅定义了一级缓存,同时也声明了这种缓存的生命周期:


  • 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
  • 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
  • SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可-以继续使用;
  • 会话结束

但是,从源码我们不难发现一个问题,从缓存中取到的内容,是作为查询结果直接返回的。这意味着,如果我们对结果进行修改,将直接改变缓存值。并且,因为一级缓存没有提供关闭的参数,所以此时我们有两种方式解决

1.深拷贝sql结果,对拷贝的对象进行操作而非原对象

2.对指定sql,设置参数 flushCache=“true”,表示任何时候语句被调用,都会导致本地缓存和二级缓存被清空,形如

<select id="save" parameterType="XX" flushCache="true" useCache="false"> </select>

这样设置能生效的原因是,该参数打开后,会在SQL执行前清除掉缓存,这样相当于禁用了缓存功能

三、二级缓存

1. 开启二级缓存

二级缓存不同于一级缓存,它默认是关闭的,需要手动开启,我们知道一级缓存是由BaseExecutor负责存储的,那么负责二级缓存的实际上是它的兄弟类CachingExecutor

9db07d8ca9d442e9ba7075d8e573d178.png


它的缓存存储位置在TransactionalCacheManager 类中

public class CachingExecutor implements Executor {
  // 普通的执行器,通常就是我们上面的BaseExecutor的一个子类
  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
}
public class TransactionalCacheManager {
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
}

那么,我们如何才能启用这个执行器呢?首先,我们必须得在全局设置缓存可用,在配置文件上加上


mybatis.configuration.cache-enabled=true

mybatis-plus.configuration.cache-enabled=true


然后在我们都mapper层加上设置即可。

<mapper namespace="xx.xxx.xxx.mapper.xxxMapper">
   <cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"></cache>
</mapper>

当然,如果sql不是以xml形式写的,而是在mapper接口里以注解形式写的,那只要在mapper层加上注解 @CacheNamespace 也是同样的效果


2. 二级缓存的弊端

二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。


7cc7506764ab442bb55f3a67c4795df0.png

即使我们为其设定了自动刷新的时间,二级缓存的脏读概率仍然很大。尤其是在多表查询的情况下,尽管可以给不同的mapper设定同一个缓存,但复杂场景下这种繁琐的配置几乎不可用。

<cache-ref namespace="com.example.mapper.UserInfoMapper" />

所以二级缓存实际上很少有人会开启。


相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
6月前
|
存储 缓存 NoSQL
mybatisplus一二级缓存
MyBatis-Plus 继承并优化了 MyBatis 的一级与二级缓存机制。一级缓存默认开启,作用于 SqlSession,适用于单次会话内的重复查询;二级缓存需手动开启,跨 SqlSession 共享,适合提升多用户并发性能。支持集成 Redis 等外部存储,增强缓存能力。
|
8月前
|
缓存 Java 数据库连接
Mybatis一级缓存详解
Mybatis一级缓存为开发者提供跨数据库操作的一致性保证,有效减轻数据库负担,提高系统性能。在使用过程中,需要结合实际业务场景选择性地启用一级缓存,以充分发挥其优势。同时,开发者需注意其局限性,并做好事务和并发控制,以确保系统的稳定性和数据的一致性。
288 20
|
10月前
|
缓存 Java 数据库连接
Mybatis一级缓存、二级缓存详讲
本文介绍了MyBatis中的查询缓存机制,包括一级缓存和二级缓存。一级缓存基于同一个SqlSession对象,重复查询相同数据时可直接从缓存中获取,减少数据库访问。执行`commit`操作会清空SqlSession缓存。二级缓存作用于同一namespace下的Mapper对象,支持数据共享,需手动开启并实现序列化接口。二级缓存通过将数据存储到硬盘文件中实现持久化,为优化性能,通常在关闭Session时批量写入缓存。文章还说明了缓存的使用场景及注意事项。
332 7
Mybatis一级缓存、二级缓存详讲
|
10月前
|
SQL 缓存 Java
框架源码私享笔记(02)Mybatis核心框架原理 | 一条SQL透析核心组件功能特性
本文详细解构了MyBatis的工作机制,包括解析配置、创建连接、执行SQL、结果封装和关闭连接等步骤。文章还介绍了MyBatis的五大核心功能特性:支持动态SQL、缓存机制(一级和二级缓存)、插件扩展、延迟加载和SQL注解,帮助读者深入了解其高效灵活的设计理念。
|
11月前
|
缓存 Java 数据库连接
十、MyBatis的缓存
十、MyBatis的缓存
220 6
|
12月前
|
缓存 NoSQL Java
Mybatis学习:Mybatis缓存配置
MyBatis缓存配置包括一级缓存(事务级)、二级缓存(应用级)和三级缓存(如Redis,跨JVM)。一级缓存自动启用,二级缓存需在`mybatis-config.xml`中开启并配置映射文件或注解。集成Redis缓存时,需添加依赖、配置Redis参数并在映射文件中指定缓存类型。适用于查询为主的场景,减少增删改操作,适合单表操作且表间关联较少的业务。
236 6
|
SQL Java 数据库连接
Mybatis架构原理和机制,图文详解版,超详细!
MyBatis 是 Java 生态中非常著名的一款 ORM 框架,在一线互联网大厂中应用广泛,Mybatis已经成为了一个必会框架。本文详细解析了MyBatis的架构原理与机制,帮助读者全面提升对MyBatis的理解和应用能力。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Mybatis架构原理和机制,图文详解版,超详细!
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
492 4
|
缓存 Java 数据库连接
MyBatis缓存机制
MyBatis提供两级缓存机制:一级缓存(Local Cache)默认开启,作用范围为SqlSession,重复查询时直接从缓存读取;二级缓存(Second Level Cache)需手动开启,作用于Mapper级别,支持跨SqlSession共享数据,减少数据库访问,提升性能。
239 1