【springboot专题】二十 springboot中如何配置多数据源

程序猿学社的GitHub,欢迎Star
https://github.com/ITfqyd/cxyxs
本文已记录到github,形成对应专题。

前言

随着业务的不断增加,我们的系统会越来越庞大,因此,一个项目中使用多个数据源,是我们可能会遇到的问题。本来就来,看看springboot多数据源是怎么搭建的。

多数据源,如何划分?

分为两种。

  • 分包,分包主要是根据业务划分。
  • 注解方式,实际上就是通过aop进行拦截,不同的注解里面的值,指向不同的数据源。

环境

为了减少写sql,更好的提高开发效率,引入mybatis-plus

名称 作用
springboot2.0 简化配置,快速构建一个项目
mybatis-plus 简化sql的一个插件,可以实现通过操作实体类,操作数据库的表。
mysql 数据库(两个库pro和pro1)

单个数据源

为什么要说一说搭建单个数据源,社长,多数据源勒?来自某某社友的吐槽,你个糟老头子,坏的很。
先搭建单个数据源也是为了简化难度。先保证单个数据源没有问题后,我们再来挑战多个数据源,不要,还没头学会走路,就想着跑,还是得一步一个脚印。才能才不会摔倒。也不会遇到各种各样的报错。

  • 小技巧:可以把一个复杂的问题,拆分成多个小的问题,一个个的解决。

sql脚本

创建一个数据库pro

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for emp
-- ----------------------------
DROP TABLE IF EXISTS `emp`;
CREATE TABLE `emp`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `age` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of emp
-- ----------------------------
INSERT INTO `emp` VALUES (1, '后羿', 21);
INSERT INTO `emp` VALUES (2, '韩信', 40);
INSERT INTO `emp` VALUES (3, '兰陵王', 11);

SET FOREIGN_KEY_CHECKS = 1;

搭建项目

创建一个springboot项目

可以发现有一个test1包和test2包,我们先用test1包实现调用数据库成功。

pom.xml依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.cloudtech</groupId>
    <artifactId>moredatasource</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>moredatasource</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 增加thymeleaf座标  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--简化实体类-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!--swagger2-->
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>spring-boot-starter-swagger</artifactId>
            <version>1.5.1.RELEASE</version>
        </dependency>

        <!--MP插件,简化sql操作-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • springboot是2.0以上,mp的版本是3.0以上,如果不一样,可能会有惊喜。

application.yml

server:
    port: 8888

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/pro?useUnicode=true&characterEncoding=utf8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8
    username: root
    password: root

mybatis-plus:
  ##mapper.xml文件存放的路径
  mapper-locations: classpath*:/com/cxyxs/moredatasource/test1/mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 这个也没有什么好说的,log-impl是打印sql日志。
@Data
public class Emp {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer age;
}
  • 可以发现我们的代码没有set get,这是因为我们引入lombok插件
  • @TableId(type = IdType.AUTO) 数据库ID自增,国产的源码看起来就是舒服,中文注释,对我这种英文不好的,很好使。

dao

@Mapper
@Repository
public interface EmpMapper1 extends BaseMapper<Emp> {

}

在这里插入图片描述

  • @Repository 为了解决报红错的,但是,这个红色的波浪线,不会影响代码的运行,但是,对于我这种有强迫症的人,看起来十分的不舒服。

controller

package com.cxyxs.moredatasource.controller;

import com.cxyxs.moredatasource.entity.Emp;
import com.cxyxs.moredatasource.test1.dao.EmpMapper1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;


/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/7 12:15
 * Modified By:
 */
@RestController
public class TestController {
    @Autowired
    private EmpMapper1 empMapper;

    @GetMapping("/getKing")
    public List getKing(){
        List<Emp> emps = empMapper.selectList(null);
        return emps;
    };
}
  • 操作数据库用到mybatis-plus的一些知识,可以看看springboot集成mybatis-plus
  • @RestController注解,实际上就是两个注解的效果。我们看一看的源码就知道是怎么一回事。
  • @GetMapping 使用的RESTful风格,等价于@RequestMapping(method = RequestMethod.GET)

springboot入口

