前言

Springboot多数据源整合有许多显著的好处和作用,尤其在SaaS(软件及服务),还有以数据为驱动的决策和分析中非常重要。

一、多数据源是什么?

示例:多数据源是指在一个应用程序中配置和使用多个不同的数据库连接池,以便连接和操作多个数据库。这在需要从不同数据库中获取数据或将数据写入不同数据库的场景中非常有用。例如,一个应用程序可能需要从一个数据库中读取数据,同时将处理后的数据写入另一个数据库。

二、使用步骤

1.配置

其中使用的数据库连接池为druid连接池使用redis存储数据库的名称,用户名和密码
在这里测试使用(test)主库作为主库,在主库中维护一张数据库表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
create table sys_ds_manage
(
id bigint auto_increment
primary key,
port_number varchar(255) not null comment '端口号',
port_ds_name varchar(255) not null comment '数据库名',
port_name_exegesis varchar(255) not null comment '数据库注释',
port_ds_username varchar(255) not null comment '数据库用户',
port_ds_password varchar(255) not null comment '数据库密码',
create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
status tinyint default 0 not null comment '0表示正常,1表示停用',
build_ds tinyint default 0 not null comment '0表示未建库,1表示已建库',
is_deleted tinyint default 0 not null comment '删除标记(0:可用 1:不可用)',
constraint ds_manage_pk2
unique (id)
)
comment '数据库表';


Maven导入:
dynamic: 多数据源配置
redis:做键值存储
druid:为数据库连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.15</version>
</dependency>

yml配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
spring:
redis:
host: localhost
port: 6379
database: 0
timeout: 1800000
password:
jedis:
pool:
max-active: 20 #最大连接数
max-wait: -1 #最大阻塞等待时间(负数表示没限制)
max-idle: 5 #最大空闲
min-idle: 0
datasource:
druid:
statViewServlet:
enabled: true
login-username: admin
login-password: 123456
url-pattern: /druid/*
web-stat-filter:
enabled: true
session-stat-enable: true
session-stat-max-count: 1000
filter:
stat:
enabled: true
log-slow-sql: true
slow-sql-millis: 10
dynamic:
primary: master
strict: false
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
username: *** #自己的数据库连接账户
password: *** #自己的数据库连接密码
# test为主数据库
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
type: com.alibaba.druid.pool.DruidDataSource

2.java代码实现

代码如下:
设置一个自定义注解,该注解标注的位置在(service实现类)serviceImpl的方法上

1
2
3
4
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface SaaS {
}

注解配置类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
@Aspect
public class SaasAspect {

@Resource
private RedisTemplate<String ,Object> redisTemplate;

@Pointcut(value = "@annotation(com.demo.config.aop.saas.SaaS)")
private void sa(){}

@Before(value = "sa()")
public Object SaasAs(){
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
if (Objects.isNull(requestAttributes)){
throw new OaException(ResultCodeEnum.FAIL);
}
HttpServletRequest request = requestAttributes.getRequest();
String keyToken = request.getSession().getServletContext().getAttribute("dsManageKeyToken").toString();
String dskey = Objects.requireNonNull(redisTemplate.opsForValue().get("dsKey:"+keyToken)).toString();
request.getSession().setAttribute("test",dskey);
return true;
}
}

创建一个初始化数据库连接工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DsManageUtil {

// 选择数据库
public static void chooseDsMange(String dsManage) {
ServletRequestAttributes servletRequestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert servletRequestAttributes != null;
HttpServletRequest request = servletRequestAttributes.getRequest();
request.getSession().removeAttribute("test");
request.getSession().setAttribute("test", dsManage);
}
}

初始化数据库连接工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class InitDBUtil {
// 初始化数据源
public static DruidDataSource getInitDBUtil() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
return dataSource;
}


// 创建数据库
public static Boolean createDataSourceUtil(String username, String password, String portNumber, String DBName) {
try {
// 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
String oldUrl = "jdbc:mysql://#{url}/";
String replaceUrl = oldUrl.replace("#{url}", portNumber);
// 创建连接
Connection conn = DriverManager.getConnection(
replaceUrl,
username,
password);
Statement st = conn.createStatement();
// 编写sql语句
String oldDataCraeteSql = "create database #{name}";
String oldDataUseSql = "use #{name}";
// 使用sql语句,创建数据库
String replaceOldDataCraeteSql = oldDataCraeteSql.replace("#{name}", DBName);
st.executeUpdate(replaceOldDataCraeteSql);
// 使用数据库
String replaceOldDataUseSql = oldDataUseSql.replace("#{name}", DBName);
st.executeUpdate(replaceOldDataUseSql);
// 创建sql执行器
ScriptRunner scriptRunner = new ScriptRunner(conn);
// 错误停止执行
scriptRunner.setStopOnError(true);
// 读取sql文件该文件路径为主数据库的表结构
// 在该表结构中不包括存储数据库连接信息的表
FileInputStream fileInputStream = new FileInputStream(
"admin/src/main/resources/***.sql");
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
// 执行sql文件
scriptRunner.runScript(inputStreamReader);

// 关闭连接
inputStreamReader.close();
fileInputStream.close();
st.close();
conn.close();
return true;
} catch (Exception e) {
throw new RuntimeException(e);
}
}


/**
* 测试链接数据库
* @return
*/
public static Boolean getConnection(DsManageDTO dsManageDTO) {
Connection conn = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://" +
dsManageDTO.getIpNum() +
":" +
dsManageDTO.getPortNum() +
"/" +
dsManageDTO.getPortDsName() +
"?user=" +
dsManageDTO.getPortDsUsername() +
"&password=" +
dsManageDTO.getPortDsPassword();
conn = DriverManager.getConnection(url);
System.out.println("成功连接数据库!!!");
} catch (Exception ex1) {
throw new OaException(ResultCodeEnum.FAIL);
} finally {
assert conn != null;
try {
System.out.println("断开数据库连接");
conn.close();
} catch (SQLException e) {
log.error(e.toString());
}
}
return true;
}
}

