3分钟搞定SpringBoot+Mybatis+druid多数据源和分布式事务

Wesley13
• 阅读 549

       在一些复杂的应用开发中,一个应用可能会涉及到连接多个数据源,所谓多数据源这里就定义为至少连接两个及以上的数据库了。

       下面列举两种常用的场景:

        一种是读写分离的数据源,例如一个读库和一个写库,读库负责各种查询操作,写库负责各种添加、修改、删除。

       另一种是多个数据源之间并没有特别明显的操作,只是程序在一个流程中可能需要同时从A数据源和B数据源中取数据或者同时往两个数据库插入数据等操作。

       对于这种多数据的应用中,数据源就是一种典型的分布式场景,因此系统在多个数据源间的数据操作必须做好事务控制。在springboot的官网中发现其支持的分布式事务有三种Atomikos 、Bitronix、Narayana。本文涉及内容中使用的分布式事务控制是Atomikos,感兴趣的可以查看https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-jta.html

当然分布式事务的作用并不仅仅应用于多数据源。例如:在做数据插入的时候往一个kafka消息队列写消息,如果信息很重要同样需要保证分布式数据的一致性。

一、了解多数据源配置中的那些坑

        其实目前网上已经有许多的关于SpringBoot+Mybatis+druid+Atomikos技术栈的文章,在这里也很感谢那些乐于分享的同行们。本文中涉及的许多的问题也是吸纳了许多中外文相关技术博客文档的优点,算是站在巨人的肩膀做一次总结吧。抛开废话,下面列举一些几点多数据源带来的坑吧。

  1. 配置麻烦,尤其是对于许多开发的新手,看了许多网上的文章,也许还配置不对,还有面对一堆的文章,可能还无法鉴别那些文章的方法是比较可行的。
  2. 配置了多数据源后发现加入事务后并不能完成数据源的切换。
  3. 配置多数据源时发现增加了许多的配置工作量。
  4. springboot环境下mybatis应用打成jar包后无法扫描别名。

二、如何配置一个springboot多数据源项目

    本文使用的技术栈是:SpringBoot+Mybatis+druid+Atomikos,因此使用其他技术栈的可以参考他人博客或者是根据本文内容改造。

重要的技术框架依赖:

 <!-- ali druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.6</version>
</dependency>
 <!-- mybatis spring -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>
 <!--atomikos transaction management-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

注意:对于使用mysql jdbc 6.0的同鞋必须更新druid到最新的1.1.6,否则druid无法支持分布式事务。感兴趣的可查看官方的release说明。

  1. 编写AbstractDataSourceConfig抽象数据源配置

    /**

    • 针对springboot的数据源配置
    • @author yu on 2017/12/28.

    */ public abstract class AbstractDataSourceConfig {

    protected DataSource getDataSource(Environment env,String prefix,String dataSourceName){
        Properties prop = build(env,prefix);
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        ds.setUniqueResourceName(dataSourceName);
        ds.setXaProperties(prop);
        return ds;
    }
    
    protected Properties build(Environment env, String prefix) {
        Properties prop = new Properties();
        prop.put("url", env.getProperty(prefix + "url"));
        prop.put("username", env.getProperty(prefix + "username"));
        prop.put("password", env.getProperty(prefix + "password"));
        prop.put("driverClassName", env.getProperty(prefix + "driver-class-name", ""));
        prop.put("initialSize", env.getProperty(prefix + "initialSize", Integer.class));
        prop.put("maxActive", env.getProperty(prefix + "maxActive", Integer.class));
        prop.put("minIdle", env.getProperty(prefix + "minIdle", Integer.class));
        prop.put("maxWait", env.getProperty(prefix + "maxWait", Integer.class));
        prop.put("poolPreparedStatements", env.getProperty(prefix + "poolPreparedStatements", Boolean.class));
        prop.put("maxPoolPreparedStatementPerConnectionSize",
                env.getProperty(prefix + "maxPoolPreparedStatementPerConnectionSize", Integer.class));
        prop.put("validationQuery", env.getProperty(prefix + "validationQuery"));
        prop.put("validationQueryTimeout", env.getProperty(prefix + "validationQueryTimeout", Integer.class));
        prop.put("testOnBorrow", env.getProperty(prefix + "testOnBorrow", Boolean.class));
        prop.put("testOnReturn", env.getProperty(prefix + "testOnReturn", Boolean.class));
        prop.put("testWhileIdle", env.getProperty(prefix + "testWhileIdle", Boolean.class));
        prop.put("timeBetweenEvictionRunsMillis", env.getProperty(prefix + "timeBetweenEvictionRunsMillis", Integer.class));
        prop.put("minEvictableIdleTimeMillis", env.getProperty(prefix + "minEvictableIdleTimeMillis", Integer.class));
        prop.put("useGlobalDataSourceStat",env.getProperty(prefix + "useGlobalDataSourceStat", Boolean.class));
        prop.put("filters", env.getProperty(prefix + "filters"));
        return prop;
    }
    

    }

