Mybatis操作多数据源的实现

目录
  • 1. 注入多数据源
  • 2. 动态数据源
    • (1) 创建并注入动态数据源
    • (2) Mybatis配置类
    • (3) 使用注解简化数据源切换
  • 3. 结语

现在有一个Mysql数据源和一个Postgresql数据源,使用Mybatis对两个数据源进行操作:

1. 注入多数据源

可以对两个数据源分别实现其Service层和Mapper层,以及Mybatis的配置类:

@Configuration
// 这里需要配置扫描包路径,以及sqlSessionTemplateRef
@MapperScan(basePackages = "com.example.mybatisdemo.mapper.mysql", sqlSessionTemplateRef = "mysqlSqlSessionTemplate")
public class MysqlMybatisConfigurer {
    /**
     * 注入Mysql数据源
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    public DataSource mysqlDatasource() {
        return new DruidDataSource();
    }
    /**
     * 注入mysqlSqlSessionFactory
     */
    @Bean
    public SqlSessionFactory mysqlSqlSessionFactory(DataSource mysqlDatasource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(mysqlDatasource);
        // 设置对应的mapper文件
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:" +
                "/mappers/MysqlMapper.xml"));
        return factoryBean.getObject();
    }
    /**
     * 注入mysqlSqlSessionTemplate
     */
    @Bean
    public SqlSessionTemplate mysqlSqlSessionTemplate(SqlSessionFactory mysqlSqlSessionFactory) {
        return new SqlSessionTemplate(mysqlSqlSessionFactory);
    }
    /**
     * 注入mysqlTransactionalManager
     */
    @Bean
    public DataSourceTransactionManager mysqlTransactionalManager(DataSource mysqlDatasource) {
        return new DataSourceTransactionManager(mysqlDatasource);
    }
}
@Configuration
// 这里需要配置扫描包路径,以及sqlSessionTemplateRef
@MapperScan(basePackages = "com.example.mybatisdemo.mapper.postgresql", sqlSessionTemplateRef = "postgresqlSqlSessionTemplate")
public class PostgresqlMybatisConfigurer {
    /**
     * 注入Postgresql数据源
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.postgresql")
    public DataSource postgresqlDatasource() {
        return new DruidDataSource();
    }
    /**
     * 注入postgresqlSqlSessionFactory
     */
    @Bean
    public SqlSessionFactory postgresqlSqlSessionFactory(DataSource postgresqlDatasource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(postgresqlDatasource);
        // 设置对应的mapper文件
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:" +
                "/mappers/PostgresqlMapper.xml"));
        return factoryBean.getObject();
    }
    /**
     * 注入postgresqlSqlSessionTemplate
     */
    @Bean
    public SqlSessionTemplate postgresqlSqlSessionTemplate(SqlSessionFactory postgresqlSqlSessionFactory) {
        return new SqlSessionTemplate(postgresqlSqlSessionFactory);
    }
    /**
     * 注入postgresqlTransactionalManager
     */
    @Bean
    public DataSourceTransactionManager postgresqlTransactionalManager(DataSource postgresqlDatasource) {
        return new DataSourceTransactionManager(postgresqlDatasource);
    }
}

在配置类中,分别注入了一个事务管理器TransactionManager,这个和事务管理是相关的。在使用@Transactional注解时,需要配置其value属性指定对应的事务管理器。

2. 动态数据源

Spring中提供了AbstractRoutingDataSource抽象类,可以用于动态地选择数据源。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map<Object, Object> targetDataSources;
    @Nullable
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    @Nullable
    private DataSource resolvedDefaultDataSource;
    // 略
}

通过源码可以看到,该抽象类实现了InitializingBean接口,并在其afterPropertiesSet方法中将数据源以<lookupkey, dataSource>的形式放入一个Map中。

public void afterPropertiesSet() {
    if (this.targetDataSources == null) {
        throw new IllegalArgumentException("Property 'targetDataSources' is required");
    } else {
        this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = this.resolveSpecifiedLookupKey(key);
            DataSource dataSource = this.resolveSpecifiedDataSource(value);
            // 将数据源以<lookupkey, dataSource>的形式放入Map中
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }
}

该类中还有一个determineTargetDataSource方法,是根据lookupkey从Map中获取对应的数据源,如果没有获取到,则使用默认的数据源。

protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = this.determineCurrentLookupKey();
    // 根据lookupkey从Map中获取对应的数据源
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
        dataSource = this.resolvedDefaultDataSource;
    }
    if (dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    } else {
        return dataSource;
    }
}