package com.cxyxs.moredatasource;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = {"com.cxyxs"})
@MapperScan("com.cxyxs.moredatasource.test1.dao")
public class MoredatasourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(MoredatasourceApplication.class, args);
    }

}
  • @SpringBootApplication实际上就是三个注解的组合
  • @MapperScan 扫描的Mapper类所在包的路径,也就是我们所谓的dao包。
  • ComponentScan 自动扫描当前包及子包下所有类,得理解这句话

访问网站

输入http://localhost:8080/getKing

  • 有不少社友在问,社长,社长,返回的结果集,你界面好直观,数据结构很清楚。而我的都堆积在一起,阅读起来很累。那是因为我使用的谷歌浏览器,所以,我直接百度上搜索"谷歌json插件"。

到这里单个数据源,我们已经搞定,可以给自己奖励一个鸡腿!
下面,我们继续多数据源的搭建。话不多说,走起,盘他

多个数据源(分包)

mybatis-plus(简称MP),以后的文章中,写MP,就是表示mybatis-plus

利用单个数据源的sql,再重新搭建一个数据库pro1

  • 上面是两个数据库表的对应数据,为了方便后面区分,age是有明显的区别的。第一个表是123,第二个库的表是456.

表关系

pro test1
pro1 test2

项目改造

在单个数据源上改造

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.cloudtech</groupId>
    <artifactId>moredatasource</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>moredatasource</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 增加thymeleaf座标  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--简化实体类-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!--swagger2-->
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>spring-boot-starter-swagger</artifactId>
            <version>1.5.1.RELEASE</version>
        </dependency>

        <!--MP插件,简化sql操作-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>

        <!--swagger2-->
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>spring-boot-starter-swagger</artifactId>
            <version>1.5.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <!--解决编译后,xml文件没有过去的问题-->
        <resources>
                <resource>
                    <directory>src/main/java</directory>
                    <includes>
                        <include>**/*.xml</include>
                    </includes>
                </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • 相对於单个数据源,多引入了一个swagger,主要是实现api接口文档可视化。

applicaion.yml

server:
    port: 8888
spring:
  datasource:
    test1:
      # 使用druid数据源
      type: com.alibaba.druid.pool.DruidDataSource
      jdbc-url: jdbc:mysql://localhost:3306/pro?useUnicode=true&characterEncoding=utf8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

    test2:
      # 使用druid数据源
      type: com.alibaba.druid.pool.DruidDataSource
      jdbc-url: jdbc:mysql://localhost:3306/pro1?useUnicode=true&characterEncoding=utf8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
      
mybatis-plus:
  configuration:
    ##打印sql日志,本地测试使用,生产环境不要使用,注意、注意、注意
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

####扫描swagger注解
swagger:
  base-package: com.cxyxs
  • jdbc-url和driver-class-name这两个参数得注意,在多数据源配置中,有变动,我们熟悉的是url,所以不少人都会有困惑,同样的代码,为什么,社长你的启动就不报错,难道是社长人品太好的原因。
  • 前方高能,注意警惕,我们可以发现配置连接数据库的那块代码,有了明显的变动,增加了一个test1和test2。spring.datasource.test1和spring.datasource.test2后面会用到,这个是前缀,type,jdbc-url等等这些是后缀,是固定的。
  • swagger是需要配置扫描范围的

config配置

数据源配置1

package com.cxyxs.moredatasource.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/7 18:14
 * Modified By:
 */
