mybatis源码简单阅读


mybatis源码简单阅读

环境准备

配置文件

数据库的不是关键,随便配一个就好

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="db.properties"/>
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <package name="cn.cimoc.mapper"/>
    </mappers>
</configuration>

db.properties

url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&rewriteBatchedStatements=true&useSsl=false
username=root
password=root
driver=com.mysql.cj.jdbc.Driver

log4j.properties

# 设置根 #
log4j.rootLogger = debug, stdout

# 输出信息到控制台 #
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%p] %d{HH:mm:ss} %l: %n%m%n

Maven依赖

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.32</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.32</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.30</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
</dependency>

测试类

@Slf4j
public class MyBatisTest {

    SqlSession session1;

    SqlSession session2;

    @Before
    public void sessionInit() {
        try {
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
            session1 = factory.openSession();
            session2 = factory.openSession();
            log.debug("sqlSession创建成功");
        } catch (IOException e) {
            log.error("加载配置文件出错:", e);
        }
    }
}
@Mapper
public interface UserInfoMapper {
    UserInfo selectById(int id);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.cimoc.mapper.UserInfoMapper">
    <!--cache标签开启二级缓存-->
    <cache/>
    <select id="selectById" resultType="cn.cimoc.pojo.UserInfo">
        SELECT * FROM user_info WHERE id = #{id}
    </select>
</mapper>

构造 Factory

在测试类的 @Before 方法中,我们先读取了配置文件,并用这个配置创建了一个工厂,核心方法是 build12行

点进去走到深处,可以看到以下代码,对配置文件进行了解析

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    SqlSessionFactory var5;
    try {
        // 首先解析XML配置文件
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // parser解析后获得Configuration对象,并创建一个DefaultSqlSessionFactory
        var5 = this.build(parser.parse());
    } catch (Exception var14) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
    } finally {
        ErrorContext.instance().reset();
        try {
            inputStream.close();
        } catch (IOException var13) {

        }
    }
    return var5;	
}

创建 SqlSession

SqlSession 可以通过 SqlSessionFactory 的 openSession 方法创建

public interface SqlSessionFactory {
    SqlSession openSession();
    SqlSession openSession(boolean autoCommit);
    SqlSession openSession(Connection connection);
    SqlSession openSession(TransactionIsolationLevel level);
    SqlSession openSession(ExecutorType execType);
    SqlSession openSession(ExecutorType execType, boolean autoCommit);
    SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
    SqlSession openSession(ExecutorType execType, Connection connection);
    Configuration getConfiguration();
}

其中有非常多的参数类型,先从最简单的无参开始

构造 Factory中,创建的是一个 DefaultSqlSessionFactory,所以我们进入这个工厂类查看源码

1. 无参

@Override
public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

继续进入下一个方法

该方法有三个参数,分别为执行器类型、事务隔离级别、是否自动提交,而由于上面我们使用的是无参 openSessioin,所以这三个参数都使用了默认值

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

上面的两段代码都出现了一个 configuration 属性

这个 configuration 在 构造 Factory 中已经提到过了,是解析XML配置文件后生成的配置类

接下来开始看这一大段代码

  • 第 4 行,从配置中获取了 Environment 对象,这个对象与建议数据库连接紧密相关,其内部包含一个 DataSource 对象
  • 第 5 行,根据环境获取到事务工厂
  • 第 6 行,使用工厂创建事务对象
  • 第 7 行,创建一个执行器对象,用来执行 Sql
  • 第 8 行,构造一个默认的 SqlSession 实现类并返回

这里研究的是无参 openSession,所以那些方法参数先不做解释。不过可以很清楚的看出,一个 Sqlsession 需要四样东西:

  1. 配置——Configuration
  2. 事务——Transaction
  3. 执行器——Executor
  4. 是否自动提交

其中,事务被装入了执行器,交由执行器管理

2. 开启自动提交

@Override
public SqlSession openSession(boolean autoCommit) {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
}

仅需要传入一个 true,就可以开启事务的自动提交

熟悉 JDBC 的话,你应该知道 Connection 中可以设置是否自动提交

void setAutoCommit(boolean autoCommit) throws SQLException;

mybatis 也只是起一个中介的作用,并不是自己来提交,而是帮我们开启 Connection 的自动提交:

SqlSession 的创建过程

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        // 创建事务,传入了autoCommit参数
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