lookupkey是通过determineTargetDataSource方法获取到的,而它是一个抽象方法,我们要做的就是通过实现这个方法,来控制获取到的数据源。

@Nullable
protected abstract Object determineCurrentLookupKey();

(1) 创建并注入动态数据源

创建AbstractRoutingDataSource的子类,实现determineCurrentLookupKey方法

public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.get();
    }
}

这里的DataSourceContextHolder是一个操作ThreadLocal对象的工具类

public class DataSourceContextHolder {
    /**
     * 数据源上下文
     */
    private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
    /**
     * 设置数据源类型
     */
    public static void set(DataSourceType type) {
        contextHolder.set(type);
    }
    /**
     * 获取数据源类型
     *
     * @return DataSourceType
     */
    public static DataSourceType get() {
        return contextHolder.get();
    }
    /**
     * 使用MYSQL数据源
     */
    public static void mysql() {
        set(DataSourceType.MYSQL);
    }
    /**
     * 使用Postgresql数据源
     */
    public static void postgresql() {
        set(DataSourceType.POSTGRESQL);
    }
    public static void remove() {
        contextHolder.remove();
    }
}

通过调用DataSourceContextHolder.mysql()或者DataSourceContextHolder.postgresql()就能修改contextHolder的值,从而在动态数据源的determineTargetDataSource方法中就能获取到对应的数据源。

在数据源配置类中,将mysql和postgresql的数据源设置到动态数据源的Map中,并注入容器。

@Configuration
public class DataSourceConfigurer {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    public DataSource mysqlDatasource() {
        return new DruidDataSource();
    }
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.postgresql")
    public DataSource postgresqlDatasource() {
        return new DruidDataSource();
    }
    @Bean
    public RoutingDataSource routingDataSource(DataSource mysqlDatasource, DataSource postgresqlDatasource) {
        Map<Object, Object> dataSources = new HashMap<>();
        dataSources.put(DataSourceType.MYSQL, mysqlDatasource);
        dataSources.put(DataSourceType.POSTGRESQL, postgresqlDatasource);
        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setDefaultTargetDataSource(mysqlDatasource);
        // 设置数据源
        routingDataSource.setTargetDataSources(dataSources);
        return routingDataSource;
    }
}

(2) Mybatis配置类

由于使用了动态数据源,所以只需要编写一个配置类即可。

@Configuration
@MapperScan(basePackages = "com.example.mybatisdemo.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
public class MybatisConfigurer {
    // 注入动态数据源
    @Resource
    private RoutingDataSource routingDataSource;
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(routingDataSource);
        // 这里可以直接设置所有的mapper.xml文件
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath" +
                ":mappers/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
    @Bean
    public DataSourceTransactionManager transactionalManager(DataSource mysqlDatasource) {
        return new DataSourceTransactionManager(mysqlDatasource);
    }
}

(3) 使用注解简化数据源切换

我们虽然可以使用DataSourceContextHolder类中的方法进行动态数据源切换,但是这种方式有些繁琐,不够优雅。可以考虑使用注解的形式简化数据源切换。
我们先定义两个注解,表示使用Mysql数据源或Postgresql数据源:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Mysql {
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Postgresql {
}

再定义一个切面,当使用了注解时,会先调用切换数据源的方法,再执行后续逻辑。

@Component
@Aspect
public class DataSourceAspect {
    @Pointcut("@within(com.example.mybatisdemo.aop.Mysql) || @annotation(com.example.mybatisdemo.aop.Mysql)")
    public void mysqlPointcut() {
    }
    @Pointcut("@within(com.example.mybatisdemo.aop.Postgresql) || @annotation(com.example.mybatisdemo.aop.Postgresql)")
    public void postgresqlPointcut() {
    }
    @Before("mysqlPointcut()")
    public void mysql() {
        DataSourceContextHolder.mysql();
    }
    @Before("postgresqlPointcut()")
    public void postgresql() {
        DataSourceContextHolder.postgresql();
    }
}

在使用动态数据源的事务操作时有两个需要注意的问题:

问题一    同一个事务操作两个数据源

Mybatis使用Executor执行SQL时需要获取连接,BaseExecutor类中的getConnection方法调用了SpringManagedTransaction中的getConnection方法,这里优先从connection字段获取连接,如果connection为空,才会调用openConnection方法,并把连接赋给connection字段。

也就是说,如果你使用的是同一个事务来操作两个数据源,那拿到的都是同一个连接,会导致数据源切换失败。

protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = this.transaction.getConnection();
    return statementLog.isDebugEnabled() ? ConnectionLogger.newInstance(connection, statementLog, this.queryStack) : connection;
}
public Connection getConnection() throws SQLException {
    if (this.connection == null) {
        this.openConnection();
    }
    return this.connection;
}
private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
    LOGGER.debug(() -> {
        return "JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring";
    });
}