ps:AbstractDataSourceConfig对于其他数据库链接池的配置是可以改动的。

2.编写关于基于注解的动态数据源切换代码,这部分主要是将数据库源交给AbstractRoutingDataSource类,并由它的determineCurrentLookupKey()进行决定数据源的选择。关于这部分的代码,其实网上的做法基本差不多,这里也就列举出来了大家可以阅读其他相关的博客,但是这部分的代码是可以单独封装成一个模块的,封装好后不管对于Springboot项目还是SpringMVC项目将封装的模块导入都是可以正常工作的。可以参考本人目前开源的https://gitee.com/sunyurepository/ApplicationPower项目中的datasource-aspect模块。

3.应用2中的通用封装模块并做写小改动,这里所谓的主要是你可能会像,在上面第二步中的写的切面作用类可能没有是用aop的注解或者是使用自定义注解的默认拦截失效,这时继承下通用模块中的类重写一个AOP作用类。例如:

@Aspect
@Component
public class DbAspect extends DataSourceAspect {

    @Pointcut("execution(* com.power.learn.dao.*.*(..))")
    @Override
    protected void datasourceAspect() {
        super.datasourceAspect();
    }
}

4.编写一个MyBatisConfig,该类的作用就是创建Mybatis多个数据源的java配置了。例如想建立两个数据源一个叫one,另一个叫two

@Configuration
@MapperScan(basePackages = MyBatisConfig.BASE_PACKAGE, sqlSessionTemplateRef = "sqlSessionTemplate")
public class MyBatisConfig extends AbstractDataSourceConfig {

    //mapper模式下的接口层
    static final String BASE_PACKAGE = "com.power.learn.dao";

    //对接数据库的实体层
    static final String ALIASES_PACKAGE = "com.power.learn.model";

    static final String MAPPER_LOCATION = "classpath:com/power/learn/mapping/*.xml";


    @Primary
    @Bean(name = "dataSourceOne")
    public DataSource dataSourceOne(Environment env) {
        String prefix = "spring.datasource.druid.one.";
        return getDataSource(env,prefix,"one");
    }

    @Bean(name = "dataSourceTwo")
    public DataSource dataSourceTwo(Environment env) {
        String prefix = "spring.datasource.druid.two.";
        return getDataSource(env,prefix,"two");
    }



    @Bean("dynamicDataSource")
    public DynamicDataSource dynamicDataSource(@Qualifier("dataSourceOne")DataSource dataSourceOne,@Qualifier("dataSourceTwo")DataSource dataSourceTwo) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("one",dataSourceOne);
        targetDataSources.put("two",dataSourceTwo);

        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);
        dataSource.setDefaultTargetDataSource(dataSourceOne);
        return dataSource;
    }

    @Bean(name = "sqlSessionFactoryOne")
    public SqlSessionFactory sqlSessionFactoryOne(@Qualifier("dataSourceOne") DataSource dataSource)
        throws Exception {
        return createSqlSessionFactory(dataSource);
    }

    @Bean(name = "sqlSessionFactoryTwo")
    public SqlSessionFactory sqlSessionFactoryTwo(@Qualifier("dataSourceTwo") DataSource dataSource)
        throws Exception {
        return createSqlSessionFactory(dataSource);
    }




    @Bean(name = "sqlSessionTemplate")
    public CustomSqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactoryOne")SqlSessionFactory factoryOne,@Qualifier("sqlSessionFactoryTwo")SqlSessionFactory factoryTwo) throws Exception {
        Map<Object,SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>();
        sqlSessionFactoryMap.put("one",factoryOne);
        sqlSessionFactoryMap.put("two",factoryTwo);

        CustomSqlSessionTemplate customSqlSessionTemplate = new CustomSqlSessionTemplate(factoryOne);
        customSqlSessionTemplate.setTargetSqlSessionFactorys(sqlSessionFactoryMap);
        return customSqlSessionTemplate;
    }

    /**
     * 创建数据源
     * @param dataSource
     * @return
     */
    private SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws Exception{
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setVfs(SpringBootVFS.class);
        bean.setTypeAliasesPackage(ALIASES_PACKAGE);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));
        return bean.getObject();
    }
}

