上一篇文章《SpringBoot实现多数据源(五)【多数据源事务控制】》
六、dynamic-datasource 多数据源组件
官方文档:https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611
基于 SpringBoot 的多数据源组件,功能强悍,支持 Seata 分布式事务
- 支持 数据源分组,适用于多种场景,纯粹多库、读写分离、一主多从混合模式
- 支持数据源敏感配置信息加密 ENC()
- 支持每个数据库独立初始化表结构 scheme 和数据库 database
- 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)
- 支持 自定义注解,需继承 DS(3.2.0+)
- 提供并简化 Druid,HikariCp、BeeCp、Dbcp2的快速集成
- 提供对 Mybatis-Plus、Quartz、ShardingJdbc、P6sy、Jndi等组件的集成方案
- 提供 自定义数据源来源 方案(如全从数据库加载)
- 提供项目启动后 动态增加移除数据源 方案
- 提供 Mybatis 环境下的 纯读写分离 方案
- 提供使用 spel 动态参数解析数据源方案,内置 spel,session、header,支持自定义
- 支持 多层数据源嵌套切换(ServiceA >>> ServiceB >>> ServiceC)
- 提供基于 Seata 的分布式事务方案
- 提供本地数据源事务方案(附:不能和原生 Spring 事务混合)
约定
- 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD
- 配置文件所有以"_"下划线分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下
- 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换,默认使用轮询
- 默认的数据源名称为 master,你可以通过 spring.datasource.dynamic.primary 修改
- 方法上的注解优先于类上的注解
- DS 支持继承抽象类上的 DS,暂不支持继承接口上的 DS
DynamicDataSource 原理
- 通过 DynamicDataSourceAutoConfiguration 自动配置类
- 配置了 DynamicRoutingDataSource,相当于前面定义的 DynamicDataSource,用来动态提供数据源
- 配置了 DynamicDataSourceAnnotationAdvisor 就相当于前面定义的切面类
- 设置了 DynamicDataSourceAnnotationInterceptor,当前 advisor 的拦截器,可以理解为前面定义的环绕通知
- 当执行方法,会执行 DynamicDataSourceAnnotationInterceptor#invoke 来进行增强
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取当前@DS注解的value值
String dsKey = this.determineDatasourceKey(invocation);
// 设置当前数据源的标识ThreadLocal中
DynamicDataSourceContextHolder.push(dsKey);
Object var3;
try {
// 执行目标方法
var3 = invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
return var3;
}
- 在执行数据库操作的时候,就会调用DataSource.getConnection(),此时DataSource指的就是 DynamicRoutingDataSource
- 然后执行模板抽象方法 AbstractRoutingDataSource#determineDataSource,被 DynamicRoutingDataSource 重写后调用
// AbstractRoutingDataSource 抽象类方法
protected abstract DataSource determineDataSource();
public Connection getConnection() throws SQLException {
String xid = TransactionContext.getXID();
if (StringUtils.isEmpty(xid)) {
return this.determineDataSource().getConnection();
} else {
String ds = DynamicDataSourceContextHolder.peek();
ds = StringUtils.isEmpty(ds) ? "default" : ds;
ConnectionProxy connection = ConnectionFactory.getConnection(ds);
return (Connection)(connection == null ? this.getConnectionProxy(ds, this.determineDataSource().getConnection()) : connection);
}
}
// DynamicRoutingDataSource 类中的方法
public DataSource determineDataSource() {
// 拿到切换的数据源标识
String dsKey = DynamicDataSourceContextHolder.peek();
// 通过该表示获取对应的数据源
return this.getDataSource(dsKey);
}
用例测试
- 创建一个 dynamic_datasource_framework 的 SpringBoot 模块,并导入依赖
- pom.xml
<dependencies>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<!--dynamic-datasource-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
</dependencies>
- 应用配置文件
- application.yml
spring:
autoconfigure:
# 排除 Druid 自动配置
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
datasource:
dynamic:
# 设置默认的数据源或者数据源组,默认值即为master
primary: master
# 严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
strict: false
datasource:
master:
# 3.2.0开始支持SPI可省略此配置
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/write?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/read?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
# 指定使用 druid 数据源
druid:
# 连接池初始化大小
initial-size: 5
# 最小空闲连接数
min-idle: 10
# 最大连接数
max-active: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
mybatis:
mapper-locations: classpath:com/vinjcent/mapper/**/*.xml
type-aliases-package: com.vinjcent.pojo
- 实体类、Mapper层(Dao层)、Service层
- People
package com.vinjcent.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class People {
private String name;
}
- PeopleMapper
package com.vinjcent.mapper;
import com.vinjcent.pojo.People;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface PeopleMapper {
List<People> list();
boolean save(People people);
}
- PeopleServiceImpl(看方法写Service接口)
package com.vinjcent.service.impl;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.vinjcent.mapper.PeopleMapper;
import com.vinjcent.pojo.People;
import com.vinjcent.service.PeopleService;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class PeopleServiceImpl implements PeopleService {
private final PeopleMapper peopleMapper;
@Autowired
public PeopleServiceImpl(PeopleMapper peopleMapper) {
this.peopleMapper = peopleMapper;
}
/**
* 说明: 不能和原生 Spring 事务混合,不使用 @DSTransactional 注解无法开启事务,即事务不会生效
*/
// 从库,如果按照下划线命名方式配置多个,可以指定前缀即可.如slave_1、slave_2、slave3...,只需要设置salve即可,默认使用负载均衡算法
@DS("slave")
@Override
public List<People> list() {
return peopleMapper.list();
}
@DS("master")
@Override
public boolean mSave(People people) {
return peopleMapper.save(people);
}
@DS("slave")
@Override
public boolean sSave(People people) {
boolean save = peopleMapper.save(people);
return save;
}
@DSTransactional
public boolean save (People people) {
PeopleService peopleService = (PeopleService) AopContext.currentProxy();
peopleService.sSave(people);
peopleService.mSave(people);
// 模拟事务回滚
int a = 1 / 0;
return true;
}
}
- 主启动类,扫描mapper接口、暴露代理对象
package com.vinjcent;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.vinjcent.mapper")
@SpringBootApplication
public class DynamicDatasourceFrameworkApplication {
public static void main(String[] args) {
SpringApplication.run(DynamicDatasourceFrameworkApplication.class, args);
}
}
- 运行并测试接口
- PeopleController
package com.vinjcent.controller;
import com.vinjcent.pojo.People;
import com.vinjcent.service.PeopleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("people")
public class PeopleController {
private final PeopleService peopleService;
@Autowired
public PeopleController(PeopleService peopleService) {
this.peopleService = peopleService;
}
@GetMapping("/list")
public List<People> getAllPeople() {
return peopleService.list();
}
@GetMapping("/insert")
public String addPeople() {
peopleService.save(new People("vinjcent"));
return "添加成功";
}
}