简介

dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。

其支持 Jdk 1.7+, SpringBoot 1.5.x 2.x.x 3.x.x

特性

  • 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
  • 支持数据库敏感配置信息 加密(可自定义) ENC()。
  • 支持每个数据库独立初始化表结构schema和数据库database。
  • 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
  • 支持 自定义注解 ,需继承DS(3.2.0+)。
  • 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
  • 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
  • 提供 自定义数据源来源 方案(如全从数据库加载)。
  • 提供项目启动后 动态增加移除数据源 方案。
  • 提供Mybatis环境下的 纯读写分离 方案。
  • 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
  • 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
  • 提供 基于seata的分布式事务方案 。
  • 提供 本地多数据源事务方案。

快速入门 

第一步:pom.xml  引入dynamic-datasource-spring-boot-starter、

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

第二步:application.yml 配置多数据源

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://192.168.43.10:3306/bill?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://192.168.43.10:3306/usc?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver

第三步:使用@DS注解,切换数据源

@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解

注解结果
没有@DS默认数据源
@DS("dsName")dsName可以为组名也可以为具体某个库的名称

连接池集成 

在application.yml 配置文件中添加Hikari数据库连接池配置。

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://192.168.43.10:3306/bill?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://192.168.43.10:3306/usc?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:
        minimum-idle: 10
        maximum-pool-size: 20
        idle-timeout: 500000
        max-lifetime: 540000
        connection-timeout: 60000
        connection-test-query: select 1

dynamic-datasource 支持如下数据库连接池:beecp\dbcp2\druid\hikair 等常用数据库连接池。

Beecp 通用配置:

beecp:
      fairness: true
      initial-size: 5
      max-active: 20
      idleTimeout: 60000
      connectionTestSql: SELECT 1

dbcp2 通用配置:

dbcp2:
    initial-size:5
    min-idle:5
    max-idle:10
    max-open-prepared-statements:100

druid 通用配置:

druid:
      # 初始化大小
      initial-size: 5
      # 最小连接数
      min-idle: 10
      # 最大连接数
      max-active: 20
      # 获取连接时的最大等待时间
      max-wait: 60000
      # 一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      # 多久才进行一次检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置扩展插件:stat-监控统计,log4j-日志,wall-防火墙(防止SQL注入),去掉后,监控界面的sql无法统计
      filters: stat,wall
      # 检测连接是否有效的 SQL语句,为空时以下三个配置均无效
      validation-query: SELECT 1
      # 申请连接时执行validationQuery检测连接是否有效,默认true,开启后会降低性能
      test-on-borrow: true
      # 归还连接时执行validationQuery检测连接是否有效,默认false,开启后会降低性能
      test-on-return: true
      # 申请连接时如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效,默认false,建议开启,不影响性能
      test-while-idle: true
      # 是否开启 StatViewServlet
      stat-view-servlet:
        enabled: true
        # 访问监控页面 白名单,默认127.0.0.1
        allow: 127.0.0.1
        login-username: admin
        login-password: admin
      # FilterStat
      filter:
        stat:
          # 是否开启 FilterStat,默认true
          enabled: true
          # 是否开启 慢SQL 记录,默认false
          log-slow-sql: true
          # 慢 SQL 的标准,默认 3000,单位:毫秒
          slow-sql-millis: 5000
          # 合并多个连接池的监控数据,默认false
          merge-sql: false

hikair 通用配置:

 hikari:
        minimum-idle: 10
        maximum-pool-size: 20
        idle-timeout: 500000
        max-lifetime: 540000
        connection-timeout: 60000
        connection-test-query: select 1

温馨提示:上述配置对象属性值与 dynamic-datasource定义的属性值对象可能存在差异,请以:

com.baomidou.dynamic.datasource.spring.boot.autoconfigure.* 相关配置类对象为主 

验证 dynamic-datasource 数据库连接池是否生效。

在任意类中依赖DataSource 接口类,检查输出的接口实例类是否为:com.baomidou.dynamic.datasource.DynamicRoutingDataSource。

示例代码:

package com.zzg.controller;