划重点(考试要考):注意最后createSqlSessionFactory方法中的这一行代码bean.setVfs(SpringBootVFS.class),对于springboot项目采用java类配置Mybatis的数据源时,mybatis本身的核心库在springboot打包成jar后有个bug,无法完成别名的扫描,在低版本的mybatis-spring-boot-starter中需要自己继承Mybatis核心库中的VFS重写它原有的资源加载方式。在高版本的mybatis-spring-boot-starter已经帮助实现了一个叫SpringBootVFS的类。感兴趣的可以到官方项目了解这个bughttps://github.com/mybatis/spring-boot-starter/issues/177

5.解决分布式事务控制下数据源无法动态切换的问题。对于为每一个数据源创建单独的静态数据源并且配置固定以扫描不同包上的mapper接口层情况是不会出现这种问题的,可以很好的调用不同包下的mapper层,因为数据源一开就已经初始化好了,分布式事务不会影响你调用不同的数据源,也不需要前面的步骤。

对于动态多数据源架构的场景,数据源都是通过aop来完成切换了,但是因为事务控制在切换之前,因此切换就被事务阻止了。曾经在解决这个问题是,很幸运的是我在google中搜索是发现了一个很有趣的方案,并且是国内的人实现放在github上的。下面看下源码核心。

**
 * from https://github.com/igool/spring-jta-mybatis
 */
public class CustomSqlSessionTemplate extends SqlSessionTemplate {
    
   //......省略
    @Override
    public SqlSessionFactory getSqlSessionFactory() {
        SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactorys.get(DataSourceContextHolder.getDatasourceType());
        if (targetSqlSessionFactory != null) {
            return targetSqlSessionFactory;
        } else if (defaultTargetSqlSessionFactory != null) {
            return defaultTargetSqlSessionFactory;
        } else {
            Assert.notNull(targetSqlSessionFactorys, "Property 'targetSqlSessionFactorys' or 'defaultTargetSqlSessionFactory' are required");
            Assert.notNull(defaultTargetSqlSessionFactory, "Property 'defaultTargetSqlSessionFactory' or 'targetSqlSessionFactorys' are required");
        }
        return this.sqlSessionFactory;
    }
    //......省略

}

就是重写一个SqlSessionTemplate来改变让SqlSessionFactory动态的获取数据源。

targetSqlSessionFactorys.get(DataSourceContextHolder.getDatasourceType());

DataSourceContextHolder一般就是你在第二步中创建的数据源上下文操作类,这个只需要根据自己需求做改动即可。当然这个类我个人也建议像第二步一样单独放到一个模块中,可以参考本人目前开源的https://gitee.com/sunyurepository/ApplicationPower项目中的mybatis-template模块。专门为mybatis场景准备,但是我不建议和第二步和代码合并在一起,因为对于数据切换的切面控制代码可以放到非mybatis的项目中。

6.多数据源的项目配置文件配置。这里采用yml。其配置参考如下:

#Spring boot application.yml

# spring
spring:
  #profiles : dev
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      one:
        url: jdbc:mysql://localhost:3306/project_boot?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        minIdle: 1
        maxActive: 20
        initialSize: 1
        timeBetweenEvictionRunsMillis: 3000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 'ZTM' FROM DUAL
        validationQueryTimeout: 10000
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        maxWait: 60000
        # 打开PSCache,并且指定每个连接上PSCache的大小
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,wall,log4j2
        useGlobalDataSourceStat: true
      two:
        url: jdbc:mysql://localhost:3306/springlearn?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        minIdle: 1
        maxActive: 20
        initialSize: 1
        timeBetweenEvictionRunsMillis: 3000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 'ZTM' FROM DUAL
        validationQueryTimeout: 10000
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        maxWait: 60000
        # 打开PSCache,并且指定每个连接上PSCache的大小
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,wall,log4j2
        useGlobalDataSourceStat: true
  jta:
    atomikos:
      properties:
        log-base-dir: ../logs
    transaction-manager-id: txManager
server:
  port: 8080
  undertow:
     accesslog:
      enabled: true
      dir: ../logs

ps:jta就是配置让springboot启动分布式事务支持。

7.编码测试

dao层实例(对应两个数据源,使用注解动态切换):

@TargetDataSource(DataSourceKey.ONE)
public interface StudentOneDao {

    /**
     * 保存数据
     * @param entity
     * @return
     */
    int save(Student entity);

}


@TargetDataSource(DataSourceKey.TWO)
public interface StudentTwoDao {

