文档章节

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

上官胡闹
 上官胡闹
发布于 01/10 23:03
字数 3395
阅读 1873
收藏 90

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

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

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

       另一种是多个数据源之间并没有特别明显的操作,只是程序在一个流程中可能需要同时从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等

 

 

 

© 著作权归作者所有

共有 人打赏支持
上官胡闹
粉丝 49
博文 81
码字总数 57443
作品 1
成都
程序员
加载中

评论(4)

上官胡闹
上官胡闹

引用来自“小镇刁民”的评论

这样能叫分布式事务。。。。
Spring Boot supports distributed JTA transactions across multiple XA resources using either an Atomikos or Bitronix embedded transaction manager.
这个仅是我个人对官方文档这句话的理解,没接触过高大上的东西,不知道真正的分布式事务定义是什么
小镇刁民
小镇刁民
这样能叫分布式事务。。。。
上官胡闹
上官胡闹

引用来自“大连馋师”的评论

狠,真狠~ 3分钟搞定。
哈哈,搞技术不吹牛
大连馋师
大连馋师
狠,真狠~ 3分钟搞定。
如何实现J2EE的分布式事务管理器?

大家好,最近项目中用到了多数据源,需要进行事务处理,普通的JDBC事务只能针对一种数据源,对于多数据源需要使用分布式事务,在分布式事务这块没有接触过,希望得到大家的帮助。 谢谢!

山哥
2010/07/23
796
2
mvilplss/nature-framework

框架简介 nature-framework是一个以自由为理念,以易用,代码简洁,开发快速,功能强大,易扩展,低耦合为目标,适用于快速开发的轻量级MVC+ORM框架。无getter/setter方法,无xml配置,包括源...

mvilplss
2017/01/13
0
0
spring 多数据源一致性事务方案

spring 多数据源配置 spring 多数据源配置一般有两种方案: 1、在spring项目启动的时候直接配置两个不同的数据源,不同的sessionFactory。在dao 层根据不同业务自行选择使用哪个数据源的ses...

纯洁的虫纸
2015/11/19
0
0
数据库分库分表(sharding)系列(四) 多数据源的事务处理

系统经sharding改造之后,原来单一的数据库会演变成多个数据库,如何确保多数据源同时操作的原子性和一致性是不得不考虑的一个问题。总体上看,目前对于一个分布式系统的事务处理有三种方式:...

bluishglc
2012/07/27
0
0
Spring事务隔离级别与传播机制,spring+mybatis+atomikos实现分布式事务管理

本文转载于本人另一博客【http://blog.csdn.net/liaohaojian/article/details/68488150】 1.事务的定义:事务是指多个操作单元组成的合集,多个单元操作是整体不可分割的,要么都操作不成功,...

半山闲人
2017/04/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring加载properties文件的两种方式

在项目中如果有些参数经常需要修改,或者后期可能需要修改,那我们最好把这些参数放到properties文件中,源代码中读取properties里面的配置,这样后期只需要改动properties文件即可,不需要修...

架构师springboot
10分钟前
0
0
分布式事务,原来可以这么玩?

多个数据要同时操作,如何保证数据的完整性,以及一致性? 答 : 事务 ,是常见的做法。 举个栗子: 用户下了一个订单,需要修改 余额表 , 订单 表 , 流水 表 ,于是会有类似的伪代码: st...

微笑向暖wx
13分钟前
0
0
IE6兼容PNG32图片显示PNG8图片

IE6并不是不支持PNG图片,只是不支持半透明通道。 是支持PNG8色表引索全透明的。 以往都是通过滤镜或统统使用PNG8实现兼容。 但是我发现twitter的png图标可以在chrome中显示png32,在IE6显示...

linsk1998
25分钟前
0
0
linux运维需要掌握的基础知识

踏入linux运维工程师这一职业,其实有很多工具技能需要掌握,下面我来给大家一一介绍。 1、shell脚本和另一个脚本语言,shell是运维人员必须具备的,不懂这个连入职都不行,至少也要写出一些...

linuxprobe16
26分钟前
0
0
《netty入门与实战》笔记-03:数据传输载体 ByteBuf 介绍

ByteBuf结构 首先,我们先来了解一下 ByteBuf 的结构 以上就是一个 ByteBuf 的结构图,从上面这幅图可以看到: ByteBuf 是一个字节容器,容器里面的的数据分为三个部分,第一个部分是已经丢弃...

Funcy1122
59分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部