业务场景:
- 针对某些服务端端,一般都是单体部署,并没有严格按照微服务原则进行数据隔离,如何让这个单体服务去管理多个数据库?
- 如果使用ShardingSphere,那么在这个场景下,会不会太重了,他是做分库分表的,大部分业务场景只需要做简单的查询就可以了
- 一般像常见的电商后台项目,大多使用的MyBatis-plus框架访问数据库。对于MyBatis和MyBatis-plus框架,这里不多做介绍了。跨多个数据库管理的后台数据。这里分享三种常用的多数据源管理方案:
一、使用Spring提供的AbstractRoutingDataSource
这种方式的核心是使用Spring提供的AbstractRoutingDataSource抽象类,注入多个数据源。
1、配置多数据源
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
datasource1:
url: jdbc:mysql://127.0.0.1:3306/datasource1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: root123456
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
datasource2:
url: jdbc:mysql://127.0.0.1:3306/datasource2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: root123456
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
2、配置类
/***
* @Author 摸鱼码长
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
public DataSourceTransactionManager transactionManager1(DynamicDataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
@Bean
public DataSourceTransactionManager transactionManager2(DynamicDataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
3、为targetDataSources初始化所有数据源
/***
* @Author 摸鱼码长
*/
@Component
@Primary // 将该Bean设置为主要注入Bean
public class DynamicDataSource extends AbstractRoutingDataSource {
// 当前使用的数据源标识
public static ThreadLocal<String> name=new ThreadLocal<>();
// 库1
@Autowired
DataSource dataSource1;
// 库2
@Autowired
DataSource dataSource2;
// 返回当前数据源标识
@Override
protected Object determineCurrentLookupKey() {
return name.get();
}
@Override
public void afterPropertiesSet() {
// 为targetDataSources初始化所有数据源
Map<Object, Object> targetDataSources=new HashMap<>();
targetDataSources.put("s1",dataSource1);
targetDataSources.put("s2",dataSource2);
super.setTargetDataSources(targetDataSources);
// 为defaultTargetDataSource 设置默认的数据源
super.setDefaultTargetDataSource(dataSource1);
super.afterPropertiesSet();
}
}
思考:
如果要切换数据源,我要在每个方法都写上这么一个DynamicDataSource.name.set(name);那么对业务的侵入是不是太重了?更优雅的解决方案,使用切片!
4、封装成切面处理
@Component
@Aspect
public class DynamicDataSourceAspect implements Ordered {
// 在每个访问数据库的方法执行前执行。(扫描这个包下有没有WR注解这个方法)
@Before("within(com.tuling.dynamic.datasource.service.impl.*) && @annotation(wr)")
public void before(JoinPoint point, WR wr){
// 在开始之前找到wr这个value,然后到DynamicDataSource进行切换
String name = wr.value();
DynamicDataSource.name.set(name);
System.out.println(name);
}
@Override
public int getOrder() {
return 0;
}
}
/***
* @Author 摸鱼码长
*
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {
String value() default "s1";
}
二、使用MyBatis注册多个SqlSessionFactory
mybatis其实他是只支持单数据源的?那么他的注入方式是什么呢?其实是通过他后台注入的SqlSessionFactory对象,他底层会引入spring容器中的DataSource组件来构建一个SqlSessionFactory还会构建一个DataSourceTransactionManager这些对象。
那么我们想让mybatis管理多个数据源怎么办?
只能我们自己去重新注册这些组件,让mybatis针对每一套数据源重新组装一个mybatis出来,就需要将MyBatis底层的DataSource、SqlSessionFactory、DataSourceTransactionManager这些核心对象一并进行手动注册。一一对应不同配置,再对应扫描不同数据源
@Configuration
@MapperScan(basePackages = "com.snow.datasource.dynamic.mybatis.mapper.r",sqlSessionFactoryRef="s1SqlSessionFactory")
public class RMybatisConfig {
// 第一步,构建一个DataSource
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
// 第二步,构建一个SqlSessionFactory
@Bean
@Primary
public SqlSessionFactory s1SqlSessionFactory() throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource2());
return sessionFactory.getObject();
}
// 第三步,构建事务管理器
@Bean
public DataSourceTransactionManager s2TransactionManager(){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource2());
return dataSourceTransactionManager;
}
@Bean
public TransactionTemplate s1TransactionTemplate(){
return new TransactionTemplate(s1TransactionManager());
}
@Bean
public TransactionTemplate s2TransactionTemplate(){
return new TransactionTemplate(s2TransactionManager());
}
}
这样,在业务代码里,就可以通过引入不同的mapper来实现写入不同的数据源
public void sava1(Save1DTO dto){
dto.setUuid = IdWorker.get32UUID();
dto.setPath = "/pathS1";
gen1Mapper.save(dto);
}
public void sava2(Save2DTO dto){
dto.setUuid = IdWorker.get32UUID();
dto.setPath = "/pathS2";
gen2Mapper.save(dto);
}
但是,多数据源中,事务的情况怎么处理呢?
像以下代码的情况,会怎么样,虽然我加了@Transactional,思考一下?
只有抛异常,因为底层也不知道事务是哪个数据源
@Transactional(rollbackFor = Exception.class)
public boolean save(){
Save1DTO dto = new Save1DTO();
dto.setUuid = IdWorker.get32UUID();
dto.setPath("/newPath");
saveService.save(dto);
int i = 1/0;
return true;
}
那么如何解决事务的问题?
显而易见,我们必须得进到@Transactional这个注解里面,看看他源码是如何处理的。
没有指定事务管理器他走的默认的.....
所以,指定我们自己写的事务管理器就行
@Transactional(rollbackFor = Exception.class, value = "s1TransactionManager")
public boolean save(){
Save1DTO dto = new Save1DTO();
dto.setUuid = IdWorker.get32UUID();
dto.setPath("/newPath");
saveService.save(dto);
int i = 1/0;
return true;
}
再思考,一个方法中即有库1数据源,也有库2数据源,该如何处理事务?
idea直接提示报错
很简单,把一个方法拆成2个方法
- 这不就是简单的实现了一个分布式事务嘛!大的不行就先拆成小的
思考🤔:这种方式有什么弊端?
是不是每次针对不同的数据源我们要写很多的配置尼 ?
三、使用dynamic-datasource框架
dynamic-datasource是MyBaits-plus作者设计的一个多数据源开源方案。使用这个框架非常简单,只需要引入对应的pom依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
这样就可以在SpringBoot的配置文件中直接配置多个数据源
这样就配置完成了master和slave_1两个数据库。
接下来在使用时,只要在对应的方法或者类上添加@DS注解(在方法上面、类上面都可以)即可。例如
@Service
public class GenServiceImpl {
@Autowired
private GenMapper genMapper;
@DS("slave") // 从库, 如果按照下划线命名方式配置多个 , 可以指定前缀即可(组名)
public List<SaveDTO> list() {
return genMapper.list();
}
@DS("master")
public void save(SaveDTO dto) {
genMapper.save(dto);
}
@DS("master")
@DSTransactional
public void saveAll(){
// 执行多数据源的操作
}
}
思考🤔:这种操作非常简单,那这么做就完啦?
NO NO NO .....
盲猜,最大的问题,那么只能是:事务的问题!
对liao!
虽然有这个注解,但是只是单数据源.....
如何在此场景下做多数据源的回滚?
查看源码的自动配置类分析:
拦截怎么做的呢?找他的invoke方法
来继续分析他底层源码:
进入notify之后,他内部做了一些转换,然后继续进notify方法
走到这一步,已是不易,核心就剩connection这个东西了。这个东西呢,也不是啥高级的,就是最基础的 java.sql下的,最基础jdbc操作里的
他的本质就是通过这个DataSouce去拿到这个connection。
如果我们做应该怎么做?
是不是应该拿到所有数据源的connection,然后来做回滚。思路都有了,接下来就不用再讲了吧。
四、针对多租户场景,如何做?
那么是不是可以从这个里面得到启发,核心就是,用户绑定不同数据源,那么,可不可以把用户和数据源之间的关联关系,保存下来,在进行切数据源操作的时候,匹配一下当前的用户