Mybatis

缓存与Executor

一级缓存

一级缓存是sqlSession级别的,默认开启,在BaseExecutor中实现,其具体实现为key-value结构的HashMap。

一级缓存命中条件
1、查询sql和参数值必须相同;
2、查询的StatementID必须相同;
查询的StatementID指的是查询函数的全路径,例如:com.xsh.webstudy.mybatis.mapper.UserMapper.queryAll�。如果是同一个mapper中的不同方法或者是不同mapper类中的方法都会造成StatementID的不同。
3、必须是同一个SqlSession会话;
一级缓存本质上就是会话级别的缓存,与一个SqlSession的生命周期相同,如果两个相同的查询是在不同的SqlSession中则不会命中一记缓存。
4、RowBounds返回行范围必须相同;
RowBounds本质上就是给查询sql增加分页,所以分页的limit行为也会影响到查询的sql,如果RowBounds不同则不能命中一级缓存。通过SqlSession查询的时候可以手动设置RowBounds的值,默认情况下是不分页,值为RowBounds.DEFAULT,如下:
image.png
一级缓存失效条件
1、手动清除一级缓存,例如使用sqlSession.clearCache�()或者设置flushCache=true;
2、sqlSession中进行了update操作,如果在会话过程中进行了任意的update操作都会清空一级缓存。

一级缓存源码解析
1、query()
image.png
2、一级缓存实现
一级缓存实现类在PerpetualCache�中,底层实现为HashMap:
image.png
3、一级缓存key
一级缓存的key也就是指PerpetualCache中HashMap的key值,只有相同key值的查询才能够从缓存中获取到数据,key值也决定了一级缓存是否能够命中,其包括如下六个内容:
image.png
截图中只有五个内容,原因是因为该sql是没有参数的查询sql,缺少的内容是sql的查询参数值。

二级缓存

二级缓存是跨SqlSession的,需要手动配置开启;
二级缓存也称作为应用级缓存,与一级缓存不同的是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存存在更高的命中率,适合缓存修改较少的数据。

二级缓存命中条件
1、SqlSession必须提交后才会命中;
2、Sql语句相同,查询参数相同;
3、相同的StatementID;
4、RowBounds相同。

二级缓存组件结构分析

1、Cache组件分析
mybatis的二级缓存是永久存在的跨线程的缓存区域,所以因此二级缓存与一级缓存相比复杂度和功能性都有所上升,其要满足线程同步、防溢出、过期清理等等功能。对此二级缓存设计了一个顶级的Cache接口,用于限定缓存的基本功能,例如获取数据、删除数据、保存数据、获取缓存区id和大小等等,源码如下:

package org.apache.ibatis.cache;

import java.util.concurrent.locks.ReadWriteLock;

public interface Cache {

  String getId();

  void putObject(Object key, Object value);

  Object getObject(Object key);

  Object removeObject(Object key);

  void clear();