问题二      两个独立事务分别操作两个数据源

(1) 在开启事务的时候,DataSourceTransactionManager中的doBegin方法会先获取Connection,并保存到ConnectionHolder中,将数据源和ConnectionHolder的对应关系绑定到TransactionSynchronizationManager中。

protected void doBegin(Object transaction, TransactionDefinition definition) {
    DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction;
    Connection con = null;
    try {
        if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            // 获取连接
            Connection newCon = this.obtainDataSource().getConnection();
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
            }
            // 保存到ConnectionHolder中
            txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
        }
        txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
        // 从ConnectionHolder获取连接
        con = txObject.getConnectionHolder().getConnection();
        // 略
        // 将数据源和ConnectionHolder的关系绑定到TransactionSynchronizationManager中
        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
        }
         // 略
}

(2) TransactionSynchronizationManager的bindResource方法将数据源和ConnectionHolder的对应关系存入线程变量resources中。

public abstract class TransactionSynchronizationManager {
    // 线程变量
    private static final ThreadLocal<Map<Object, Object>> resources =
         new NamedThreadLocal<>("Transactional resources");
    // 略
    // 绑定数据源和ConnectionHolder的对应关系
    public static void bindResource(Object key, Object value) throws IllegalStateException {
       Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
       Assert.notNull(value, "Value must not be null");
       Map<Object, Object> map = resources.get();
       // set ThreadLocal Map if none found
       if (map == null) {
          map = new HashMap<>();
          resources.set(map);
       }
       Object oldValue = map.put(actualKey, value);
       // Transparently suppress a ResourceHolder that was marked as void...
       if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
          oldValue = null;
       }
       if (oldValue != null) {
          throw new IllegalStateException(
                "Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread");
       }
    }
    // 略
}

(3) 上边提到的openConnection方法,其实最终也是从TransactionSynchronizationManager的resources中获取连接的

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    Assert.notNull(dataSource, "No DataSource specified");
    // 获取ConnectionHolder
    ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
    if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
        logger.debug("Fetching JDBC Connection from DataSource");
        Connection con = fetchConnection(dataSource);
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            try {
                ConnectionHolder holderToUse = conHolder;
                if (conHolder == null) {
                    holderToUse = new ConnectionHolder(con);
                } else {
                    conHolder.setConnection(con);
                }
                holderToUse.requested();
                TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
                holderToUse.setSynchronizedWithTransaction(true);
                if (holderToUse != conHolder) {
                    TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
                }
            } catch (RuntimeException var4) {
                releaseConnection(con, dataSource);
                throw var4;
            }
        }
        return con;
    } else {
        conHolder.requested();
        if (!conHolder.hasConnection()) {
            logger.debug("Fetching resumed JDBC Connection from DataSource");
            conHolder.setConnection(fetchConnection(dataSource));
        }
        // 从ConnectionHolder中获取连接
        return conHolder.getConnection();
    }
}

也就是说,如果修改了数据源,那么resources中就找不到对应的连接,就可以重新获取连接,从而达到切换数据源的目的。然而我们数据源的只有一个,就是动态数据源,因此即使使用两个独立事务,也不能成功切换数据源。

3. 结语

如果想要使用动态数据源的事务处理,可能需要考虑使用多线程分布式的事务处理机制;
如果使用直接注入多个数据源的方式实现事务处理,实现简单,但是各数据源事务是独立的;
应该根据具体情况进行选择。