事务的创建过程

public class JdbcTransactionFactory implements TransactionFactory {

  @Override
  public Transaction newTransaction(Connection conn) {
    return new JdbcTransaction(conn);
  }

  @Override
  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
    // 创建事务
    return new JdbcTransaction(ds, level, autoCommit);
  }
}

JdbcTransaction

我们看看里面是怎么创建数据库连接 Connection 的

protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
        log.debug("Opening JDBC Connection");
    }
    // 从数据源对象获取连接
    connection = dataSource.getConnection();
    if (level != null) {
        // 设置事务等级
        connection.setTransactionIsolation(level.getLevel());
    }
    // 设置自动提交
    setDesiredAutoCommit(autoCommit);
}
protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
    try {
        if (connection.getAutoCommit() != desiredAutoCommit) {
            if (log.isDebugEnabled()) {
                log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");
            }
            // 这里就成功设置了 JDBC 的自动提交
            connection.setAutoCommit(desiredAutoCommit);
        }
    } catch (SQLException e) {
        // Only a very poorly implemented driver would fail here,
        // and there's not much we can do about that.
        throw new TransactionException("Error configuring AutoCommit.  "
                                       + "Your driver may not support getAutoCommit() or setAutoCommit(). "
                                       + "Requested setting: " + desiredAutoCommit + ".  Cause: " + e, e);
    }
}

3. 指定事务等级

@Override
public SqlSession openSession(TransactionIsolationLevel level) {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), level, false);
}

TransactionIsolationLevel 是一个枚举类,包含了数据库的事务等级

含义
NONE表示不开启事务
READ_COMMITTED读已提交
READ_UNCOMMITTED读未提交
REPEATABLE_READ重复读
SERIALIZABLE序列化
SQL_SERVER_SNAPSHOTSQL Server的非标准级别

4. 指定执行器

@Override
public SqlSession openSession(ExecutorType execType) {
    return openSessionFromDataSource(execType, null, false);
}

ExecutorType 是一个枚举类,包含了三种执行器:SIMPLEREUSEBATCH

执行器的创建是在下面这一行代码

final Executor executor = configuration.newExecutor(tx, execType);

点进去可以看到,内部是根据我们传递的枚举类型,创建对应的执行器,通常我们用的都是 SimpleExecutor 所以如果我们不显式指定的话,默认就是这个执行器,详见 Configuration 类的 getDefaultExecutorType 方法

如果在配置中开启了二级缓存,那么执行器还会被包装进一个 CachingExecutor,关于二级缓存,我们后面再讲

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    // 创建执行器
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    // 二级缓存
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    // 自定义插件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

获取 Mapper

创建完 SqlSession 后,就可以通过它获取 Mapper 对象

@Override
public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
}

往代码深处走,会进入一个 MapperRegistry,而其中是通过代理工厂获取到示例的。

Mybatis 之所以可以只写接口就能调用方法,正是因为其使用了动态代理

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        // 通过代理工厂获取到示例
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}

继续往深处走,可以找到代理类 MapperProxy

学过动态代理相关知识的话,应该知道动态代理的核心方法就是 invoke

所以我们直接去看代理类的 invoke 方法

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // 如果是继承自Object的方法,那么不进行干预
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            // 否则进入cachedInvoker方法,获取MapperMethodInvoker对象后再执行invoke方法
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
        return MapUtil.computeIfAbsent(methodCache, method, m -> {
            // 如果是接口的默认方法,那么依旧不干预
            if (m.isDefault()) {
                try {
                    if (privateLookupInMethod == null) {
                        return new DefaultMethodInvoker(getMethodHandleJava8(method));
                    } else {
                        return new DefaultMethodInvoker(getMethodHandleJava9(method));
                    }
                } catch (IllegalAccessException | InstantiationException | InvocationTargetException
                         | NoSuchMethodException e) {
                    throw new RuntimeException(e);
                }
            } else {
                // 否则就是需要代理的方法了,接下来去查看PlainMethodInvoker类
                return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
            }
        });
    } catch (RuntimeException re) {
        Throwable cause = re.getCause();
        throw cause == null ? re : cause;
    }
}

PlainMethodInvoker 的逻辑很简单,这里直接就开始执行我们写在 Mapper 里的方法了

@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    return mapperMethod.execute(sqlSession, args);
}