import com.zzg.entity.BaseProjectPO;
import com.zzg.service.IBaseProjectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.sql.DataSource;
import java.util.List;

@RestController
@RequestMapping("/baseProject")
public class BaseProjectController {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private IBaseProjectService service;

    @GetMapping("/list")
    public List<BaseProjectPO> list() {
        System.out.println(dataSource.getClass());
        return service.list();
    }

    @GetMapping("{id}")
    public BaseProjectPO getBaseProjectInfo(@PathVariable String id){
        return service.getById(id);
    }
}

在list 逻辑方法中,输出DataSource 接口实例对象为:com.baomidou.dynamic.datasource.DynamicRoutingDataSource

第三方集成 /MyBatis-plus

在application.yml 和Application 程序入口添加如下配置:

application.yml

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.zzg.entity
Application
package com.zzg;

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

@SpringBootApplication
@MapperScan({"com.zzg.mapper"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

进阶使用

动态添加和移除数据源

示例:

1、定义数据源Model

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class DataSourceDTO {
    @NotBlank
    @ApiModelProperty(value = "连接池名称", example = "db1")
    private String poolName;
    @NotBlank
    @ApiModelProperty(value = "JDBC driver", example = "com.mysql.cj.jdbc.Driver")
    private String driverClassName;
    @NotBlank
    @ApiModelProperty(value = "JDBC url 地址", example = "jdbc:mysql://x.x.x.x:3306/x?useUnicode=true&characterEncoding=utf-8")
    private String url;
    @NotBlank
    @ApiModelProperty(value = "JDBC 用户名", example = "sa")
    private String username;
    @ApiModelProperty(value = "JDBC 密码")
    private String password;
}

2、Controller 定义添加和移除数据源

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.*;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.zzg.common.DataSourceDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
 
import javax.sql.DataSource;
import java.util.Set;
 
@RestController
@RequestMapping("/datasources")
@Api(tags = "添加删除数据源")
public class DataSourceController {
 
    @Autowired
    private DataSource dataSource;
    @Autowired
    private DefaultDataSourceCreator dataSourceCreator;
    @Autowired
    private DruidDataSourceCreator druidDataSourceCreator;
    @Autowired
    private HikariDataSourceCreator hikariDataSourceCreator;
 
    @GetMapping
    @ApiOperation("获取当前所有数据源")
    public Set<String> now() {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        return ds.getDataSources().keySet();
    }
 
    //通用数据源会根据maven中配置的连接池根据顺序依次选择。
    //默认的顺序为druid>hikaricp>beecp>dbcp>spring basic
    @PostMapping("/add")
    @ApiOperation("通用添加数据源(推荐)")
    public Set<String> add(@Validated @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        return ds.getDataSources().keySet();
    }
 
    @PostMapping("/addDruid")
    @ApiOperation("基础Druid数据源")
    public Set<String> addDruid(@Validated @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        dataSourceProperty.setLazy(true);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = druidDataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        return ds.getDataSources().keySet();
    }
 
    @PostMapping("/addHikariCP")
    @ApiOperation("基础HikariCP数据源")
    public Set<String> addHikariCP(@Validated @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = hikariDataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        return ds.getDataSources().keySet();
    }
 
    @DeleteMapping
    @ApiOperation("删除数据源")
    public String remove(String name) {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        ds.removeDataSource(name);
        return "删除成功";
    }
}

数据库加密

第一步:使用框架自带加密工具类com.baomidou.dynamic.datasource.toolkit.CryptoUtils对需要加密的字符串进行加密。

@Test
void test() throws Exception {
    String passWord= CryptoUtils.encrypt("123456");
    System.out.println(passWord);
    // 输出加密密码
 //Y3ycHCcZGa+N+OK+qXTWA0gJ1L1N+FYrswTgRQEegdKVTefiujYxjlytR6zOuV5Y3AifL/P10yWshYKQaqpkkQ==
}

第二步:修改配置文件中的相关密码

server:
  port: 8080
spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://192.168.43.10:3306/bill?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true
          username: root
          password: ENC(Y3ycHCcZGa+N+OK+qXTWA0gJ1L1N+FYrswTgRQEegdKVTefiujYxjlytR6zOuV5Y3AifL/P10yWshYKQaqpkkQ==)
          driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://192.168.43.10:3306/usc?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true
          username: root
          password: ENC(Y3ycHCcZGa+N+OK+qXTWA0gJ1L1N+FYrswTgRQEegdKVTefiujYxjlytR6zOuV5Y3AifL/P10yWshYKQaqpkkQ==)
          driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:
        minimum-idle: 10
        maximum-pool-size: 20
        idle-timeout: 500000
        max-lifetime: 540000
        connection-timeout: 60000
        connection-test-query: select 1
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.zzg.entity

自动读写分离

通过上面的学习,我们已经学习了解到了@DS 注解,基于此注解我们可以快速实现简单版本的读写分离。

package com.zzg.mapper;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zzg.entity.BaseArch;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface BaseArchMapper extends BaseMapper<BaseArch> {

    @DS("master")
    int addUser(BaseArch entity);

    @DS("slave_1")
    BaseArch findUser(@Param("id") String id);
}

不过这种方式有点繁琐,每个Mapper都需要添加注解。我们 可以通过MyBatis 拦截器 + 手动切换数据源实现读写分离。

定义:读写分离MyBatis 拦截器

@Intercepts({@Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
), @Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
)})
@Component
@Primary
public class MasterSlaveAutoRoutingPlugin implements Interceptor {