@Configuration
@MapperScan(basePackages= {"com.cxyxs.moredatasource.test1.dao"},sqlSessionFactoryRef="test1SqlSessionFactory")
public class DataSourceConfig1 {
    @Bean(name="test1DataSource")
    @ConfigurationProperties(prefix="spring.datasource.test1")
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name="test1SqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource,@Qualifier("testPaginationInterceptor") PaginationInterceptor paginationInterceptor) throws Exception {
        MybatisSqlSessionFactoryBean bean=new MybatisSqlSessionFactoryBean ();
        bean.setDataSource(dataSource);
        Resource[] resources = new PathMatchingResourcePatternResolver()
                .getResources("classpath*:/com/cxyxs/moredatasource/test1/mapper/*.xml");
        bean.setMapperLocations(resources);
        Interceptor[] plugins = new Interceptor[]{paginationInterceptor};
        bean.setPlugins(plugins);
        return bean.getObject();
    }

    @Bean(name="test1TransactionManager")//配置事务
    public DataSourceTransactionManager testTransactionManager(@Qualifier("test1DataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean("testPaginationInterceptor")
    public PaginationInterceptor paginationInterceptor(){
        return new PaginationInterceptor();
    }


    @Bean(name="test1SqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
       return new SqlSessionTemplate(sqlSessionFactory);
    }
}
  • springboot2.0以上会有一个错误,需要你指定一个默认数据源,在springboot2.0以后就修复了这个问题,如果springboot版本在2.0一下的社友可以在每个@Bean注解下面增加一句代码@Primary

  • paginationInterceptor注解是配置MP分页插件,不了解的社友可以看看这篇文章
    springboot集成mybatis-plus

  • 配置好分页插件的bean,是不是要跟SqlSessionFactory建立联系。在单个数据源中,我们是不需要建立联系的。

  • 下面代码是配置mybatis配置文件在哪里。还记得我们单数据源配置的时候,配置在哪里吗?配置在yml里面。

  • 分享一下,个人搭建过程中,遇到的问题,最初我使用的是SqlSessionFactory这个对象,在测试过程中,发现,所有的mybatis调用数据库的操作都没有毛病,但是,我一调用MP提供的一些insert方法时,发现,提示找不到这个方法。就在想,是不是又要建立什么联系,查了一些资料,才发现MP自己实现了一个工厂MybatisSqlSessionFactoryBean,换成这个对象,问题就解决了,从这,我得到一个启示,学习一个东西,不应该简单的会使用,而应该深入了解他的一个思想。进行一些简单的源码学习。

 //MP的工厂
 MybatisSqlSessionFactoryBean bean=new MybatisSqlSessionFactoryBean ();
//mybatis的工厂
//SqlSessionFactory bean=new MybatisSqlSessionFactoryBean ();

数据源配置2

直接把数据源配置1的类copy一下,全文把test1替换成test2。把类名的1改成2.放在注入到springboot中报错。

  • boot2.0一下版本的社友,把数据源配置1代码copy过来的时候,记得去掉@Primary注解,不然springboot会犯困惑,到底要加载那个数据源,会有疑惑,有疑惑,那只能给你报错。
package com.cxyxs.moredatasource.config;

import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/7 18:14
 * Modified By:
 */
@Configuration
@MapperScan(basePackages= {"com.cxyxs.moredatasource.test2.dao"},sqlSessionFactoryRef="test2SqlSessionFactory")
public class DataSourceConfig2 {
    @Bean(name="test2DataSource")
    @ConfigurationProperties(prefix="spring.datasource.test2")
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name="test2SqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("test2DataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean bean=new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSource);
       Resource[] resources = new PathMatchingResourcePatternResolver()
                .getResources("classpath*:/com/cxyxs/moredatasource/test2/mapper/*.xml");
        bean.setMapperLocations(resources);
        return bean.getObject();
    }

    @Bean(name="test2TransactionManager")//配置事务
    public DataSourceTransactionManager testTransactionManager(@Qualifier("test2DataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name="test2SqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
       return new SqlSessionTemplate(sqlSessionFactory);
    }
}

实体类

package com.cxyxs.moredatasource.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import org.omg.CORBA.IDLType;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/7 12:03
 * Modified By:
 */
@Data
public class Emp {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer age;
}

dao类和mapper

test1包

@Repository
public interface EmpMapper1 extends  BaseMapper<Emp>{
    @Select("select * from emp")
    public List<Emp> selectList();
    /**
     * 测试mapper
     * @return
     */
    public List<Emp> getAll();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cxyxs.moredatasource.test1.dao.EmpMapper1">
  
  <!--  根据区域名称获取区域代码-->
   <select id="getAll" resultType="com.cxyxs.moredatasource.entity.Emp">
		select * from emp
  </select>
</mapper>

test2包

package com.cxyxs.moredatasource.test2.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cxyxs.moredatasource.entity.Emp;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/7 12:01
 * Modified By:
 */
@Repository
public interface EmpMapper2  extends  BaseMapper<Emp>{
    @Select("select * from emp")
    public List<Emp> selectList();

    /**
     * 测试mapper
     * @return
     */
    public List<Emp> getAll();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cxyxs.moredatasource.test2.dao.EmpMapper2">
  