执行方法

execute 方法比较长,这里就不贴代码了,里面的逻辑清晰,根据方法的类型(增、删、改、查)执行 sqlSession 里对应的方法

其中增、删、改非常简短,毕竟这三类业务本身就不复杂。如果你继续深究,会发现 sqlSession 里面的 insert、delete 最终调用的都是 update

那么废话不多说,我们来看查询的逻辑

查询就复杂多了,毕竟查询的返回类型可以有很多种,所以 execute 中进行了大量的 if 判断,好吧其实也没几句

不同的返回类型,就有不同的查询代码,但其实最终都需要用到 sqlSession 的 selectList 方法,哪怕你只查一条数据,selectOne 最终也是调用 selectList

selectList

进过一堆调用链,找到最底下的代码,发现核心方法是执行器的 query

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 核心方法
        return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

到这里你会发现,有两个类实现了 query 方法,分别是 BaseExecutorCachingExecutor

BaseExecutor 是一个抽象类,上面提到过的 SimpleExecutorReuseExecutorBatchExecutor 都继承自它

CachingExecutor 则是他们的包装类

所以我们先看 BaseExecutor 里的 query 方法

1. BaseExecutor#query

在此之前,先简单介绍一下 MappedStatement,简称 ms :这个类对应 xml 中的一整个 sql 标签(例如 <select>里面是你写的sql</select>),每一个 Mapper 的 MappedStatement 都是单例的

继续进入 query 方法

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取sql信息
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 创建缓存key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@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.");
    }
    // isFlushCacheRequired用来控制缓存的刷新,例如增删改导致了数据不同步
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // localCache是BaseExecutor的内置缓存,也就是mybatis的一级缓存
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            // 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();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
        }
    }
    return list;
}

2. CachingExecutor#query

CachingExecutorBaseExecutor 的包装类,内部包含了真实的执行器对象:

private final Executor delegate;

在执行 query 方法时,会先从 CachingExecutor 的缓存中查询,没有结果才会去调用 delegate 的 query 方法

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
    // 获取 ms 的Cache,上面说过 ms 是单例的,所以即便是不同的 sqlSession,这一行获取到的都是同一个 Cache,也就是mybatis的二级缓存
    Cache cache = ms.getCache();
    // 如果获取到了缓存对象,这里还没开始获取缓存
    if (cache != null) {
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            // tcm 是对 Cache 的一层包装,这里从tcm 中查询是否有缓存
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);
            // 没有缓存则调用 BaseExecutor 的 query 进行查询
            if (list == null) {
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 查询完写入缓存,tcm 虽然是对 Cache 的包装,但是最终也会写入 Cache
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    // 没获取到缓存对象则调用 BaseExecutor 的 query
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

上面提到了 Cache 的包装 tcm

我们进去看一看

TransactionalCacheManager

public class TransactionalCacheManager {
	/** 
	 * TransactionalCache 是 Cache 的实现类(也可以说是包装类),在这里都是临时的缓存,与 Cache 内部的数据无关
	 * 可以理解最底下的方法 getTransactionalCache
	 * 所以 tcm 的 get、put 都是在对临时缓存做操作
	 * 既然是临时缓存,那么是怎么实现共享的呢,这就要看 commit 方法了,这个 commit 可不是提交到数据库,而是提交到缓存
	 */
    private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

    public void clear(Cache cache) {
        getTransactionalCache(cache).clear();
    }

    public Object getObject(Cache cache, CacheKey key) {
        return getTransactionalCache(cache).getObject(key);
    }

    public void putObject(Cache cache, CacheKey key, Object value) {
        getTransactionalCache(cache).putObject(key, value);
    }

    /**
     * commit 方法调用了所有 TransactionalCache 实例的 commit
     * 接下来就去看 TransactionalCache
     */
    public void commit() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.commit();
        }
    }

    public void rollback() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.rollback();
        }
    }

    private TransactionalCache getTransactionalCache(Cache cache) {
        return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
    }

}

TransactionalCache

先看构造方法

public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
}

构造方法将一个 Cache 包装进来,从上面的 tcm 我们也可以看到,TransactionalCache 在 new 的时候传入的就是 ms 的 Cache 对象

那么接下来你应该能猜到数据是怎么写回 Cache 了吧

public void commit() {
    if (clearOnCommit) {
        delegate.clear();
    }
    flushPendingEntries();
    reset();
}