  int getSize();

  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

Cache接口有多个子类,子类分别实现二级缓存的具体功能,并且采用装饰器和责任链的模式依次进行各个子类功能的运行,完成二级缓存的存储。
image.png
如上图所示,按照所有的实现子类构成了二级缓存的责任链,责任链的上游Chche实现包装了下游实现,构成装饰器。
从debug的方式具体来看Cache的责任链具体实现如下:
image.png
2、为什么二级缓存需要SqlSession提交之后才会命中?
因为mybatis二级缓存是跨线程的缓存区域,如果在某个回话中对数据进行修改,然后进行查询,查询到数据之后,则会将数据结果放置在二级缓存中,此时就可以被其他的会话线程从二级缓存中查询到数据,但是如果初始会话未正常结束发生了回滚,此时其他会话线程已经从二级缓存中获取到了错误的数据,由此可能会产生数据脏读。
所以,二级缓存必须确保线程的SqlSession完成所有任务进行提交之后再把数据写入到二级缓存中。在此,线程会话借助了事务缓存管理器来实现,事务缓存管理器在CachingExecutor中具体使用。
image.png
在某个会话中查询了数据并不会直接放在二级缓存区中,而是放在线程内部私有的事务缓存管理器的暂存区中,待SqlSession提交之后,再把数据暂存区中的数据刷新到对应的缓存区中,由此可以解决多线程之间的脏读问题。事务缓存管理器的具体使用是在CacheingExecutor中,是与会话线程生命周期相同,一一对应的。而事务缓存管理器的暂存区是与使用到的二级缓存缓存区一一对应,所谓的二级缓存缓存区就是指的使用了二级缓存的Mapper。
从debug的形式来查看SqlSession、CachingExecutor、TRM和Cache的关系:
image.png

Executor

从历史的学习可知,在原始jdbc操作数据库的时候,大致分为如下几个步骤:获取数据库链接、预编译sql(获取Statement)、执行查询、读取结果。而在mybatis中,这些功能就由执行器Executor来完成。
Executor结构类图如下:
image.png
其中,Executor是顶层接口,BaseExecutor是其一个抽象实现,在其中实现了一级缓存,SimpleExecutor、ReuseExecutor、BatchExecutor是具体数据库修改、查询功能的实现,CachingExecutor实现了二级缓存。

案例前置代码内容
接下来通过案例来说明不同执行器的特点,在此之前需要创建一个集成了mybatis的工程,以及准备好mybatis相关的配置文件,主要内容如下:
1、pom依赖

      	<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.1</version>
        </dependency>

2、mybatis-config.xml

<?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>
    <!-- 2.全局配置参数 -->
    <settings>
        <!-- 打开全局缓存开关(二级环境),默认值就是true -->
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <!-- 和Spring整合后environments配置将被废除 -->
    <environments default="development">
        <environment id="development">
            <!-- 使用JDBC事务管理 -->
            <transactionManager type="JDBC"/>
            <!-- 数据库连接池 -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/my_db"/>
                <property name="username" value="root"/>
                <property name="password" value="rootroot"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 8.加载映射文件 -->
    <mappers>
        <mapper resource="com/xsh/webstudy/mybatis/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

3、初始化mybatis案例的前置对象(包括读取配置文件、创建数据库链接、创建jdbc事务)

    public static final String URL = "jdbc:mysql://localhost:3306/my_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
    public static final String USERNAME = "root";
    public static final String PASSWORD = "rootroot";

    private Configuration configuration;
    private Connection connection;
    private JdbcTransaction jdbcTransaction;

    @Before
    public void init() throws SQLException {
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory build = sqlSessionFactoryBuilder.build(MybatisTest.class.getResourceAsStream("/mybatis-config.xml"));
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        jdbcTransaction = new JdbcTransaction(connection);
    }

SimpleExecutor

SimpleExecutor是一个基础的执行器,无缓存实现,执行多少次sql语句就进行多少次sql编译装载。

    /**
     * 简单执行器
     * @throws SQLException
     */
    @Test
    public void simpleTest() throws SQLException {
        SimpleExecutor simpleExecutor = new SimpleExecutor(configuration, jdbcTransaction);
        MappedStatement mappedStatement = configuration.getMappedStatement("com.xsh.webstudy.mybatis.mapper.UserMapper.queryAll");
        List<Object> objects = simpleExecutor.doQuery(mappedStatement,
                null,
                RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER,
                mappedStatement.getBoundSql(null));
        simpleExecutor.doQuery(mappedStatement,
                null,
                RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER,
                mappedStatement.getBoundSql(null));
        System.out.println(objects);
    }

案例中执行器调用了使用同样的sql语句调用了两次相同的duQuery(),仍然会进行两次相同的sql语句编译,如下:
image.png

ReuseExecutor�

ReuseExecutor�是一个可重用的执行器,可重用指的是相同的sql只进行一次编译装载并重复使用。