  <!--  根据区域名称获取区域代码-->
   <select id="getAll" resultType="com.cxyxs.moredatasource.entity.Emp">
		select * from emp
  </select>
</mapper>
  • 在这里给大家推荐一款提高开发效率的工具,不是打广告,MybatisX 是一款基于 IDEA 的快速开发插件,为效率而生。他也是MP官方推荐的,简单说一下他的作用。我们再dao类写上如下代码。使用MybatisX插件会帮我们生成对应的xml文件。
public List<Emp> getAll();

插件自动生成的

 <select id="getAll" resultType="com.cxyxs.moredatasource.entity.Emp"></select>

controll类

package com.cxyxs.moredatasource.controller;

import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.cxyxs.moredatasource.entity.Emp;
import com.cxyxs.moredatasource.test1.dao.EmpMapper1;
import com.cxyxs.moredatasource.test2.dao.EmpMapper2;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;


/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/7 12:15
 * Modified By:
 */
@RestController
@Api("测试多数据源接口")
public class TestController {
    @Autowired
    private EmpMapper1 empMapper1;
    @Autowired
    private EmpMapper2 empMapper2;

    @ApiOperation("测试mybatis@select注解,通过test1数据库实现")
    @GetMapping("/getKing1")
    public List getKing1(){
        List<Emp> emps = empMapper1.selectList();
        return emps;
    };


    @ApiOperation("测试mybatis@select注解,通过test2数据库实现")
    @GetMapping("/getKing2")
    public List getKing2(){
        List<Emp> emps = empMapper2.selectList();
        return emps;
    };

    @ApiOperation("测试mybatis的mapper.xml文件调用,通过test1数据库实现")
    @GetMapping("/getKing3")
    public List getKing3(){
        List<Emp> emps = empMapper1.getAll();
        return emps;
    };

    @ApiOperation("测试mybatis的mapper.xml文件调用,通过test2数据库实现")
    @GetMapping("/getKing4")
    public List getKing4(){
        List<Emp> emps = empMapper2.getAll();
        return emps;
    };

    @ApiOperation("通过mp调用test1数据库实现查询")
    @GetMapping("/getKing5")
    public List getKing5(){
        List<Emp> emps = empMapper1.selectList(null);
        return emps;
    };

    @ApiOperation("通过mp调用test2数据库实现查询")
    @GetMapping("/getKing6")
    public List getKing6(){
        List<Emp> emps = empMapper2.selectList(null);
        return emps;
    };
}
  • @api等等注解都是swgger提供的一些注解,也就是让我们通过页面,可以看到这些注释,方便前端开发调用,不至于,动不动就问后台,提供开发效率。这不是本文的重点,不了解swagger的社友,可以去学习一下。
  • 模拟了调用2个库,通过三种方式实现。第一种,@select注解,第二种,通过mybatis的xml文件调用,第三种,通过MP提供的公有方法实现。也是考虑到,实际项目开发过程中,可能每一种都有可能用到。

启动类

package com.cxyxs.moredatasource;

import com.spring4all.swagger.EnableSwagger2Doc;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = {"com.cxyxs"})
@EnableSwagger2Doc
public class MoredatasourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(MoredatasourceApplication.class, args);
    }

}
  • @EnableSwagger2Doc 启动swagger

测试

输入网址http://localhost:8080/swagger-ui.html#/

  • 选择某个请求点击try out
  • 往下来,可以查看Response Body,获取对应后端返回的结果。

到这里我们springboot+mybatis-plus+多数据源+swagger就实现了。

多数据源事务

为什么要使用事务?

  • 例如隔壁小王给社长转账的100元,他的业务可以分为2步
  • 第一步:隔壁小王扣100元
  • 第二步:社长+100元。

如果第一步和第二步之间就报错勒,可能会存在隔壁小王扣了100,但是社长,也没有收到钱,隔壁小王赖上社长,这我跳到黄河都说不清。
为了保证社长的一世清白,这是我们的主角事务开始隆重登场。

简单的理解事务流程

  • START TRANSACTION开启事务
  • 隔壁小王update
  • 社长 update
  • commit 提交事务或者rollback回滚事务。

给多数据源代码添加事务