/** 
 * 这个方法将所有需要提交的缓存写入 Cache,这个 Cache 就是 ms 中的 Cache
 * 而 ms 是单例的,所以不同 SqlSession 都能共享到这个二级缓存 Cache
 */
private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}

看到这里,你可能有个疑问,二级缓存需要我们手动 commit 吗,开启 autoCommit 是否会自动提交?

回想一下,数据库的自动提交是怎么实现的?其实是交由 jdbc 来实现的,mybatis 只是进行了一个判断而已

显然 jdbc 和缓存搭不上边,那么缓存怎么能自动提交呢,除非 mybatis 来实现这个机制,那么我们就去源码看看吧

我们来看 DefaultSqlSession 的 commit 方法

@Override
public void commit(boolean force) {
    try {
        // 调用执行器的 commit,并且传入了一个 boolean 参数,我们看看这个参数是怎么获取的
        executor.commit(isCommitOrRollbackRequired(force));
        dirty = false;
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}
/** 
 * 满足以下任意条件
 * 1. 没有开启自动提交并且进行过增/删/改
 * 2. 手动强制提交
 * 那么返回 true
 * 否则返回 false
 * 这么看起来可能比较抽象,结合 commit 可以知道,不考虑强制提交的情况:
 * 如果开启了 autocommit,不强制提交的话,永远是 false,也就是不给提交
 * 没开的话,即使我们想提交,如果没进行过增/删/改,也是 false
 */
private boolean isCommitOrRollbackRequired(boolean force) {
    return (!autoCommit && dirty) || force;
}

那么这个 boolean 值在后面能起到什么作用呢

在 BaseExecutor#commit 中

// 如果为true才让jdbc进行提交
if (required) {
    transaction.commit();
}

在 CachingExecutor#commit 中,委托给 BaseExecutor#commit

delegate.commit(required);

关键的地方来了

CachingExecutor#commit 总共两行代码,一行是上面那句代码,提交数据库,另一行就是关于缓存的提交了

tcm.commit();

注意:tcm 的提交并没有用到 require,require 是唯一和 autoCommit 有关的值

看到这,即使不看下去你也能知道了,autoCommit 都与 tcm 无关了,自然不能自动提交

不信的话,继续点进这个 commit 方法,就回到上面的 tcm 源码了,里面并没有任何与自动这个概念相关的代码

3. 总结

  1. 在进行查询的时候,如果开启了二级缓存,那么会先调用 CachingExecutor

    如果二级缓存里没查到数据,才会调用 BaseExecutor,查一级缓存

    如果一级缓存也没数据,才会对数据库进行查询,并且写入一级和二级缓存

flowchart TB
    A[查询]-->B["二级缓存(如果开启)"]
    B-->C[一级缓存]
    C-->D[数据库]

一级和二级代表里数据库的距离,距离越远,才会越先用到

而不是字面意义上的一级大于二级

测试

同个 SqlSession

不提交

@Test
public void test1() {
    try {
        UserInfoMapper mapper = session1.getMapper(UserInfoMapper.class);
        mapper.selectById(1);
        mapper.selectById(1);
    } catch (Exception e) {
        log.error("", e);
    }
}
[DEBUG] 09:19:25 MyBatisTest.sessionInit(MyBatisTest.java:32): 
sqlSession创建成功
[DEBUG] 09:19:25 org.apache.ibatis.cache.decorators.LoggingCache.getObject(LoggingCache.java:60): 
Cache Hit Ratio [cn.cimoc.mapper.UserInfoMapper]: 0.0
[DEBUG] 09:19:25 org.apache.ibatis.transaction.jdbc.JdbcTransaction.openConnection(JdbcTransaction.java:137): 
Opening JDBC Connection
[DEBUG] 09:19:27 org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:434): 
Created connection 1116094714.
[DEBUG] 09:19:27 org.apache.ibatis.transaction.jdbc.JdbcTransaction.setDesiredAutoCommit(JdbcTransaction.java:101): 
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@428640fa]
[DEBUG] 09:19:27 org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): 
==>  Preparing: SELECT * FROM user_info WHERE id = ?
[DEBUG] 09:19:27 org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): 
==> Parameters: 1(Integer)
[DEBUG] 09:19:27 org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): 
<==      Total: 1
[DEBUG] 09:19:27 org.apache.ibatis.cache.decorators.LoggingCache.getObject(LoggingCache.java:60): 
Cache Hit Ratio [cn.cimoc.mapper.UserInfoMapper]: 0.0

