前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >mybatis 核心原理 线程隔离

mybatis 核心原理 线程隔离

作者头像
平凡的学生族
发布2020-01-02 17:40:47
8640
发布2020-01-02 17:40:47
举报
文章被收录于专栏:后端技术

建议同时学习@Transaction, spring对事务的管理

spring @Transactional原理

序言

如果你只是阅读源码,只是为了“背诵”、“说出”它的过程,却不知道其原理,那过后就忘记了,你也不会明白源码的真正结构。

一句话总结

mybatis, 连接池与spring的事务管理配合, 共同使原本复杂的基于多句jdbc的调用, 变成只需对Mapper的一句调用即可完成. 它们是对jdbc的封装.

  • spring的事务管理简化了事务的开启与提交, 还有连接从连接池获取和归还的步骤
  • SqlSessionTemplate是单例, 封装了SqlSession的创建与释放, 还有在事务方法(@Transactional)中SqlSession的维持与再获取.

Mybatis在@Transactional中的精髓在于利用TransactionSynchronizationManager的ThreadLocal变量(resources)维持各个线程的DefaultSqlSession

在你调用Mapper方法时, 其实你调用的是实现了Mapper接口的代理对象, 该对象会将调用都转交给SqlSessionTemplate单例.

所以各个线程同时调用同一个Mapper时, 实际上SqlSessionTemplate也被同时调用了, 这就要求SqlSessionTemplate是线程安全的.

SqlSessionTemplate隐藏了DefaultSqlSession创建/释放/维持在ThreadLocal的细节.这样在同一个@Transactional下的多个Mapper调用中:

  • DefaultSqlSession在第一次Mapper调用中被创建, 并被存在ThreadLocal
  • 在第二次Mapper调用中, DefaultSqlSession又被从ThreadLocal中取出来

我们记得mybatis一级缓存是会话级别的, 由于在@Transactional下的多个Mapper调用实际用的是同一个DefaultSqlSession(也是同一个线程执行的, 因为线程是一路往下执行的), 所以这个一级缓存也会得以发挥作用.

请求的传递

在需要Mapper对象的Bean时, MapperFactoryBean被调用getObject并创建对应Mapper的代理对象.

  • 该代理对象会将接口请求转交给MapperMethod执行
  • MapperMethod会将数组形式的参数包装为单个参数(null/变量/Map), 然后交给sqlSessionTemplate执行(该变量全局唯一). 并从后者接收单个的返回值.
    • MapperMethod的作用在于将参数数组包装为一个变量
  • SqlSessionTemplate实现了SqlSession接口, 但是:
    • 对selectOne, selectMap等调用, 转交给sqlSessionProxy变量
    • 而对commit, rollback等调用, 抛出异常
    • SqlSessionTemplate的作用是简化调用方步骤, 调用者不必再提交/回滚事务. 另外一个作用是本身是线程安全的, 全局只有一个单例, 通过SqlSessionInterceptor封装了非线程安全的DefaultSqlSession的创建与释放.
  • sqlSessionProxy代理对象的调用显然都被SqlSessionInterceptor::invoke截获了, 该方法有三步:
    1. 获取sqlSession
    2. 由该sqlSession对象执行
    3. 释放sqlSession
    • SqlSessionInterceptor的目的是让调用方省略了获取/释放sqlSession的步骤, 让外部只需调用SqlSession的接口方法即可.
  • 在有@Transactional的情况下, TransactionInterceptor会被AOP织入方法调用的前后
    • TransactionInterceptor继承了TransactionAspectSupport, invokeWithinTransaction方法中:
      • createTransactionIfNecessary会调用status = tm.getTransaction(txAttr);会一路调用DataSourceTransactionManager::doBegin
        • Connection newCon = obtainDataSource().getConnection()会从连接池取出连接, 并交给TransactionSynchronizationManager维护
        • conn.setAutoCommit(false)开启事务
    • commitTransactionAfterReturning中
      • txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());->processCommit->doCommit会调用con.commit();提交事务
      • BaseExecutor::close->SpringManagedTransaction::close->releaseConnection->doReleaseConnection->doCloseConnection中通过con.close();关闭了连接
    • TransactionInterceptor的作用在于省去了开启连接和事务, 关闭事务的步骤
  • TransactionSynchronizationManager的作用是维护不同线程各自使用的Transaction, DefaultSqlSession和Connection. 线程运行到需要用到的时候, 只需从TransactionSynchronizationManager中取用即可
    • TransactionSynchronizationManager的原理是用ThreadLocal<Map<Object, Object>> resources实现的, 该变量为每一个线程维护一个Map数据结构.

附上下图是关于各个步骤所简化的地方.

2019-12-27 22-46-43 的屏幕截图.png

关于SqlSessionTemplate来源的分析

sqlSessionTemplate是全局共享的变量,所以应当保证线程安全.该变量由SqlSessionDaoSupport提供,

