Spring Boot多数据源开发实践:从踩坑到解决

最近开发水文监测系统时遇到了多数据源切换问题:明明存在的表却报 “Table doesn’t exist”。本文记录排查过程和解决方案,希望能帮助遇到类似情况的朋友。

问题背景

大断面是水文监测中的重要概念,用于展示河道/水库的横断面形状。本次任务是为前端图表组件提供适配的数据接口。

系统配置了三个数据源:

数据源 数据库 用途
MASTER hydrology 主业务数据库
SLAVE hydrology 从库(默认关闭)
WATER ysq 水务数据库(独立连接)

问题发现

错误现象

接口测试时直接报错:

1
Table 'hydrology.st_rvsect_b' doesn't exist

这个表明明存在,为什么会报不存在?

问题分析

经过排查发现,ST_RVSECT_B 表实际存储在 WATER 数据源,但代码直接调用了 Mapper 层,绕过了 Service 层的 @DataSource 注解,导致使用了默认的 MASTER 数据源。

错误代码示例

1
2
3
4
5
6
7
8
9
// StationArchiveBiz.java - 错误示例
@Autowired
private StRvsectBMapper stRvsectBMapper;

public CrossSectionResponseDTO getCrossSection(String stcd) {
// 直接调用 Mapper,绕过了数据源切换!
List<StRvsectB> sectionPoints = stRvsectBMapper.selectLatestSectionPointsByStcd(stcd);
// ...
}

这种写法的问题在于:多数据源切换是通过 AOP 实现的,只有在 Service 层的方法上添加 @DataSource 注解才能生效。直接调用 Mapper 会使用默认数据源。

解决方案

修复代码

在 Service 层添加方法并配置数据源注解:

1
2
3
4
5
6
7
8
9
// IStRvsectBService.java
List<StRvsectB> selectLatestSectionPointsByStcd(String stcd);

// StRvsectBServiceImpl.java
@Override
@DataSource(value = DataSourceType.WATER) // 关键!
public List<StRvsectB> selectLatestSectionPointsByStcd(String stcd) {
return stRvsectBMapper.selectLatestSectionPointsByStcd(stcd);
}

然后在 Biz 层调用 Service:

1
2
3
4
5
6
7
8
9
// StationArchiveBiz.java - 修复后
@Autowired
private IStRvsectBService stRvsectBService;

public CrossSectionResponseDTO getCrossSection(String stcd) {
// 通过 Service 调用,正确切换数据源
List<StRvsectB> sectionPoints = stRvsectBService.selectLatestSectionPointsByStcd(stcd);
// ...
}

架构示意

1
2
3
4
5
6
7
8
9
10
Controller/Biz 层


Service 层 ◄── @DataSource 注解在这里生效


Mapper 层


数据库(根据注解切换)

响应格式重构

解决数据源问题后,还需要重构响应格式以适配前端图表组件。

新旧格式对比

旧格式:

1
2
3
4
5
{
"points": [{ "distance": 0, "elevation": 95 }],
"warningWaterLevel": 120,
"waterHistory": [...]
}

新格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"information": {
"normz": 120,
"actz": 69.797,
"dsflz": 125,
"ckflz": 127,
"fsltdz": 119,
"flcnhgwl": 124
},
"riverbedDataSource": [
{ "x": 0, "y": 95 },
{ "x": 10, "y": 92 }
],
"dataSource": [{ "z": 120, "q": 100 }],
"name": "白垢",
"riverbedNames": ["起点距(m)", "高程(m)"]
}

DTO 设计

使用内部类组织嵌套结构,保持代码整洁:

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
@Data
public class CrossSectionResponseDTO {

private Information information;
private List<RiverbedPoint> riverbedDataSource;
private List<FlowPoint> dataSource;
private String name;
private List<String> riverbedNames;

@Data
public static class Information {
private BigDecimal normz; // 正常蓄水位
private BigDecimal actz; // 兴利库容
private BigDecimal dsflz; // 设计洪水位
private BigDecimal ckflz; // 校核洪水位
private BigDecimal fsltdz; // 汛限水位
private BigDecimal flcnhgwl; // 防洪高水位
}

@Data
public static class RiverbedPoint {
private Double x; // 起点距
private Double y; // 高程
}

@Data
public static class FlowPoint {
private BigDecimal z; // 水位
private BigDecimal q; // 流量
}
}

经验总结

多数据源开发的注意事项

  1. 永远通过 Service 层访问数据库,不要在 Biz/Controller 层直接注入 Mapper
  2. 数据源注解放在 Service 实现类的方法上,确保 AOP 拦截生效
  3. 跨数据源查询要考虑事务问题,必要时需要分布式事务方案

业务术语要确认清楚

本次开发中遇到了术语的坑:

术语 含义 单位
兴利水位 水位高度 m
兴利库容 (ACTCP) 水的体积 10^6m³

数据库中没有”兴利水位”字段,只有”兴利库容”,开发前务必与业务方确认字段含义。

空数据处理

不同类型测站的数据完整性不同,接口应正确处理空数据情况:

测站类型 断面数据 特征水位 水位-流量
水库站 (RR) 可能无 通常无
河道站 (ZQ) 可能有

结语

多数据源开发中,数据源切换的时机和位置非常重要。记住一个原则:让数据源切换发生在正确的层次

希望这篇文章对你有帮助!