Process finished with exit code 0

虽然测试代码中我们调用了两次 selectById ,但是只对数据库进行了一次查询

在最下面有一句

Cache Hit Ratio [cn.cimoc.mapper.UserInfoMapper]: 0.0

这是二级缓存的命中率,0 就代表二级缓存中没有这条数据

所以这里走的是一级缓存

提交

@Test
public void test2() {
    try {
        UserInfoMapper mapper = session1.getMapper(UserInfoMapper.class);
        mapper.selectById(1);
        session1.commit();
        mapper.selectById(1);
    } catch (Exception e) {
        log.error("", e);
    }
}
[DEBUG] 09:22:32 MyBatisTest.sessionInit(MyBatisTest.java:32): 
sqlSession创建成功
[DEBUG] 09:22:32 org.apache.ibatis.cache.decorators.LoggingCache.getObject(LoggingCache.java:60): 
Cache Hit Ratio [cn.cimoc.mapper.UserInfoMapper]: 0.0
[DEBUG] 09:22:32 org.apache.ibatis.transaction.jdbc.JdbcTransaction.openConnection(JdbcTransaction.java:137): 
Opening JDBC Connection
[DEBUG] 09:22:34 org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:434): 
Created connection 1116094714.
[DEBUG] 09:22:34 org.apache.ibatis.transaction.jdbc.JdbcTransaction.setDesiredAutoCommit(JdbcTransaction.java:101): 
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@428640fa]
[DEBUG] 09:22:34 org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): 
==>  Preparing: SELECT * FROM user_info WHERE id = ?
[DEBUG] 09:22:34 org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): 
==> Parameters: 1(Integer)
[DEBUG] 09:22:34 org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): 
<==      Total: 1
[DEBUG] 09:22:34 org.apache.ibatis.cache.decorators.LoggingCache.getObject(LoggingCache.java:60): 
Cache Hit Ratio [cn.cimoc.mapper.UserInfoMapper]: 0.5

Process finished with exit code 0

同样,在第一次查询提交后,第二次查询也没有走数据库

这次的命中率是 0.5,所以走的是二级缓存

现在已经证实了上面所说的,二级缓存需要手动提交,接下来测试不同 sqlSession 共享二级缓存

不同 SqlSession

@Test
public void test3() {
    try {
        UserInfoMapper mapper1 = session1.getMapper(UserInfoMapper.class);
        UserInfoMapper mapper2 = session2.getMapper(UserInfoMapper.class);
        mapper1.selectById(1);
        session1.commit();
        mapper2.selectById(1);
    } catch (Exception e) {
        log.error("", e);
    }
}
[DEBUG] 09:24:58 MyBatisTest.sessionInit(MyBatisTest.java:32): 
sqlSession创建成功
[DEBUG] 09:24:58 org.apache.ibatis.cache.decorators.LoggingCache.getObject(LoggingCache.java:60): 
Cache Hit Ratio [cn.cimoc.mapper.UserInfoMapper]: 0.0
[DEBUG] 09:24:58 org.apache.ibatis.transaction.jdbc.JdbcTransaction.openConnection(JdbcTransaction.java:137): 
Opening JDBC Connection
[DEBUG] 09:25:00 org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:434): 
Created connection 1116094714.
[DEBUG] 09:25:00 org.apache.ibatis.transaction.jdbc.JdbcTransaction.setDesiredAutoCommit(JdbcTransaction.java:101): 
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@428640fa]
[DEBUG] 09:25:00 org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): 
==>  Preparing: SELECT * FROM user_info WHERE id = ?
[DEBUG] 09:25:00 org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): 
==> Parameters: 1(Integer)
[DEBUG] 09:25:00 org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): 
<==      Total: 1
[DEBUG] 09:25:00 org.apache.ibatis.cache.decorators.LoggingCache.getObject(LoggingCache.java:60): 
Cache Hit Ratio [cn.cimoc.mapper.UserInfoMapper]: 0.5

Process finished with exit code 0

依旧是只走了一次数据库,说明二级缓存确实是可以跨 SqlSession 共享的


文章作者: ❤纱雾
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ❤纱雾 !
评论
  目录