前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring 和 Mybatis 使用不同的数据源会怎样?

Spring 和 Mybatis 使用不同的数据源会怎样?

作者头像
书唐瑞
发布2022-06-02 14:55:25
5290
发布2022-06-02 14:55:25
举报
文章被收录于专栏:Netty历险记Netty历险记

本篇文章要讨论的一个问题点, 给Spring和Mybatis设置不同的数据库数据源会怎样?

注意. 正常情况下一定要给Spring和Mybatis设置相同的数据库数据源.

案例代码位置

https://github.com/infuq/spring-framework/tree/main/infuq-t/src/main/java/com/infuq/mybatis

案例代码结构

代码语言:javascript
复制
//AppConfig.java

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@EnableTransactionManagement
@MapperScan("com.infuq.mybatis.mapper")
@ComponentScan
public class AppConfig {


    // 数据源
    @Bean
    public DataSource druidDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test_0?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true");
        dataSource.setUsername("root");
        dataSource.setPassword("9527");

        return dataSource;
    }

    // 数据源
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test_1?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true");
        dataSource.setUsername("root");
        dataSource.setPassword("9527");

        return dataSource;
    }

    // Mybatis需要一个SqlSessionFactory, 因此向容器中注入一个SqlSessionFactory
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean.getObject();

    }

    // 事务管理器, 用于事务管理
    @Bean
    public DataSourceTransactionManager druidTransactionManager(DataSource druidDataSource) {
        return new DataSourceTransactionManager(druidDataSource);
    }

}

通过图形的方式, 描述上面AppConfig.java代码的结构

据库数据源分别设置到SqlSessionFactory和事务管理器. SqlSessionFactory用于Mybatis操作数据库时使用,比如insert,update等. 事务管理器用于Spring开启事务等操作.

代码语言:javascript
复制
// UserServiceImpl.java

import com.infuq.mybatis.mapper.UserMapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.transaction.annotation.Transactional;

public class UserServiceImpl implements UserService {

  @Autowired
  private UserMapper userMapper;
  
  @Transactional(transactionManager = "druidTransactionManager")
  @Override
  public void getList() {
      userMapper.getList();
  }

}

代码中使用了事务管理器, 但使用了select作为案例讲解,并没有使用insert/update作为案例讲解,读者不要太在意.

程序运行之后,看一下,Spring容器中存在的UserServiceImpl实例和UserMapper实例`长啥样`.

在容器中存放的是Service的代理对象, 代理对象中存在真正的被代理对象(即真正的UserServiceImpl实例), 在被代理对象内部, 又有mapper代理对象, mapper代理对象持有sqlSessionFactory对象, sqlSessionFactory持有数据源.

Service的代理对象内部还有一个事务拦截器TransactionInterceptor

在调用链路上,在Service代理对象和Service被代理对象之间, 还有一个事务拦截器会被调用到.

开始运行程序

运行程序之后,首先调用到service代理对象, 在调用到事务拦截器TransactionInterceptor, 就在这个事务拦截器中拿到了容器中的事务管理器TransactionManager, 而这个事务管理器就是我们之前配置的.

代码语言:javascript
复制
//AppConfig.java
@Bean
public DataSourceTransactionManager druidTransactionManager(DataSource druidDataSource) {
    return new DataSourceTransactionManager(druidDataSource);
}

这个事务管理器有一个很重要的事情需要做. 它需要获取一个数据库连接, 并开启事务.

那么这个数据库连接从哪里得到呢?

在配置事务管理器的时候,给它设置了一个数据源, 那么事务管理器就从这个数据源中得到一个数据库连接. 而且它是通过ThreadLocal实现的. 如果一个线程在执行的过程使用了多个数据库数据源, 那么一个数据源对应一条数据库连接的关系会被保存到ThreadLocal中, 保证线程在操作一个数据库的时候只会使用一条相同的数据库连接. 具体实现在

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

代码语言:javascript
复制
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
  DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
  Connection con = null;

  try {
    if (!txObject.hasConnectionHolder() ||
        txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
      // 拿到事务管理器中设置的数据源,并根据这个数据源创建一个数据库连接
      Connection newCon = obtainDataSource().getConnection();
      txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
    }

    txObject.getConnectionHolder().setSynchronizedWithTransaction(true);

    con = txObject.getConnectionHolder().getConnection();

    Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
    txObject.setPreviousIsolationLevel(previousIsolationLevel);
    txObject.setReadOnly(definition.isReadOnly());


    if (con.getAutoCommit()) {
      txObject.setMustRestoreAutoCommit(true);
      // 开启事务
      con.setAutoCommit(false);
    }

    prepareTransactionalConnection(con, definition);
    txObject.getConnectionHolder().setTransactionActive(true);

    int timeout = determineTimeout(definition);
    if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
      txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
    }

    if (txObject.isNewConnectionHolder()) {
        // 将 dataSource -> connection 关系存到ThreadLocal中
      TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
    }
  }

}

总结. Spring会将Service的代理对象放入容器中, 当调用代理对象的方法时, 首先会调用到事务拦截器TransactionInterceptor中,这个事务拦截器会拿到容器中的事务管理器, 事务管理器会根据设置的数据源, 创建一个数据库连接, 并开启事务. 同时也会把数据源->数据库连接保存到ThreadLocal.

接下来看Mybatis层面的代码逻辑.

经过层层调用, Mybatis也需要拿到数据库连接,为接下来的操作数据库. 那么它这个连接是怎么拿到的呢?

Mybatis本来是想从ThreadLocal中拿到一个数据库连接的, 但是Mybatis持有的这个数据源在ThreadLocal中没有对应的数据库连接, 而ThreadLocal中已存在的数据源是在事务管理器的时候放入的, 它们不是同一个数据源.

因此, Mybatis 需要根据自己拿到的数据源自己去创建一个数据库连接了. 并把它也放到ThreadLocal中.

如上图, 由于文章开头, 在配置事务管理器和SqlSessionFactory时,分别设置了不同的数据源, 最终就导致, 事务管理器开启事务的时候, 使用的数据源A创建的一个数据库连接. 而Mybatis在进行实际操作数据库的时候, 使用的数据源B创建的一个数据库连接. 造成了开启事务和进行实际数据库操作的连接不是同一个连接.

因此,在配置的时候,需要将SqlSessionFactory和事务管理器设置成相同的数据源.

代码语言:javascript
复制
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setDataSource(dataSource);
    return factoryBean.getObject();

}

@Bean
public DataSourceTransactionManager druidTransactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

这样的话, mybatis 根据数据源在拿取数据库连接的时候, 发现ThreadLocal中已经有对应数据源的数据库连接了, 因为在事务管理器的时候, 由事务管理器已经把数据源对应的数据库连接放入到ThreadLocal中了.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Netty历险记 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档