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