多数据源系列

1、springboot2.6+Mybatis静态多数据源(集成JTA(Atomikos案例)实现分布式事务控制) 2、springboot2.6+Mybatis动态多数据源AOP切换(AbstractRoutingDataSource) 3、springboot2.6+Mybatis注解多数据源使用dynamic-datasource-spring-boot-starter为依赖

说明

随着应用用户数量的增加,相应的并发请求的数量也会跟着不断增加,慢慢地,单个数据库已经没有办法满足我们频繁的数据库操作请求了。 在某些场景下,我们可能会需要配置多个数据源,使用多个数据源(例如实现数据库的读写分离)来缓解系统的压力等,同样的,SpringBoot官方提供了相应的实现来帮助开发者们配置多数据源,据我目前所了解到的,一般分为两种方式静态与动态(AOP和dynamic)。 本文使用的是静态的方式。而且加了将spring原有的数据源信息管理的类型改为atomikos实现的AtomikosDataSourceBean数据源类(他也是javax.sql.DataSource的实现类),该类有一个属性是xaDataSource(xa就是分布式事务处理的一种模型规范),实现一下xaDataSource赋给这个属性; 之后各个数据源不要再独自去声明事务控制对象了,因为这时候Atomikos会自动有一个统一的分布式事务控制对象来控制事务。

本文中数据库用的是mysql5.7。完整代码地址在结尾!!

为什么用多数据源

其实在系统设计时,应当尽量避免一个项目接入多个数据源。我们应该尽量收敛一个数据库的使用者,这样在后续进行一些数据迁移、数据库重构等工作时能够降低风险和难度。 当然,这并不是绝对的情况,所谓“存在即是合理”。多个数据源的使用从另一方面来说能够大大的降低编码便捷性。我们不再需要通过Dubbo、SpringCloud等方式去通过其他系统中获取相关的数据。

简介

在大部分情况下,搭建单数据源就能够满足需求了,但是特殊情况下也需要使用多数据源,这里就写了一个demo搭建多数据源,注意这里没有使用AbstractRoutingDataSource 来实现动态切换 静态方式方式说白了,其实就是根据不同的包路径定义多个数据源,想用哪个用哪个呗。

静态数据源方案文件结构

最好每个数据源的mapper对应每个目录,分开来做。

spring boot 2 继承 jedis_多数据源

maven引入:
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
 </dependency>
 <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
 </dependency>
application.yml 配置文件
master1:
  url: jdbc:mysql://127.0.0.1:3306/master1?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
  username: root
  password: 123456
  driverClassName: com.mysql.cj.jdbc.Driver
master2:
  url: jdbc:mysql://127.0.0.1:3306/master2?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
  username: root
  password: 123456
  driverClassName: com.mysql.cj.jdbc.Driver
oracle:
  url: jdbc:oracle:thin:@10.132.212.63:1688:TESTDB
  username: flx
  password: flx202108
  driverClassName: oracle.jdbc.OracleDriver



logging:
  level:
    com.xkcoding: debug
    com.xkcoding.orm.mybatis.mapper: trace
    
server:
  port: 8080
#  servlet:
#    context-path: /demo
如下代码片段配置了其中一个数据源的参数
@Configuration
@MapperScan(basePackages = Master1DataSourceConfig.PACKAGE, sqlSessionTemplateRef = "master1SqlSessionTemplate")
public class Master1DataSourceConfig {
    static final String PACKAGE = "com.orm.mybatis.mapper.master1";
    static final String MAPPER_LOCATION = "classpath:mapper/master1/*.xml";

    @Value("${master1.url}")
    private String url;

    @Value("${master1.username}")
    private String user;

    @Value("${master1.password}")
    private String password;

    @Value("${master1.driverClassName}")
    private String driverClass;

//非分布式事务数据源 (多数据源事务情况下只能回滚单个数据源)
//因为事务会统一交给Atomikos全局事务,(因为是用了AtomikosDataSourceBean管理数据源),
//所以不能添加其他事务管理器
//    @Bean(name = "master1DataSource")     //非分布式事务数据源 (多数据源事务情况下只能回滚单个数据源)
//    public DataSource master1DataSource() {
        DataSource dataSource = DataSourceBuilder.create().driverClassName(driverClass).password(password).url(url).username(user).build();
//         HikariDataSource hikariDataSource = new HikariDataSource();
//         hikariDataSource.setJdbcUrl(url);
//         hikariDataSource.setUsername(user);
//         hikariDataSource.setDriverClassName(driverClass);
//         hikariDataSource.setPassword(password);
//         hikariDataSource.setMinimumIdle(5);
//         hikariDataSource.setMaximumPoolSize(20);
//         hikariDataSource.setAutoCommit(true);
//         hikariDataSource.setPoolName("SpringBootDemoHikariCP");
//         hikariDataSource.setMaxLifetime(60000);
//         hikariDataSource.setConnectionTimeout(30000);
//        return hikariDataSource;
//    }

