🚀 有来作者主页
- 主页:有来技术
🔥 开源项目
🌺 仓库主页
💖 欢迎点赞、收藏、留言。如有错误,敬请指正!
前言
本篇文章介绍了如何在 youlai-boot 中接入多数据源。
群里小伙伴在使用youlai-boot接入多数据源时,遇到了很多问题,借此机会写一篇博客,介绍一下使用youlai-boot对接多数据源的方法
引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>4.3.1</version>
</dependency>
注意,因为youlai-boot使用的springboot版本是3.3.4,所以引入的依赖必须为spring-boot3。以下是引入的springboot的依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/>
</parent>
接入步骤
1. 创建数据库和表
创建两个库 youlai_boot
和 youlai_boot1
,演示表为 dynamic_datasource_test
。两个表结构相同,但数据不同。
图中分别为youlai_boot和youlai_boot1库,演示表为dynamic_datasource_test。
表结构相同,为了方便演示,这里我手动向两个不同的表塞入了不同的数据。id都为1
2. 在配置文件里引入多个数据源,
spring:
jackson:
## 默认序列化时间格式
date-format: yyyy-MM-dd HH:mm:ss
## 默认序列化时区
time-zone: GMT+8
datasource:
dynamic:
primary: 'master' # 默认数据源
strict: false # 是否严格检查数据源,false 表示不严格检查
datasource:
master:
url: jdbc:mysql://localhost:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/youlai_boot1?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
3. 演示代码
- DynamicDatasourceModel.java
package com.youlai.boot.modules.demo.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 动态数据源-业务模块演示实体
*
* @author Theo
* @since 2024/11/04
*/
@Data
@TableName("dynamic_datasource_test")
public class DynamicDatasourceModel {
private Long id;
private String tempValue;
}
- DynamicDatasourceController.java
package com.youlai.boot.modules.demo.controller;
import com.youlai.boot.modules.demo.service.DynamicDatasourceService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 动态数据源-业务模块演示Controller
*
* @author Theo
* @since 2024/11/04
*/
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/dynamic/datasource")
public class DynamicDatasourceController {
private final DynamicDatasourceService dynamicDatasourceService;
@GetMapping("/test")
public String test() {
return dynamicDatasourceService.test();
}
@GetMapping("/test1")
public String test1() {
return dynamicDatasourceService.test1();
}
}
- DynamicDatasourceService.java
package com.youlai.boot.modules.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.modules.demo.model.DynamicDatasourceModel;
/**
* 动态数据源-业务模块演示Service
*
* @author Theo
* @since 2024/11/04
*/
public interface DynamicDatasourceService extends IService<DynamicDatasourceModel> {
String test();
String test1();
}
- DynamicDatasourceServiceImpl.java
package com.youlai.boot.modules.demo.service.impl;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.modules.demo.mapper.DynamicDatasourceMapper;
import com.youlai.boot.modules.demo.model.DynamicDatasourceModel;
import com.youlai.boot.modules.demo.service.DynamicDatasourceService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 动态数据源-业务模块演示ServiceImpl
*
* @author Theo
* @since 2024/11/04
*/
@Service
@RequiredArgsConstructor
public class DynamicDatasourceServiceImpl extends ServiceImpl<DynamicDatasourceMapper, DynamicDatasourceModel> implements DynamicDatasourceService {
@DS("master")
@Override
public String test() {
return this.getById(1L).getTempValue();
}
@DS("slave")
@Override
public String test1() {
return this.getById(1L).getTempValue();
}
}
- DynamicDatasourceMapper.java
package com.youlai.boot.modules.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.modules.demo.model.DynamicDatasourceModel;
import org.apache.ibatis.annotations.Mapper;
/**
* 动态数据源-业务模块演示
*
* @author Theo
* @since 2024/11/04
*/
@Mapper
public interface DynamicDatasourceMapper extends BaseMapper<DynamicDatasourceModel> {
}
- DynamicDatasourceMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.boot.modules.demo.mapper.DynamicDatasourceMapper">
</mapper>
为了方便演示,我给DynamicDatasourceController的访问地址配置到security的白名单中
4. 启动项目
在自动项目之后,日志显示两个数据源已成功初始化。
5. 访问两个演示地址
访问 /dynamic/datasource/test
和 /dynamic/datasource/test1
至此,动态数据源配置已经完全结束
总结
- 依赖引入:根据项目的 Spring Boot 版本,选择适合的依赖。
如果所使用的Springboot版本小于等于2.7.3,则引入的依赖为
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
</dependency>
当前文章引入dynamic-datasource-spring-boot3-starter是因为使用的为youlai-boot项目
- 使用注解
@DS
:切换数据源时使用。
@DS("slave")
@Override
public String test1() {
return this.getById(1L).getTempValue();
}
- 代码切换数据源:
@Override
public String test1() {
try {
DynamicDataSourceContextHolder.push("slave");
return this.getById(1L).getTempValue();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
根据DynamicDataSourceContextHolder的源码我们可以看到
/**
* 设置当前线程数据源
* <p>
* 如非必要不要手动调用,调用后确保最终清除
* </p>
*
* @param ds 数据源名称
* @return 数据源名称
*/
public static String push(String ds) {
String dataSourceStr = DsStrUtils.isEmpty(ds) ? "" : ds;
LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
return dataSourceStr;
}
所以我们在手动更改数据源之后,尽可能的保证执行poll方法
- 事务管理:
- 使用
@DSTransactional
注解来保证多个数据源下的事务一致性。
如果使用了多个数据源进行了增删改操作,则我们不能再使用@Transactional注解来保证事物的一致性了,因为已经涉及到两个数据库。
改用@DSTransactional注解
对应的切面文件在这里
可以看到,实现方式为通过不同的线程来执行不同库的操作,最终确定是否提交事物
- 请注意数据库表的存储引擎。MyISAM不支持事物。
动态添加数据源
需求描述:
当你项目的数据量特别大,已经达到了千万甚至更高的级别,这种情况下,领导让你根据这些用户的手机号进行取模,根据得到的结果,给不同的用户分别放到不同的库里。这样的好处是,如果我有更多的用户,我可以根据手机号对不同的数字取模,就可以分更多的库。这种情况下,你如果把这些数据库的配置都写到配置文件里,显然是非常不合适的。
解决方法
可以创建一个表,分别给这些数据源起好名,例如number_1
,number_2
,分别对应哪个数据库,在启动的时候可以将这些数据源读出来,使用DynamicRoutingDataSource的addDataSource方法,给这些数据源加载进这些动态数据源里。
LocalContextAware.java
package com.youlai.boot.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.youlai.boot.system.model.entity.DynamicDs;
import com.youlai.boot.system.service.DynamicDsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 项目启动时运行的类 会执行setApplicationContext方法
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class LocalContextAware implements ApplicationContextAware {
private final DynamicDsService dynamicDsService;
private final DynamicRoutingDataSource dynamicRoutingDataSource;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
try {
List<DynamicDs> list = dynamicDsService.list();
int i = 0;
for (DynamicDs dynamicDs : list) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl(dynamicDs.getDbUrl());
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUsername(dynamicDs.getDbUser());
druidDataSource.setPassword(dynamicDs.getDbPassword());
druidDataSource.setMaxActive(20);
druidDataSource.setMaxWait(60000L);
druidDataSource.setMinIdle(5);
druidDataSource.setMaxCreateTaskCount(5);
druidDataSource.setBreakAfterAcquireFailure(true);
druidDataSource.setOnFatalErrorMaxActive(3);
druidDataSource.init();
dynamicRoutingDataSource.addDataSource(dynamicDs.getDbName(),druidDataSource);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
代码中DynamicDsService只是为了读取
dynamic_ds
表中的数据源信息,这里就不再过多展示了
这样,数据库就动态的添加到了我们的项目中,使用方式和演示代码部分没有任何区别,事物的方式也是一致的。下面是我测试的部分代码和结果