多数据源配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Component
@Slf4j
public class DynamicDataSourceConfig {

@Resource
private DynamicRoutingDataSource dataSource;

//动态接口类
@Resource
private DsManageService dsManageService;


/**
* &#064;PostConstruct被
* &#064;PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器调用一次,
* 类似于Serclet的init()方法。被
* &#064;PostConstruct修饰的方法会在构造函数之后,init()方法之前运行。
*/
@PostConstruct
public void LoadAllDB() {
try {
DruidDataSource druidDataSource=null;
// 查询到所有状态为0 的数据库表
List<DsManage> dsManageList = dsManageService.list(
new LambdaQueryWrapper<DsManage>()
.eq(DsManage::getStatus, 0));
// 在test数据库中查找DsManage表中的数据
for (DsManage dsManage : dsManageList) {
// 创建连接池
druidDataSource = InitDBUtil.getInitDBUtil();
// 数据库url
druidDataSource.setUrl("jdbc:mysql://" + dsManage.getPortNumber() + "/" + dsManage.getPortDsName());
// 数据库用户名
druidDataSource.setUsername(dsManage.getPortDsUsername());
// 数据库密码
druidDataSource.setPassword(dsManage.getPortDsPassword());
// 将数据库的信息加载到连接中
dataSource.addDataSource(dsManage.getPortDsName(), druidDataSource);
log.info(dsManage.getPortDsName()+">>>>>>>>>>连接成功");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

Mapper接口如下

1
2
3
4
@DS("test")
public interface DsManageMapper extends BaseMapper<DsManage> {

}

Service接口

1
2
3
4
5
6
7
8
9
10
public interface DsManageService extends IService<DsManage> {

boolean delete(Long id);

Map<String,Object> selectByPage(Page<DsManage> page, SearchDsManageDTO searchDsManageDTO);

DsManageVO selectByOne(Long id);

Boolean initMySql(Long id);
}

serviceImpl实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@Service("dsManageService")
@DSTransactional
public class DsManageServiceImpl extends ServiceImpl<DsManageMapper, DsManage> implements DsManageService {

@Resource
private EmailService emailService;

@Resource
private RedisTemplate<String,Object> redisTemplate;

@Override
public Map<String, Object> selectByPage(Page<DsManage> page, SearchDsManageDTO searchDsManageDTO) {
LambdaQueryWrapper<DsManage> wrapper = new LambdaQueryWrapper<>();
if (!Objects.isNull(searchDsManageDTO.getPortNameExegesis())) {
wrapper.like(DsManage::getPortNameExegesis,
searchDsManageDTO.getPortNameExegesis()
);
}
if (!Objects.equals(searchDsManageDTO.getCreateTimeBegin(),null)){
wrapper.ge(DsManage::getCreateTime,searchDsManageDTO.getCreateTimeBegin());
}
if (!Objects.equals(searchDsManageDTO.getCreateTimeEnd(),null)){
wrapper.le(DsManage::getCreateTime,searchDsManageDTO.getCreateTimeEnd());
}
wrapper.eq(DsManage::getIsDeleted, 0);
Page<DsManage> selectPage = baseMapper.selectPage(page, wrapper);
List<DsManage> dsManageList = selectPage.getRecords();

List<DsManageVO> dsManageVOList = new ArrayList<>();

for (DsManage dsManage : dsManageList) {
DsManageVO dsManageVO = getDsManageVO(dsManage);
dsManageVOList.add(dsManageVO);
}
Map<String, Object> map = new HashMap<>();
map.put("total", page.getTotal());
map.put("rows", dsManageVOList);
return map;
}

@Override
public DsManageVO selectByOne(Long id) {
DsManage dsManage = baseMapper.selectById(id);
return getDsManageVO(dsManage);
}

@Override
public boolean delete(Long id) {
return baseMapper.deleteById(id)>0;
}

@Override
public Boolean initMySql(Long id) {
DsManage dsManage = baseMapper.selectById(id);
if (Objects.equals(dsManage.getBuildDs(), 1)) {
return false;
}
dsManage.setBuildDs(1);
baseMapper.updateById(dsManage);

ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
assert requestAttributes != null;
String token = requestAttributes.getRequest().getHeader("token");
Object object = redisTemplate.opsForValue().get("user:" + token);

User user = JSONUtil.toBean(JSONUtil.toJsonStr(object), User.class);

EmailVo emailVo = new EmailVo();
emailVo.setFromMail(user.getEmail());
emailVo.setSubject("数据库建库操作");
emailVo.setText(dsManage.getPortDsName() + ">>>>>>>>>>>已经完成建立数据库和数据库初始化");
emailService.sendMail(emailVo);

return InitDBUtil.createDataSourceUtil(
dsManage.getPortDsUsername(),
dsManage.getPortDsPassword(),
dsManage.getPortNumber(),
dsManage.getPortDsName());
}

private static DsManageVO getDsManageVO(DsManage dsManage) {
DsManageVO dsManageVO = BeanUtil.copyProperties(dsManage, DsManageVO.class);
String dsPortDsName = dsManage.getPortNumber();
String[] split = dsPortDsName.split(":");
dsManageVO.setIpNum(split[0]);
dsManageVO.setPortNum(split[1]);
return dsManageVO;
}

}

数据库映射实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DsManageVO implements Serializable {

private Long id;
//ip地址
private String ipNum;
//端口号
private String portNum;
//数据库名
private String portDsName;
//数据库注释
private String portNameExegesis;
//数据库用户
private String portDsUsername;
//数据库密码
private String portDsPassword;
//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
//更新时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
//建库 0表示未建库,1表示已建库
private Integer buildDs;
//状态标记(0:正常 1:停用)
private Integer status;
}

前端传值实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DsManageDTO implements Serializable {
private Integer id;
//ip地址
private String ipNum;
//端口号
private String portNum;
//数据库名
private String portDsName;
//数据库注释
private String portNameExegesis;
//数据库用户
private String portDsUsername;
//数据库密码
private String portDsPassword;

private Integer status;

//建库 0表示未建库,1表示已建库
private Integer buildDs;
}

Controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
@RestController
@RequestMapping("dsManage")
@CrossOrigin
public class DsManageController{
/**
* 服务对象
*/
@Resource
private DsManageService dsManageService;

/**
* 查询所有未被删除的暂存数据库表单
* @return
*/
@GetMapping()
public Result<?> selectAll(){
List<DsManage> dsManageList = dsManageService.list(new LambdaQueryWrapper<DsManage>()
.eq(DsManage::getIsDeleted, 0)
.eq(DsManage::getStatus,0)
);
return Result.ok(dsManageList);
}

/**
* 查询所有未被删除和已建库的数据库表单
* @return
*/
@GetMapping("selectBuildAll")
public Result<?> selectBuildAll(){
List<DsManage> dsManageList = dsManageService.list(new LambdaQueryWrapper<DsManage>()
.eq(DsManage::getStatus, 0)
.eq(DsManage::getBuildDs, 1)
);
ArrayList<Object> list = new ArrayList<>();
for (DsManage dsManage : dsManageList) {
Map<String,Object> map=new HashMap<>();
map.put("text",dsManage.getPortNameExegesis());
map.put("value",dsManage.getPortDsName());
list.add(map);
}
return Result.ok(list);
}

/**
* 根据数据库id查询当前用户数据
* @param id
* @return
*/
@GetMapping("{id}")
@LogPoint(businessName = "根据数据库id查询当前用户数据",businessAction = "selectByOne")
public Result<?> selectByOne(@PathVariable("id") Long id){
DsManageVO dsManageVO=dsManageService.selectByOne(id);
return Result.ok(dsManageVO);
}

@PostMapping("/{pageNum}/{pageSize}")
@LogPoint(businessName = "分页查询数据库信息",businessAction = "selectByPage")
public Result<?> selectByPage(
@RequestBody SearchDsManageDTO searchDsManageDTO,
@PathVariable("pageNum")Long pageNum,
@PathVariable("pageSize")Long pageSize
){
Page<DsManage> page = new Page<>(pageNum, pageSize);
Map<String,Object> dsManageMap=dsManageService.selectByPage(page, searchDsManageDTO);
return Result.ok(dsManageMap);
}

/**
* 增加数据库表的数据
* @param dsManageDTO
* @return
*/
@PostMapping()
@LogPoint(businessName = "增加数据库信息",businessAction = "save")
public Result<?> save(@RequestBody DsManageDTO dsManageDTO){
DsManage dsManage = BeanCopyDsManage(dsManageDTO);
List<String> portDsNameList = dsManageService
.list(new LambdaQueryWrapper<DsManage>()
.eq(DsManage::getIsDeleted, 0))
.stream()
.map(DsManage::getPortDsName)
.collect(Collectors.toList());
if (portDsNameList.contains(dsManage.getPortDsName())){
throw new OaException(ResultCodeEnum.DSMANAGE_ON);
}
return dsManageService.save(dsManage)?
Result.ok():
Result.fail();
}

@PutMapping("updateStatus")
@LogPoint(businessName = "修改数据库状态",businessAction = "updateStatus")
public Result<?> updateStatus(@RequestBody DsManageDTO dsManageDTO){
DsManage dsManage = BeanUtil.copyProperties(dsManageDTO, DsManage.class);
return dsManageService.updateById(dsManage)?
Result.ok():
Result.fail();
}

/**
* 修改数据库表中的数据
* @param dsManageDTO
* @return
*/
@PutMapping()
@LogPoint(businessName = "修改数据库信息",businessAction = "update")
public Result<?> update(@RequestBody DsManageDTO dsManageDTO){
DsManage dsManage = BeanCopyDsManage(dsManageDTO);
return dsManageService.updateById(dsManage)?
Result.ok():
Result.fail();
}

/**
* 逻辑删除数据库表的信息
* @param id
* @return
*/
@DeleteMapping("/{id}")
@LogPoint(businessName = "逻辑删除数据库表的信息",businessAction = "remove")
public Result<?> remove(@PathVariable("id") Long id){
return dsManageService.delete(id)?
Result.ok():
Result.fail();
}


/**
* 复制数据库表的实体类
* @param dsManageDTO
* @return
*/
private static DsManage BeanCopyDsManage(DsManageDTO dsManageDTO) {
DsManage dsManage = BeanUtil.copyProperties(dsManageDTO, DsManage.class);
//ip地址
String ipNum = dsManageDTO.getIpNum();
//端口号
String portNum = dsManageDTO.getPortNum();
dsManage.setPortNumber(ipNum+":"+portNum);
return dsManage;
}

/**
* 初始化数据库
* @param id
* @return
*/
@GetMapping("/init/{id}")

public Result<?> initMySql(@PathVariable("id") Long id){
return dsManageService.initMySql(id)?
Result.ok():
Result.fail();
}

/**
* 测试数据库连接
* @param dsManageDTO
* @return
*/
@PostMapping("testConnection")
@LogPoint(businessName = "测试数据库连接",businessAction = "testConnection")
public Result<?> testConnection(@RequestBody DsManageDTO dsManageDTO){
return InitDBUtil.getConnection(dsManageDTO)?
Result.ok():
Result.fail();
}
}

总结

以上就是今天要讲的内容,本文解决了多数据源前端动态切换问题。
不需要封装在代码中只需要配置在主数据库中的一张表中就可以实现动态切换。