到此这篇关于Mybatis操作多数据源的实现的文章就介绍到这了,更多相关Mybatis 多数据源内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • springboot + mybatis配置多数据源示例

    在实际开发中,我们一个项目可能会用到多个数据库,通常一个数据库对应一个数据源. 代码结构: 简要原理: 1)DatabaseType列出所有的数据源的key---key 2)DatabaseContextHolder是一个线程安全的DatabaseType容器,并提供了向其中设置和获取DatabaseType的方法 3)DynamicDataSource继承AbstractRoutingDataSource并重写其中的方法determineCurrentLookupKey(),在该方法中使用Da

  • Springcloud+Mybatis使用多数据源的四种方式(小结)

    前段时间在做会员中心和中间件系统开发时,多次碰到多数据源配置问题,主要用到分包方式.参数化切换.注解+AOP.动态添加 这四种方式.这里做一下总结,分享下使用心得以及踩过的坑. 分包方式 数据源配置文件 在yml中,配置两个数据源,id分别为master和s1. spring: datasource: master: jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db1?......... username: xxx password: xxx drive

  • Spring Boot 整合mybatis 使用多数据源的实现方法

    前言 本篇教程偏向实战,程序猿直接copy代码加入到自己的项目中做简单的修修改改便可使用,而对于springboot以及mybatis不在此进行展开介绍,如有读者希望了解可以给我留言,并持续关注,我后续会慢慢更新.(黑色区域代码部分,安卓手机可手动向左滑动,来查看全部代码) 整合 其实整合很简单,如果是用gradle的话,在build.gradle文件里加入 compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1')

  • mybatis多数据源动态切换的完整步骤

    笔者主要从事c#开发,近期因为项目需要,搭建了一套spring-cloud微服务框架,集成了eureka服务注册中心. gateway网关过滤.admin服务监控.auth授权体系验证,集成了redis.swagger.jwt.mybatis多数据源等各项功能. 具体搭建过程后续另写播客介绍.具体结构如下: 在搭建过程集成mybatis的时候,考虑到单一数据源无法满足实际业务需要,故结合c#的开发经验,进行多数据源动态集成. mybatis的多数据源可以采用两种方式进行,第一种是分包方式实现,这

  • 详解SpringBoot和Mybatis配置多数据源

    目前业界操作数据库的框架一般是 Mybatis,但在很多业务场景下,我们需要在一个工程里配置多个数据源来实现业务逻辑.在SpringBoot中也可以实现多数据源并配合Mybatis框架编写xml文件来执行SQL.在SpringBoot中,配置多数据源的方式十分便捷, 下面开始上代码: 在pom.xml文件中需要添加一些依赖 <!-- Spring Boot Mybatis 依赖 --> <dependency> <groupId>org.mybatis.spring.b

  • Spring Boot 集成Mybatis实现主从(多数据源)分离方案示例

    本文将介绍使用Spring Boot集成Mybatis并实现主从库分离的实现(同样适用于多数据源).延续之前的Spring Boot 集成MyBatis.项目还将集成分页插件PageHelper.通用Mapper以及Druid. 新建一个Maven项目,最终项目结构如下: 多数据源注入到sqlSessionFactory POM增加如下依赖: <!--JSON--> <dependency> <groupId>com.fasterxml.jackson.core<

  • spring boot+mybatis 多数据源切换(实例讲解)

    由于公司业务划分了多个数据库,开发一个项目会同事调用多个库,经过学习我们采用了注解+aop的方式实现的 1.首先定义一个注解类 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TargetDataSource { String value();//此处接收的是数据源的名称 } 2.然后建一个配置类,这个在项目启动时会加载数据源,一开始采用了HikariCP,查资料说是最快性能最好的

  • Spring+MyBatis多数据源配置实现示例

    最近用到了MyBatis配置多数据源,原以为简单配置下就行了,实际操作后发现还是要费些事的,这里记录下,以作备忘 不多废话,直接上代码,后面会有简单的实现介绍 jdbc和log4j的配置 #定义输出格式 ConversionPattern=%d %-5p [%t] %c - %m%n log4j.rootLogger=DEBUG,Console log4j.logger.com.cnblogs.lzrabbit=DEBUG log4j.logger.org.springframework=ERR

  • Spring Boot + Mybatis-Plus实现多数据源的方法

    前段时间写了一篇基于mybatis实现的多数据源博客.感觉不是很好,这次打算加入git,来搭建一个基于Mybatis-Plus的多数据源项目 Mybatis-Plus就是香 前言:该项目分为master数据源与local数据源.假定master数据源为线上数据库,local为本地数据库.后续我们将通过xxl-job的方式,将线上(master)中的数据同步到本地(local)中 项目git地址 抽时间把项目提交到git仓库,方便大家直接克隆 sql文件已置于项目中,数据库使用的mysql 创建项

  • MyBatis多数据源的两种配置方式

    前言 同一个项目有时会涉及到多个数据库,也就是多数据源.多数据源又可以分为两种情况: 1)两个或多个数据库没有相关性,各自独立,其实这种可以作为两个项目来开发.比如在游戏开发中一个数据库是平台数据库,其它还有平台下的游戏对应的数据库: 2)两个或多个数据库是master-slave的关系,比如有mysql搭建一个 master-master,其后又带有多个slave:或者采用MHA搭建的master-slave复制: MyBatis多数据源的配置主要有两种方式: 通过@MapperScan注解,

随机推荐