代码语言:javascript
复制
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
      this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
    }
  }

MapperFactoryBean继承了SqlSessionDaoSupport,在需要获取Mapper的Bean时,由父类含有的变量sqlSessionTemplate创建Mapper。

代码语言:javascript
复制
// MapperFactoryBean
@Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

// SqlSessionDaoSupport
public SqlSession getSqlSession() {
    return this.sqlSessionTemplate;
  }

SqlSessionInterceptor与SqlSessionUtils

SqlSessionInterceptor在invoke方法中调用了SqlSessionUtils::getSqlSession来获取SqlSession, 并调用了SqlSessionUtils::closeSqlSession来释放SqlSession.

getSqlSession的签名如下:

代码语言:javascript
复制
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator)

它要么借助TransactionSynchronizationManager来获取管理的SqlSession, 要么直接靠sessionFactory来创建一个SqlSession

  • 似乎在事务下,会借助TransactionSynchronizationManager.getResource维持不同线程调用时使用同一个SqlSession.
  • 不使用事务时, 靠创建一个SqlSession

获取连接

1. 需要获取连接

SimpleExecutor.java中用到了该连接

代码语言:javascript
复制
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}

BaseExecutor中

代码语言:javascript
复制
protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
}

SpringManagedTransaction.java:

代码语言:javascript
复制
@Override
public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
}

debug到这里, connection会为null, 也就是说, SpringManagedTransaction一开始是没有设置Connection的, 但是没关系, 接下来我们调用openConnection设置这个连接. (该对象被构造出来时没设置Connection, 具体可以看第3章)

  • 这一步是比较重要的, 在一个事务中, 用于事务的连接应该从始至终是同一个连接, 这样最后提交commit时的行为才正确.
  • SpringManagedTransaction被BaseExecutor持有, 而BaseExecutor又被TransactionSynchronizationManager所管理, 这样在一个事务中发挥作用的Executor是同一个, 连接也就是同一个了.

2. 从TransactionSynchronizationManager获取连接

SpringManagedTransaction

代码语言:javascript
复制
private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
    ...
}

DataSourceUtils.java

代码语言:javascript
复制
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
    try {
        return doGetConnection(dataSource);
    }
    catch (SQLException ex) {
        ...
    }
}

DataSourceUtils.java

代码语言:javascript
复制
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    Assert.notNull(dataSource, "No DataSource specified");

    ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    ...
}

在此从TransactionSynchronizationManager获取了线程级别的缓存的连接

3. SpringManagedTransaction的创建

在SqlSessionTemplate调用的SqlSessionUtils::getSession中

代码语言:javascript
复制
session = sessionFactory.openSession(executorType);

DefaultSqlSessionFactory.java

代码语言:javascript
复制
@Override
public SqlSession openSession(ExecutorType execType) {
    return openSessionFromDataSource(execType, null, false);
}
代码语言:javascript
复制
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);
      ...
    } catch (Exception e) {
      ...
    }
}

SpringManagedTransactionFactory::newTransaction

代码语言:javascript
复制
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
    return new SpringManagedTransaction(dataSource);
}

SpringManagedTransaction.java

代码语言:javascript
复制
public SpringManagedTransaction(DataSource dataSource) {
    notNull(dataSource, "No DataSource specified");
    this.dataSource = dataSource;
}

可见, SpringManagedTransaction构造出来时并没有设置Connection.

疑惑

代码语言:javascript
复制
private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

中:

  • 假如某个事务有两个mapper方法, 在某个线程A上执行了一个以后, 换到另一个线程B上执行, 而那个线程B的SqlSession刚刚才获取了一个非事务方法的Connection, 会不会导致线程B将原本用于线程A的Connection给commit
  • 又或者, 不需要commit的Connection被执行非事务内的mapper方法的线程拿到, 然后执行sqlSession.commit(true);把它给commit了? 我的解答: 实际上我提了个不是问题的问题. 因为仔细想想, 线程只是会在代码上向前执行, 依次将两个mapper方法执行完毕. 不存在某个mapper方法在一个线程上执行后, 之后的mapper方法又交给另一个线程执行的情况.

总结

  • SqlSessionTemplate是线程安全的, DefaultSqlSession不是线程安全的, 每个线程只需要持有一个DefaultSqlSession即可, mybatis将DefaultSqlSession的线程隔离保管交给spring
  • Transaction由Executor一对一管理, Executor由DefaultSqlSession一对一管理, DefaultSqlSession是在由spring维护在线程中的.
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 序言
  • 一句话总结
  • 请求的传递
  • 关于SqlSessionTemplate来源的分析
  • SqlSessionInterceptor与SqlSessionUtils
  • 获取连接
    • 1. 需要获取连接
    • 2. 从TransactionSynchronizationManager获取连接
    • 3. SpringManagedTransaction的创建
    • 疑惑
    • 总结
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档