简介
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;
    }
} 
                


