    @Bean(name = "master1DataSource")    //分布式事务数据源(多数据源事务情况下只能回滚单个数据源)
    public DataSource master1DataSource() throws SQLException {
        MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
        mysqlXaDataSource.setUrl(url);
        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
        mysqlXaDataSource.setPassword(password);
        mysqlXaDataSource.setUser(user);
        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
        // 将本地事务注册到创 Atomikos全局事务
        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(mysqlXaDataSource);
        xaDataSource.setUniqueResourceName("master1DataSource");
        xaDataSource.setMinPoolSize(5);
        xaDataSource.setMaxPoolSize(20);
        xaDataSource.setMaxLifetime(60000);
        xaDataSource.setBorrowConnectionTimeout(30);
        xaDataSource.setLoginTimeout(30);
        xaDataSource.setMaintenanceInterval(60);
        xaDataSource.setMaxIdleTime(60);
        return xaDataSource;
    }

// 非分布式事务(多数据源事务情况下只能回滚单个数据源)
//    @Bean(name = "master1TransactionManager")
//    public DataSourceTransactionManager masterTransactionManager(@Qualifier("master1DataSource") DataSource masterDataSource) {
//        return new DataSourceTransactionManager(masterDataSource);
//  }

    @Bean(name = "master1SqlSessionFactory")
    @Primary
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("master1DataSource") DataSource masterDataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(masterDataSource);
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);//驼峰
        sessionFactory.setConfiguration(configuration);
        sessionFactory.setTypeAliasesPackage("com.orm.mybatis.entity");
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(Master1DataSourceConfig.MAPPER_LOCATION));
        return sessionFactory.getObject();
    }

    @Bean(name = "master1SqlSessionTemplate")         //添加这个就不用@primary
    public SqlSessionTemplate test2SqlSessionTemplate(@Qualifier("master1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

如上配置完成了一个数据源的添加,其余数据源按此模板进行复制便可以,但是有一点值得注意,需要将各个DataSource和SqlSessionFactory的Bean名称进行区分并搭配@Qualifier进行选择,不然会导致各个数据源间调用错乱。

UserServiceImpl 业务层
@Service
    public class UserServiceImpl {

    @Resource
    private UserMapper1 userMapper1;

    @Resource
    private UserMapper2 userMapper2;

    @Resource
    private AsusPoInfoMapper3 asusPoInfoMapper3;
    @Transactional
    public void testTransitional() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date =  simpleDateFormat.format(new Date());
        String UUID = java.util.UUID.randomUUID().toString().substring(0,5);
        User user = User.builder().email("andrew@qq.com"+UUID).name("andrew"+UUID).password("123456"+UUID).phoneNumber("123"+UUID)
                .lastUpdateTime(date).createTime(date).status(0).salt("password"+UUID).build();
        AsusPoInfo asusPoInfo = AsusPoInfo.builder().id(java.util.UUID.randomUUID().toString().substring(0,20))
                .woNo("andrew").po("123456").poLine("poline").cPo("cpo123456").shipType("Direct").build();
        userMapper1.saveUser(user);
        userMapper2.saveUser(user);
        asusPoInfoMapper3.insertAsusPoInfo(asusPoInfo);
        throw new RuntimeException();
    }
测试多数据源回滚
public class UserTest extends AndrewApplicationTests {

    @Resource
    private UserServiceImpl userService;

    @Test
    public void test3(){
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");   
        String date =  simpleDateFormat.format(new Date());
        User user = User.builder().email("andrew@qq.com").name("andrew").password("123456").
        phoneNumber("123").lastUpdateTime(date).createTime(date).status(0).salt("MD5").build();
        userService.insertUser(user);
    }
}

测试结果,三个数据源都回滚插入的数据。

方案的权衡

静态多数据源方案优势在于配置简单并且对业务代码的入侵性极小,缺点也显而易见:我们需要在系统中占用一些资源,而这些资源并不是一直需要,一定程度上会造成资源的浪费。如果你需要在一段业务代码中同时使用多个数据源的数据又要去考虑操作的原子性(事务)可以用spring的jta实现事务,那么这种方案无疑会适合你。

(aop和dynamic)动态数据源(AbstractRoutingDataSource)方案配置上看起来配置会稍微复杂一些,但是很好的符合了“即拿即用,即用即还”的设计原则,我们把多个数据源看成了一个池子,然后进行消费。它的缺点正如上文所暴露的那样:我们往往需要在事务的需求下做出妥协。而且由于需要切换环境上下文,在高并发量的系统上进行资源竞争时容易发生死锁等活跃性问题。我们常用它来进行数据库的“读写分离”,不需要在一段业务中同时操作多个数据源。这种动态形式并不能用spring的jta实现,而且其他实现方式(seata等)虽然可以实现,但配置复杂且实用度不高。

如果需要使用事务,一定记得使用分布式事务进行Spring自带事务管理的替换,否则将无法进行一致性控制。

写到这里本文也就结束,好久没有撰写文章很多东西考虑不是很详尽,谢谢批评指正!

项目地址

springboot2.6+mybatishttps://gitee.com/liuweiqiang12/springboot-mybatis-static-datasource

springboot2.6+mybatis-plushttps://gitee.com/liuweiqiang12/springboot-mybatis-plus-static-datasource