一、简介
dynamic-datasourc是一个基于 SpringBoot 的快速集成多数据源的启动器,其主要特性如下:
- 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
- 支持数据库敏感配置信息 加密 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的分布式事务方案。
- 提供 本地多数据源事务方案
二、使用方法
第1步:引入
dynamic-datasource-spring-boot-starter或者
dynamic-datasource-spring-boot3-starter
<!-- 适合 SpringBoot 1.5.x + 2.x 版本 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
<!-- 适合 SpringBoot 3.x 版本 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${version}</version>
</dependency>
第2步:配置数据源
spring:
datasource:
dynamic:
enabled: true #启用动态数据源,默认true
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
grace-destroy: false #是否优雅关闭数据源,默认为false,设置为true时,关闭数据源时如果数据源中还存在活跃连接,至多等待10s后强制关闭
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
多主多从:
spring:
datasource:
dynamic:
datasource:
master_1:
master_2:
slave_1:
slave_2:
slave_3:
纯粹多库:
spring:
datasource:
dynamic:
datasource:
mysql:
oracle:
sqlserver:
postgresql:
h2:
混合配置:
spring:
datasource:
dynamic:
datasource:
master:
slave_1:
slave_2:
oracle_1:
oracle_2:
第3步:使用 @DS 切换数据源
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
三、案例:dynamic-datasource+mybatis使用方法
maven引入
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
配置好数据源
spring:
datasource:
dynamic:
primary: master1
strict: false
datasource:
master1:
url: jdbc:mysql://127.0.0.1:3306/master1?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
username: root
password: andrew
driver-class-name: com.mysql.cj.jdbc.Driver
master2:
url: jdbc:mysql://127.0.0.1:3306/master2?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
username: root
password: andrew
driver-class-name: com.mysql.cj.jdbc.Driver
master3:
url: jdbc:oracle:thin:@10.132.212.63:1688:TESTDB
username: flx
password: flx202108
driver-class-name: oracle.jdbc.OracleDriver
logging:
level:
com.xkcoding: debug
com.xkcoding.orm.mybatis.mapper: trace
server:
port: 8080
# servlet:
# context-path: /demo
mybatis:
type-aliases-package: com.orm.mybatis.dsannotation.entity
mapper-locations: classpath:mapper/*/*.xml
configuration:
map-underscore-to-camel-case: true
service层
里面在想要切换数据源的方法上加上@DS注解就行了,也可以加在整个service层上,方法上的注解优先于类上注解
@Service
public class UserServiceImpl {
@Resource
private UserMapper1 userMapper1;
@Resource
private UserMapper2 userMapper2;
@Resource
private AsusPoInfoMapper3 asusPoInfoMapper3;
@DS("master1")
public List<User> findAllUser(){
List<User> list = userMapper1.selectAllUser();
return list;
}
@DS("master2")
public List<User> findAllUser1(){
List<User> list = userMapper2.selectAllUser();
return list;
}
public User findUserById(Long id){
return userMapper1.selectUserById(id);
}
@Transactional //与dynamic不同的是,这两个注解可以一起使用会先切换数据源再事务
@DS("master1") //与dynamic不同的是,这两个注解可以一起使用会先切换数据源再事务
public void insertUser1(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = simpleDateFormat.format(new Date());
String UUID = java.util.UUID.randomUUID().toString().substring(0,5);
User user = User.builder().email("andrew@qq.com"+UUID).name("andrew"+UUID).password("123456"+UUID).phoneNumber("123"+UUID)
.lastUpdateTime(date).createTime(date).status(0).salt("password"+UUID).build();
userMapper1.saveUser(user);
}
@Transactional
@DS("master2")
public void insertUser2(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = simpleDateFormat.format(new Date());
String UUID = java.util.UUID.randomUUID().toString().substring(0,5);
User user = User.builder().email("andrew@qq.com"+UUID).name("andrew"+UUID).password("123456"+UUID).phoneNumber("123"+UUID)
.lastUpdateTime(date).createTime(date).status(0).salt("password"+UUID).build();
userMapper2.saveUser(user);
}
public void testTransitional() {
((UserServiceImpl)AopContext.currentProxy()).insertUser1();
((UserServiceImpl)AopContext.currentProxy()).insertUser2();
((UserServiceImpl)AopContext.currentProxy()).insertOracle();
}
@DS("master3")
public List<AsusPoInfo> selectOracle(){
return asusPoInfoMapper3.selectAllAsusPoInfo();
}
@DS("master3")//与dynamic不同的是,这两个注解可以一起使用会先切换数据源再事务
@Transactional
public void insertOracle(){
AsusPoInfo asusPoInfo = AsusPoInfo.builder().id(java.util.UUID.randomUUID().toString().substring(0,20))
.woNo("andrew").po("123456").poLine("poline").cPo("cpo123456").shipType("Direct").build();
asusPoInfoMapper3.insertAsusPoInfo(asusPoInfo);
}
}
测试多数据源回滚
package com.orm.mybatis.dsannotation;
import com.orm.mybatis.dsannotation.entity.AsusPoInfo;
import com.orm.mybatis.dsannotation.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import com.orm.mybatis.dsannotation.serviceImpl.UserServiceImpl;
import javax.annotation.Resource;
import java.util.List;
@SpringBootTest
class SpringbootMybatisDsannotationDatasourceApplicationTests {
//事务测试
@Resource
private UserServiceImpl userService;
@Test
void contextLoads1() {
List<AsusPoInfo> list = userService.selectOracle();
System.out.println(list);
}
@Test
void contextLoads2() {
List<User> list = userService.findAllUser();
System.out.println(list);
}
@Test
void contextLoads3() {
List<User> list = userService.findAllUser();
List<User> list1 = userService.findAllUser1();
List<AsusPoInfo> list2 = userService.selectOracle();
list.addAll(list1);
System.out.println(list);
System.out.println(list2);
}
@Test
void contextLoads4() {
userService.testTransitional();
}
}
切换数据源成功,而且事务能回滚,但如果是多数据源事务,只能回滚报错的数据源的事务。
四、方案的权衡
- 静态多数据源方案优势在于配置简单并且对业务代码的入侵性极小,缺点也显而易见:我们需要在系统中占用一些资源,而这些资源并不是一直需要,一定程度上会造成资源的浪费。如果你需要在一段业务代码中同时使用多个数据源的数据又要去考虑操作的原子性(事务)可以用spring的jta实现事务,那么这种方案无疑会适合你。
- (aop和dynamic)动态数据源(AbstractRoutingDataSource)方案配置上看起来配置会稍微复杂一些,但是很好的符合了“即拿即用,即用即还”的设计原则,我们把多个数据源看成了一个池子,然后进行消费。它的缺点正如上文所暴露的那样:我们往往需要在事务的需求下做出妥协。而且由于需要切换环境上下文,在高并发量的系统上进行资源竞争时容易发生死锁等活跃性问题。我们常用它来进行数据库的“读写分离”,不需要在一段业务中同时操作多个数据源。这种动态形式并不能用spring的jta实现,而且其他实现方式(seata等)虽然可以实现,但配置复杂且实用度不高。
- 如果需要使用事务,一定记得使用分布式事务进行Spring自带事务管理的替换,否则将无法进行一致性控制。