    /**
     * 保存数据
     * @param entity
     * @return
     */
    int save(Student entity);
}

service层

@Service("studentOneService")
public class StudentOneServiceImpl implements StudentService {

    /**
     * 日志
     */
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Resource
    private StudentOneDao studentOneDao;

    @Resource
    private StudentTwoDao studentTwoDao;
    

    @Transactional
    @Override
    public CommonResult save(Student entity) {
        CommonResult result = new CommonResult();
        try {
            studentOneDao.save(entity);
            studentTwoDao.save(entity);
            int a = 10/0;
            result.setSuccess(true);
        } catch (Exception e) {
            logger.error("StudentService添加数据异常:",e);
            //抛出异常让异常restful化处理
            throw new RuntimeException("添加数据失败");
        }
        return result;
    }
}

ps:除0操作强行造一个异常来检测分布式事务是否生效,注意对于自己捕获处理的异常情况需要throw出去,否则事务不会生效的。可以参考我提供的demo https://gitee.com/sunyurepository/multiple-datasource

三、如何解决这些该死的配置?

        按照上面的步骤处理后,基本就完成了一个多数据源应用的基础架构了,但是有人会发现了,上面这么多的配置,搞这么多代码,几分钟的时间能搞定吗,答案基本不太可能,一不小心可能还会因为写错了数据源名称又搞半天。

       因此我将介绍一种真正用几分钟时间来搭建一个多数据源项目的方法。帮你省掉这些重复的配置工作,轻松玩转n个数据源,抛弃那些该死的配置,分分钟创建一个demo。

第一步:下载https://gitee.com/sunyurepository/ApplicationPower项目

第二步:将Common-util、datasource-aspect、mybatis-template三个模块安装到你的本地maven仓库中。对于idea的用户只需要点3下大家都懂得,eclipse的用户默默的抹下眼泪吧。

第三步:在application-power的resources下找到jdbc.properties连接一个mysql的数据库.

第四步:在application-power的resources下找到generator.properties修改按照说明修改就好了

# @since 1.5
# 打包springboot时是否采用assembly
# 如果采用则将生成一系列的相关配置和一系列的部署脚本
generator.package.assembly=true

#@since 1.6
# 多数据源多个数据数据源用逗号隔开,不需要多数据源环境则空出来
# 对于多数据源会集成分布式事务
generator.multiple.datasource=mysql,oracle

# @since 1.6
# jta-atomikos分布式事务支持
generator.jta=true

主要的就是制定自己想取的数据源名称吧,如上我一个连接mysql,一个连接oracle。其他的根据自己的需求来改。

第五步:运行application-power的test中的

GenerateCodeTest

完成所有项目代码的产生和输出,然后你就可以导入idea工具测试了。

创建完你要做的几件事:

  1. 自动创建的项目会在dao层默认注入你配置的第一个数据源,因此需要根据自己的情况修改,关于数据源的名称已经自动帮你创建了一个常量类中。
  2. 给service的方法需要使用事务的方法自己加事务注解
  3. 对于非mysql数据库你需要自己添加驱动包,创建的代码默认添加mysql驱动包
  4. 在application.yml中修改你的数据源用户名,密码和连接的url地址,因为生成的默认是copy你连接数据库生成项目时的数据库连接信息。

    小结:其实创建完后整个工作就是做极少的修改,多数据源的所有配置都创建好了,连两个和连5个数据源带来的工作并不大。当然如果想用ApplicationPower来创建真实应用的童鞋,如果觉得模板中的一些依赖模块不想在公司使用也是可以稍微修改小模板来从新生成的,在使用中也希望有更好的建议被提出。

总结:

       本文主要只是对许多多数据源场景使用中相关优秀文章的总结。我个人仅仅是将这些总结的东西通过封装和我个人开源放在码云上的ApplicationPower脚手架将SpringBoot+Mybatis+druid+Atomikos的多数据源和分布式事务架构的配置通过自动化来快速输出。

申明:转载本博客内容请注明原地址https://my.oschina.net/u/1760791/blog/1605367

参考博客:

http://blog.csdn.net/a510835147/article/details/75675311等

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
BeetlSQL 3.0.10 发布,多数据源分布式sega事务支持
本次发布主要增加了分布式Sega事务支持,适合多数据源按照社区建议,修改了了springboot的yml配置方式修改了@Jackson和@UpdateTime,本来是用来作为例子,但社区开发者提供了较好的完整实现增加Sega支持<dependency<groupIdcom.ibeetl</gr
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这