    /**
     * 可重用处理器
     * @throws SQLException
     */
    @Test
    public void reuseTest() throws SQLException {
        ReuseExecutor reuseExecutor = new ReuseExecutor(configuration, jdbcTransaction);
        MappedStatement mappedStatement = configuration.getMappedStatement("com.xsh.webstudy.mybatis.mapper.UserMapper.queryAll");
        List<Object> objects = reuseExecutor.doQuery(mappedStatement,
                null,
                RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER,
                mappedStatement.getBoundSql(null));
        reuseExecutor.doQuery(mappedStatement,
                null,
                RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER,
                mappedStatement.getBoundSql(null));
        System.out.println(objects);
    }
同样的代码ReuseExecutor只会编译一次sql:

image.png

BatchExecutor�

BatchExecutor�表示批处理,只针对update操作有效,目的是先将多个update操作预处理,然后再一次性写入数据库:

/**
     * 批处理处理器
     * 只针对update操作
     * @throws SQLException
     */
    @Test
    public void batchTest() throws SQLException {
        BatchExecutor batchExecutor = new BatchExecutor(configuration, jdbcTransaction);
        MappedStatement mappedStatement = configuration.getMappedStatement("com.xsh.webstudy.mybatis.mapper.UserMapper.updateNameById");

        // 准备修改入参(map形式)
        Map<String, Object> params = new HashMap<>();
        params.put("id", 1);
        params.put("name", "222");
        batchExecutor.doUpdate(mappedStatement, params);

        Map<String, Object> paramsT = new HashMap<>();
        paramsT.put("id", 2);
        paramsT.put("name", "1111");
        batchExecutor.doUpdate(mappedStatement, paramsT);

        /**
         * doUpdate()并不会修改数据库,需要调用doFlushStatements()写入数据库
         * doUpdate()负责组装sql信息,由doFlushStatements()一次性将多个sql批量执行
         */
        batchExecutor.doFlushStatements(false);
    }

BaseExecutor

BaseExecutor是上述三个执行器的抽象父类,在其中实现了一级缓存。
BaseExecutor中同子类的doQuery()相比有一个query(),查询数据库的时候先通过query()查询一级缓存是否有目标内容,如果没有则调用子类实现的duQuery()查询数据库,案例代码debug流程如下:

    /**
     * base执行器
     * @throws SQLException
     */
    @Test
    public void baseTest() throws SQLException {
        BaseExecutor baseExecutor = new SimpleExecutor(configuration, jdbcTransaction);
        MappedStatement mappedStatement = configuration.getMappedStatement("com.xsh.webstudy.mybatis.mapper.UserMapper.queryAll");
        List<Object> objects = baseExecutor.query(mappedStatement,
                null,
                RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER);
        baseExecutor.query(mappedStatement,
                null,
                RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER);
        System.out.println(objects);
    }

image.png
image.png
image.png

CachingExecutor�

CachingExecutor�是一个实现了二级缓存的执行器,mybatis针对二级缓存单独进行了实现。CachingExecutor�中有一个delegate�属性,表示为下一个执行器,如果查询没有命中二级缓存,则会调用delegate�属性对应的执行器进行下面的查询。在此可以看作是一个装饰者模式,CachingExecutor的二级缓存行为是针对delegate的一个包装装饰增强。
所以,正常的查询流程是先通过CachingExecutor进行二级缓存查询,然后通过BaseExecutor的query()进行一级缓存的查询,然后再通过子类执行器进行数据库查询。