    private static final String MASTER = "master";

    private static final String SLAVE = "slave_1";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        try {
            DynamicDataSourceContextHolder.push(SqlCommandType.SELECT == ms.getSqlCommandType() ? SLAVE : MASTER);
            return invocation.proceed();
        } finally {
            DynamicDataSourceContextHolder.clear();
        }
    }

    @Override
    public Object plugin(Object target) {
        return target instanceof Executor ? Plugin.wrap(target, this) : target;
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

至此,我们可以移除Controller/Service/Dao/Mapper 上得@DS 注解。

手动切换数据源

请参考文章:多数据源切换[dynamic-datasource] 手动切换数据源

负载均衡 

通过 mybatis 的拦截器 + 手动切换数据源实现了读写分离,同时 dynamic-datasource 还为我们提供了负载的效果,同一个组下的默认就是负载均衡效果,怎么才是同一个组呢,上面有提到只需以下划线 _ 分割即可,下面修改配置文件:

spring:
  datasource:
    dynamic:
      primary: db_1 #设置默认的数据源或者数据源组,默认值即为master
      strict: true #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源.
      datasource:
        db_1:
          driver-class-name: com.mysql.cj.jdbc.Driver
          type: com.alibaba.druid.pool.DruidDataSource
          url: jdbc:mysql://192.168.43.10:3306/db1?useUnicode=true&characterEncoding=utf8
          username: root
          password: 123456
        db_2:
          driver-class-name: com.mysql.cj.jdbc.Driver
          type: com.alibaba.druid.pool.DruidDataSource
          url: jdbc:mysql://192.168.43.10:3306/db2?useUnicode=true&characterEncoding=utf8
          username: root
          password: 123456

声明 Dao ,指定数据源为 db :

@Mapper
@DS("db")
public interface UserMapper extends BaseMapper<UserEntity> {
}

 事务管理

大家都了解在 Spring 中事物使用 @Transactional 注解即可,但是仅针对于单个数据源的情况,多数据源下我们可以使用 jta 来控制或其他事务管理框架,在 dynamic-datasource 中又推出了 @DSTransactional 注解来代替 Spring 的 @Transactional 注解。

@Slf4j
@SpringBootTest
class DynamicDatasourceDemoApplicationTests {

    @Autowired
    DB1UserDao db1UserDao;

    @Autowired
    DB2UserDao db2UserDao;

    @Test
    @DSTransactional
    void test1() {
        UserEntity entity = new UserEntity();
        entity.setName("王五");
        entity.setAge(16);
        int db1 = db1UserDao.insert(entity);
        log.info("db1写入个数:{} ", db1);
        int db2 = db2UserDao.insert(entity);
        log.info("db2写入个数:{} ", db2);
        //模拟异常
        int a = 1 / 0;
    }
}

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