不指定某个事务

之前的内容,我们都是读的操作,不涉及到事务,本文,我们来试试写操作,并增加对应的事务来试试。
在TestController类中增加一个方法

 @ApiOperation("测试插入数据")
    @PostMapping("/saveEmp1")
    @Transactional
    public String saveEmp(Emp emp) {
        int insert = empMapper1.insert(emp);
        if(insert > 0){
            return "插入成功";
        }else{
            return "插入失败";
        }
    };

  • id是数据库自增主键,所以不需要设置值。
  • @Transactional我们都知道这个关键字是设置事务的。各位社友,觉得我直接启动项目,访问这个方法,可能会出现什么样的结果
    保存成功?
{
  "timestamp": "2020-03-08T11:53:42.234+0000",
  "status": 500,
  "error": "Internal Server Error",
  "message": "No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single matching bean but found 2: test1TransactionManager,test2TransactionManager",
  "trace": "org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single matching bean but found 2: test1TransactionManager,test2TransactionManager\r\n\tat org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1180)\r\n\tat org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:416)\r\n\tat org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)\r\n\tat org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)\r\n\tat org.springframework.transaction.interceptor.TransactionAspectSupport.determineTransactionManager(TransactionAspectSupport.java:480)\r\n\tat org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:335)\r\n\tat org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:99)\r\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)\r\n\tat org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)\r\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)\r\n\tat com.cxyxs.moredatasource.controller.TestController$$EnhancerBySpringCGLIB$$11aec56f.saveEmp(<generated>)\r\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\r\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\r\n\tat java.lang.reflect.Method.invoke(Method.java:498)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)\r\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:660)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:367)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1639)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\r\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)\r\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.lang.Thread.run(Thread.java:745)\r\n",
  "path": "/saveEmp1"
}
  • org.springframework.transaction.TransactionManager’ available: expected single matching bean but found 2,这个事关键的信息,意思说找到两个事务,就有点犯困惑,到底运行那个事务。
  • 还记得DataSourceConfig1类方法里面,我们配置了test1TransactionManager,我们指定一下,使用那个事务。

指定某个事务

还是我们的TestController类中增加一个方法

 @ApiOperation("测试给test1插入数据,增加指定某个事务的代码")
    @PostMapping("/saveEmp2")
    @Transactional(value = "test1TransactionManager")
    public String saveEmp2(Emp emp) {
        int insert = empMapper1.insert(emp);
        if(insert > 0){
            return "插入成功";
        }else{
            return "插入失败";
        }
    };


指定事务,模拟报错

 @ApiOperation("测试给test1插入数据,增加指定某个事务的代码,并故意在代码中报错")
    @PostMapping("/saveEmp3")
    @Transactional(value = "test1TransactionManager")
    public String saveEmp3(Emp emp) {
        int insert = empMapper1.insert(emp);
        //故意报错
        String str= null;
        System.out.println(str.toString());   //这里会报错
        if(insert > 0){
            return "插入成功";
        }else{
            return "插入失败";
        }
    };
  • str为null,再调用null.toString,肯定会报空指针,我们来断点跟踪一下。
  • insert的值为1,是不是表示,这个记录就写入到数据库了
    那我们来检查一下数据库表的身体,看看是否手脚干净。
  • 在方法上增加了事务,表示需要这个方法执行完,确保没问题后,才能调用commit方法,同步到数据库里。
    继续向下执行。

    -这里直接报错勒,到这里整个方法已经跑完,各位社友觉得数据是否写入成功
    再次检查数据库对应表的身体
  • 很干净,说明配置事务后,如果方法中间抛出异常,事务会回滚。

本文还未完,后续会持续更新
下次更新内容

如何解决多数据源事务问题,在实际项目开发过程中,会存在,在一个项目中,操作多个数据源的问题,在调用多个数据源过程中,我们如何保证事务的一致性,实际上,这个版本的代码,你可以再调用test1的插入方法,再调用test2库,执行插入方法。这两者调用之间弄一个报错,可以发现,test1库新增了一条数据,test2插入失败。

关注公众号"程序猿学社",回复关键字999,获取源码

程序猿学社的GitHub,欢迎Star
https://github.com/ITfqyd/cxyxs
本文已记录到github,形成对应专题。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章