	/**
     * caching执行器
     * @throws SQLException
     */
    @Test
    public void cachingTest() throws SQLException {
        MappedStatement mappedStatement = configuration.getMappedStatement("com.xsh.webstudy.mybatis.mapper.UserMapper.queryAll");

        BaseExecutor baseExecutor = new SimpleExecutor(configuration, jdbcTransaction);
        CachingExecutor cachingExecutor = new CachingExecutor(baseExecutor);

        List<Object> objects = cachingExecutor.query(mappedStatement,
                null,
                RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER);
        
        // 提交commit二级缓存才会生效
        cachingExecutor.commit(true);
        cachingExecutor.query(mappedStatement,
                null,
                RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER);
        cachingExecutor.query(mappedStatement,
                null,
                RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER);
        cachingExecutor.query(mappedStatement,
                null,
                RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER);
        System.out.println(objects);
    }

上述案例的执行日志输出中就可以判断是否使用二级缓存,在commit之后,同样sql的查询次数越多,命中二级缓存的概率越高,也就是如下截图的Cache Hit Ratio部分:
image.png
CachingExecutor query() debug过程:
image.png
image.png

StatementHandler

JDBC处理器,基于JDBC构建Statement,然后执行SQL。每次调用会话中的一次SQL,都会有与之对应的且唯一的Statement实例。
Statement在mybatis的执行流程中是位于Executor之后, 在执行器执行完毕之后就会创建Statement然后执行JDBC对应的操作。
StatementHandler源码

/*
 *    Copyright 2009-2021 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.executor.statement;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.session.ResultHandler;

/**
 * @author Clinton Begin
 */
public interface StatementHandler {

    // 	创建Statement
  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;
    // 预处理参数
  void parameterize(Statement statement)
      throws SQLException;
    // 批处理
  void batch(Statement statement)
      throws SQLException;
	// 修改
  int update(Statement statement)
      throws SQLException;
	// 查询
  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;
	// 查询游标
  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;
	// 获取动态SQL
  BoundSql getBoundSql();
	// 获取参数处理器
  ParameterHandler getParameterHandler();

}
StatementHandler共有五个子类实现,用于获取不同类型的Statement。其中有一个特殊的BaseStatementHandler抽象实现,用于创建剩余四个子类实现的公共功能,如下:

image.png

Statement执行流程

执行流程时序图
image.png
1、创建Statement
由于Statement行为是位于Executor之后,也就是在具体的doQuery()中触发,所以如果会话的查询数据命中了缓存,则不会有对应的Statement创建和执行行为。
首先,是明确创建Statement的操作由Executor的doQuery()触发,源码如下:
image.png
然后,在configuration.newStatementHandler中实质上是通过RoutingStatementHandle来完成Statement创建:
image.png
RoutingStatementHandler有且仅有一个功能,就是在构造器中通过statementType来决定创建何种Statement实现:
image.png
而statementType这个类型可以在mapper接口中通过@Options�注解来设置StatementType参数,其取值为枚举常量:STATEMENT、PREPARED、CALLABLE�,默认为PREPARED。
在RoutingStatementHandler中获取到StatementHandler的实现子类之后,doQuery()会调用prepareStatement�()来执行对应StatenmentHandler的prepare�()创建Statement,首先进入的应该是BaseStatementHandler。
创建Stetment的操作主要是通过instantiateStatement�()来完成,在BaseStatementHandler中该方法为抽象方法,具体实现由其他三个子类来完成,除了创建Statement之外,BaseStatementHandler的prepare()还可以完成设置超时时间和公共参数的功能:
image.png
最终,将会走到SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler的instantiateStatement()中:
image.png
2、填充SQL参数
Executor的doQuery()获取到Statement之后,就会在prepareStatement�()中通过StatementHandler的parameterize��()来设置参数。此时在parameterize()就会涉及到设置参数的处理器:parameterHandler�,parameterHandler�是作为StaementHandler的成员变量存在,并调用parameterHandler�的setParameters�()完成参数装填:
image.png
image.png
3、执行SQL并处理响应结果
填充完SQL参数之后,doQuery()就会调用StatementHandler的query()来执行SQL,在query()中会通过execute�()来执行SQL,并通过resultSetHandler�来处理响应结果:
image.png

posted @ 2023-01-11 10:17  原野上找一面墙  阅读(57)  评论(0编辑  收藏  举报