Appearance
Spring Boot 3.x 多数据源集成实战:MyBatis + Druid 双数据源配置方案深度解析
作者: 必码 | bima.cc
一、多数据源需求场景分析
1.1 为什么需要多数据源
在企业级 Java 应用开发中,单数据源配置能够满足大多数 CRUD 场景。然而,随着业务复杂度的提升和系统架构的演进,多数据源的需求变得日益普遍。理解"为什么需要多数据源"是做好架构设计的第一步。
1.1.1 读写分离
读写分离(Read/Write Splitting)是多数据源最经典的应用场景。在高并发业务系统中,数据库的读操作频率通常远高于写操作。通过将读请求分发到从库(Slave),写请求集中在主库(Master),可以有效降低单库压力,提升系统整体吞吐量。
在实现层面,读写分离可以分为以下几个层次:
- 代码层面: 通过不同的 DataSource 配置,在 Service 层手动选择数据源。这种方式实现简单,但侵入性较强,需要在业务代码中显式区分读写操作。
- 中间件层面: 使用 MyCat、ShardingSphere 等数据库中间件,对业务代码透明。这种方式侵入性低,但引入了额外的中间件运维成本。
- 代理层面: 通过 AOP + 自定义注解实现动态数据源切换,如
@ReadOnly注解标记读操作,自动路由到从库。
本文介绍的方案属于代码层面的手动配置方式,适合数据源数量固定(2-3个)且切换逻辑明确的场景。对于更复杂的动态路由需求,建议结合 AbstractRoutingDataSource 实现动态数据源切换。
1.1.2 多业务库隔离
在企业应用中,不同业务域的数据往往需要物理隔离。例如:
- 核心业务库: 存储用户、订单、支付等核心交易数据,要求高可用、强一致性。
- 配置管理库: 存储 AI 模型配置、系统参数等元数据,读写频率较低但需要灵活管理。
- 日志审计库: 存储操作日志、审计记录等数据,写入量大,查询模式固定。
- 报表分析库: 存储聚合后的统计数据,服务于报表和 BI 分析。
在 smart-scaffold-springboot 项目中,我们采用了双库隔离的设计:smart_scaffold_1 作为核心业务库(存储 AI 模型配置),smart_scaffold_2 作为组织架构库(存储部门信息)。这种设计的好处是:
- 故障隔离: 一个数据库出现问题不会影响另一个业务域。
- 独立扩展: 可以针对不同业务库的访问模式进行独立的性能优化。
- 安全合规: 敏感数据可以存储在独立的数据库中,便于实施更严格的安全策略。
- 迁移灵活: 业务拆分时,可以独立迁移某个业务库到新的服务。
1.1.3 数据迁移
在系统升级或架构重构过程中,经常需要同时访问新旧两个数据库。例如:
- 从单体应用迁移到微服务架构时,需要在新旧系统之间同步数据。
- 数据库分库分表改造时,需要同时读写旧库和新库,确保数据一致性。
- 跨系统数据整合时,需要从多个异构数据源汇聚数据。
多数据源配置为数据迁移提供了基础设施支撑。通过配置多个 DataSource,可以在同一个应用中同时操作新旧数据库,实现数据的渐进式迁移。
1.1.4 分库分表前置方案
在数据量增长到单库瓶颈之前,提前做好多数据源架构规划是明智的选择。分库分表的前置方案通常包括:
- 垂直分库: 按业务域将不同的表拆分到不同的数据库中。这是最简单的分库策略,也是本文方案的核心思路。
- 水平分表: 将同一张表的数据按某种规则(如用户ID哈希)分散到多个表中。
- 混合策略: 先垂直分库,再在热点表上进行水平分表。
本文介绍的双数据源方案本质上就是一种垂直分库的实现。当业务进一步增长时,可以在此基础上引入 ShardingSphere 等分库分表中间件,实现更细粒度的数据分散。
1.2 多数据源方案的选型考量
在选择多数据源实现方案时,需要考虑以下因素:
| 考量维度 | 手动配置方案 | 动态路由方案 | 中间件方案 |
|---|---|---|---|
| 实现复杂度 | 低 | 中 | 高 |
| 代码侵入性 | 中 | 低 | 最低 |
| 数据源数量 | 2-5个 | 动态扩展 | 动态扩展 |
| 运维成本 | 低 | 中 | 高 |
| 事务支持 | 单数据源事务 | 需要额外处理 | 取决于中间件 |
| 适用场景 | 数据源固定 | 需要动态切换 | 大规模分库分表 |
本文选择手动配置方案,原因是项目中的数据源数量固定为两个,切换逻辑清晰明确,手动配置方案在可维护性和性能之间取得了最佳平衡。
二、项目技术栈与整体架构
2.1 技术栈版本清单
在深入配置细节之前,我们先明确项目使用的技术栈版本。版本选择不仅影响功能特性,还直接关系到兼容性和稳定性。
| 技术组件 | 版本号 | 说明 |
|---|---|---|
| Spring Boot | 3.5.12 | 主框架,基于 Spring Framework 6.x |
| Java | 17 | LTS 版本,支持 record、sealed class 等特性 |
| MyBatis Spring Boot Starter | 3.0.5 | MyBatis 与 Spring Boot 的集成 starter |
| MyBatis Core | 3.5.14 | MyBatis 核心引擎 |
| Druid | 1.2.22 | 阿里巴巴数据库连接池 |
| MySQL Connector | 8.0.33 | MySQL JDBC 驱动 |
| MySQL Server | 8.0.33+ | 数据库服务器 |
版本选型的关键考量:
Spring Boot 3.5.12 是目前最新的稳定版本,基于 Spring Framework 6.2.x,全面支持 Jakarta EE 9+ 规范。这意味着
javax.*包名需要替换为jakarta.*,这是一个重要的迁移点。MyBatis Spring Boot Starter 3.0.5 与 MyBatis Core 3.5.14 的组合是目前最稳定的版本搭配。Starter 3.x 系列对 Spring Boot 3.x 提供了原生支持,包括自动配置类的兼容性。
Druid 1.2.22 是阿里巴巴 Druid 连接池的成熟版本,提供了丰富的监控功能和连接池管理能力。需要注意的是,Druid 的 Spring Boot Starter 在多数据源场景下并不推荐使用,因为自动配置会与手动配置产生冲突。
Java 17 是当前的 LTS 版本,Spring Boot 3.x 的最低要求就是 Java 17。Java 17 引入的 record、sealed class、pattern matching 等特性可以在项目中充分利用。
2.2 项目模块结构
smart-scaffold-springboot 采用 Maven 多模块架构,各模块职责清晰分离:
smart-scaffold-springboot/ -- 父工程(POM聚合)
├── smart-scaffold-common/ -- 公共模块(BaseMapper、BaseService、PageDTO等)
├── smart-scaffold-dao/ -- 数据访问层(数据源配置、Mapper接口、XML映射文件)
├── smart-scaffold-service/ -- 业务逻辑层(Service实现)
├── smart-scaffold-web/ -- Web层(Controller、启动类、配置文件)
└── pom.xml -- 父POM(依赖管理、版本统一)模块依赖关系:
smart-scaffold-web
└── smart-scaffold-service
└── smart-scaffold-dao
└── smart-scaffold-common这种分层架构遵循了"依赖倒置"原则,上层模块依赖下层模块提供的抽象接口,而不是具体的实现细节。公共模块(common)位于依赖链的最底层,提供了跨模块共享的基础设施。
2.3 Maven 依赖配置要点
在多数据源项目中,Maven 依赖配置有几个关键点需要注意。
父 POM 中的版本管理:
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.12</version>
</parent>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>使用 spring-boot-starter-parent 作为父 POM,可以自动管理大量依赖的版本号,减少版本冲突的风险。但需要注意的是,对于 MyBatis 和 Druid 等组件,Spring Boot 的 BOM(Bill of Materials)中管理的版本可能不是最新的,建议在 DAO 模块中显式指定版本。
DAO 模块的核心依赖:
xml
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!-- MyBatis Core -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.14</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!-- Druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.22</version>
</dependency>关于 Druid Starter 的说明: 在多数据源场景下,我们使用的是原生的 druid 依赖,而不是 druid-spring-boot-starter。原因是 druid-spring-boot-starter 的自动配置机制(DruidDataSourceAutoConfigure)会尝试创建一个默认的 DataSource,这与我们手动配置多个 DataSource 的方案冲突。使用原生 druid 依赖可以完全掌控 DataSource 的创建过程。
三、双数据源配置方案设计
3.1 整体设计思路
双数据源配置的核心思想是:为每个数据源创建一套独立的基础设施,包括 DataSource、SqlSessionFactory、SqlSessionTemplate,并通过包路径隔离 Mapper 接口。
这个设计可以用一个简单的公式来概括:
一个数据源 = 一个 DataSource + 一个 SqlSessionFactory + 一个 SqlSessionTemplate + 一组 Mapper 接口在 smart-scaffold-springboot 项目中,我们定义了两个配置类:
- SmartScaffold1Config:主库配置,标记为
@Primary,对应smart_scaffold_1数据库 - SmartScaffold2Config:从库配置,对应
smart_scaffold_2数据库
3.2 配置类结构设计
每个配置类需要完成以下三件事:
- 创建 DataSource:通过
@ConfigurationProperties绑定 YAML 配置,使用DataSourceBuilder创建连接池实例。 - 创建 SqlSessionFactory:将 DataSource 注入到
SqlSessionFactoryBean,配置 Mapper XML 文件路径。 - 创建 SqlSessionTemplate:包装 SqlSessionFactory,提供线程安全的 MyBatis 会话管理。
此外,通过 @MapperScan 注解指定每个数据源对应的 Mapper 接口包路径,确保不同的 Mapper 接口被正确的 SqlSessionFactory 处理。
3.3 @Primary 注解的作用
@Primary 是 Spring 框架中的一个重要注解,用于解决多个同类型 Bean 的情况下的注入歧义。在双数据源配置中,主库的 DataSource、SqlSessionFactory 和 SqlSessionTemplate 都需要标记 @Primary。
为什么需要 @Primary?考虑以下场景:
- Spring Boot 自动配置的某些组件(如事务管理器)需要注入一个 DataSource。当存在多个 DataSource Bean 时,Spring 不知道该注入哪一个。
- MyBatis 的某些自动配置也需要一个默认的 SqlSessionFactory。
- 其他不特定于某个数据源的组件可能需要访问数据库。
通过 @Primary,我们告诉 Spring:"当有多个候选 Bean 时,优先使用这个。"这样,不需要显式指定 Bean 名称的地方就能正常工作。
3.4 Mapper 接口包路径隔离
Mapper 接口的包路径隔离是多数据源配置的关键设计点。每个数据源的 @MapperScan 注解指定了不同的包路径:
- 主库 Mapper:
cc.bima.scaffold.dao.mapper.db1及其子包 - 从库 Mapper:
cc.bima.scaffold.dao.mapper.db2及其子包
这种设计的好处是:
- 物理隔离:不同数据源的 Mapper 接口位于不同的 Java 包中,代码组织清晰。
- 编译时检查:如果 Mapper 接口放错了包,在编译时就能发现问题。
- IDE 支持:IDE 可以根据包路径提供更好的代码提示和导航。
包路径命名规范建议:
cc.bima.scaffold.dao.mapper.{数据源标识}/
├── UserModelMapper.java -- 主库的 Mapper 接口
└── DepartmentInfoMapper.java -- 从库的 Mapper 接口使用 db1、db2 这样的数据源标识作为包名的一部分,既简洁又明确。如果项目中有更多数据源,可以继续使用 db3、db4 等命名,或者使用更具语义的名称如 core、config、log 等。
3.5 配置类代码结构(教学简化版)
以下是主库配置类的简化结构,展示了核心的配置逻辑:
java
@Configuration
@MapperScan(
basePackages = {"com.example.dao.mapper.db1"},
sqlSessionFactoryRef = "db1SqlSessionFactory"
)
public class PrimaryDataSourceConfig {
// 1. 创建主库数据源(标记 @Primary)
@Primary
@Bean("db1DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db1")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
// 2. 创建主库 SqlSessionFactory(标记 @Primary)
@Primary
@Bean("db1SqlSessionFactory")
public SqlSessionFactory primarySqlSessionFactory(
@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 指定 Mapper XML 文件路径
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/db1/*.xml"));
return bean.getObject();
}
// 3. 创建主库 SqlSessionTemplate(标记 @Primary)
@Primary
@Bean("db1SqlSessionTemplate")
public SqlSessionTemplate primarySqlSessionTemplate(
@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}从库配置类的结构类似,但不需要 @Primary 注解,且包路径和配置前缀不同:
java
@Configuration
@MapperScan(
basePackages = {"com.example.dao.mapper.db2"},
sqlSessionFactoryRef = "db2SqlSessionFactory"
)
public class SecondaryDataSourceConfig {
@Bean("db2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db2")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean("db2SqlSessionFactory")
public SqlSessionFactory secondarySqlSessionFactory(
@Qualifier("db2DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/db2/*.xml"));
return bean.getObject();
}
@Bean("db2SqlSessionTemplate")
public SqlSessionTemplate secondarySqlSessionTemplate(
@Qualifier("db2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}关键设计要点总结:
- 每个配置类的
@MapperScan中的sqlSessionFactoryRef必须指向该配置类创建的 SqlSessionFactory Bean 名称。 @Qualifier注解用于在参数注入时指定具体的 Bean 名称,避免歧义。PathMatchingResourcePatternResolver支持通配符路径匹配,classpath*:前缀表示搜索所有 classpath 路径(包括 jar 包内部)。- 主库的所有 Bean 都标记
@Primary,从库的不标记。
四、数据源配置详解
4.1 YAML 配置文件结构
在 Spring Boot 多数据源配置中,YAML 配置文件的组织方式至关重要。推荐采用"共享配置 + 环境配置"的分层结构。
主配置文件(application.yml)- 共享配置:
yaml
spring:
profiles:
active: dev
datasource:
db1:
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${DB1_USERNAME:root}
password: ${DB1_PASSWORD:}
db2:
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${DB2_USERNAME:root}
password: ${DB2_PASSWORD:}环境配置文件(application-dev.yml)- 环境特定配置:
yaml
spring:
datasource:
db1:
jdbc-url: jdbc:mysql://localhost:3306/smart_scaffold_1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
db2:
jdbc-url: jdbc:mysql://localhost:3306/smart_scaffold_2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8配置设计的几个关键点:
属性名使用
jdbc-url而非url:在多数据源场景下,Spring Boot 的DataSourceProperties默认绑定的是spring.datasource.url属性。但当我们使用@ConfigurationProperties(prefix = "spring.datasource.db1")时,需要使用jdbc-url属性名。这是因为spring.datasource.db1.jdbc-url会被映射到DataSourceProperties.jdbcUrl属性,而DataSourceBuilder.create().build()会读取这个属性来创建连接。环境变量支持:使用
${DB1_USERNAME:root}语法支持从环境变量读取配置,:root是默认值。这在容器化部署(Docker/Kubernetes)中尤为重要,敏感信息不应硬编码在配置文件中。JDBC URL 参数说明:
characterEncoding=utf8:指定字符编码useUnicode=true:启用 Unicode 支持useSSL=false:开发环境关闭 SSL(生产环境应开启)serverTimezone=GMT%2B8:设置时区为东八区(%2B是+的 URL 编码)
4.2 @ConfigurationProperties 绑定机制
@ConfigurationProperties 是 Spring Boot 提供的类型安全配置绑定机制。它的核心工作原理是:
- Spring 读取 YAML/Properties 文件中以指定前缀开头的所有属性。
- 通过 Java Bean 的 setter 方法,将属性值绑定到目标对象。
- 支持松散绑定(Relaxed Binding),例如
jdbc-url、jdbcUrl、JDBC_URL都可以映射到jdbcUrl属性。
在多数据源配置中,@ConfigurationProperties(prefix = "spring.datasource.db1") 会将 spring.datasource.db1.* 下的所有属性绑定到 DataSourceBuilder 创建的 DataSource 实例。Druid 的 DruidDataSource 类提供了以下关键属性的 setter 方法:
url/jdbcUrl:数据库连接 URLusername:数据库用户名password:数据库密码driverClassName:JDBC 驱动类名initialSize:初始连接数minIdle:最小空闲连接数maxActive:最大活跃连接数maxWait:获取连接的最大等待时间(毫秒)
4.3 Druid 连接池参数调优
Druid 连接池提供了丰富的参数配置选项,合理的参数调优对系统性能至关重要。以下是各参数的详细说明和调优建议。
4.3.1 基础连接参数
yaml
spring:
datasource:
db1:
initialSize: 5 # 初始化时建立的物理连接数
minIdle: 5 # 最小空闲连接数
maxActive: 20 # 最大活跃连接数
maxWait: 60000 # 获取连接最大等待时间(毫秒)参数详解:
initialSize(初始连接数):应用启动时,连接池会预先创建指定数量的连接。设置合理的初始值可以避免应用启动后的第一次请求因创建连接而产生延迟。建议设置为与
minIdle相同的值,通常为 5-10。minIdle(最小空闲连接数):连接池中保持的最小空闲连接数量。当空闲连接数低于此值时,连接池会创建新的连接。这个参数的设置需要考虑以下因素:
- 系统的最低并发量:确保在低峰期也有足够的空闲连接
- 连接创建成本:MySQL 连接的创建成本相对较低,但建立 SSL 连接的成本较高
- 内存占用:每个空闲连接占用一定的内存资源
maxActive(最大活跃连接数):连接池在同一时刻能够分配的最大活跃连接数。这是连接池最重要的参数之一,设置过高会导致数据库压力过大,设置过低会导致请求排队等待。计算公式参考:
maxActive = 预期最大并发数 × 平均每个请求占用连接的时间 / 请求处理时间在实际项目中,通常从 20 开始,根据监控数据进行调整。对于 MySQL 8.0,默认的
max_connections是 151,连接池的maxActive不应超过数据库的最大连接数。maxWait(最大等待时间):当连接池中没有可用连接时,调用者等待获取连接的最大时间(毫秒)。超时后会抛出异常。设置为 60000(60秒)是一个合理的默认值。如果频繁出现获取连接超时,说明
maxActive设置过小,需要调大。
4.3.2 连接回收参数
yaml
spring:
datasource:
db1:
timeBetweenEvictionRunsMillis: 60000 # 空闲连接回收器运行间隔(毫秒)
minEvictableIdleTimeMillis: 300000 # 连接最小空闲时间(毫秒)
maxEvictableIdleTimeMillis: 900000 # 连接最大空闲时间(毫秒)参数详解:
timeBetweenEvictionRunsMillis:Druid 的空闲连接回收器(Eviction Thread)的运行间隔。默认值为 60000ms(60秒)。回收器会定期检查连接池中的空闲连接,回收超过
minEvictableIdleTimeMillis的连接。这个参数不需要频繁调整。minEvictableIdleTimeMillis:连接在池中最小空闲时间,超过此时间的空闲连接可能被回收。默认值为 300000ms(5分钟)。设置过短会导致频繁创建和销毁连接,设置过长会占用过多数据库连接资源。
maxEvictableIdleTimeMillis:连接在池中最大空闲时间,超过此时间的空闲连接一定会被回收。默认值为 420000ms(7分钟)。这个参数提供了一个硬性的上限,确保长时间不使用的连接被释放。
4.3.3 连接验证参数
yaml
spring:
datasource:
db1:
validationQuery: SELECT 1 # 连接有效性检测SQL
testWhileIdle: true # 空闲时是否检测连接有效性
testOnBorrow: false // 借出时是否检测连接有效性
testOnReturn: false // 归还时是否检测连接有效性
keepAlive: true // 开启连接保活参数详解:
validationQuery:用于检测连接是否有效的 SQL 语句。
SELECT 1是最常用的验证查询,执行开销极小。对于 Oracle 数据库,通常使用SELECT 1 FROM DUAL。MySQL 8.0 推荐使用SELECT 1。testWhileIdle:在空闲连接回收器运行时,是否对空闲连接进行有效性检测。建议设置为
true,这是最推荐的连接验证策略,因为:- 只在回收器运行时检测,不影响正常请求的性能
- 能够及时发现并清理已被数据库端关闭的连接
- 检测频率由
timeBetweenEvictionRunsMillis控制
testOnBorrow:在从连接池借出连接时是否检测连接有效性。建议设置为
false,因为:- 每次借出都检测会增加一次数据库查询的开销
- 如果配合
testWhileIdle=true,空闲连接已经被定期验证,借出时的额外验证是冗余的 - 在高并发场景下,这个开销会被放大
testOnReturn:在归还连接到连接池时是否检测连接有效性。建议设置为
false,原因与testOnBorrow类似。keepAlive:开启连接保活功能。当连接池中的空闲连接长时间不活动时,Druid 会定期发送心跳查询(使用
validationQuery)防止连接被数据库端或中间网络设备关闭。建议在以下场景中开启:- 使用了数据库中间件(如连接代理、防火墙)会主动关闭空闲连接
- 数据库服务器配置了
wait_timeout较短的超时时间 - 网络环境中存在 NAT 网关会清理长时间不活动的 TCP 连接
4.3.4 监控与统计参数
yaml
spring:
datasource:
db1:
filters: stat,wall,slf4j # 启用的过滤器
connectionProperties: druid.stat.slowSqlMillis=3000 # 慢SQL阈值参数详解:
filters:Druid 的扩展过滤器链,常用的过滤器包括:
stat:SQL 执行统计过滤器,记录 SQL 执行时间、执行次数等统计信息wall:SQL 防火墙过滤器,防止 SQL 注入攻击slf4j:日志过滤器,通过 SLF4J 输出 SQL 执行日志log4j/log4j2:日志过滤器(根据项目使用的日志框架选择)
connectionProperties:Druid 的扩展属性。
druid.stat.slowSqlMillis=3000设置了慢 SQL 的阈值为 3 秒,执行时间超过 3 秒的 SQL 会被记录到慢 SQL 日志中。
4.4 Druid 监控页面配置
Druid 内置了一个功能强大的监控页面(Druid Monitor),可以实时查看连接池状态、SQL 执行统计、慢 SQL 记录等信息。在生产环境中,Druid 监控页面是排查数据库性能问题的重要工具。
要启用 Druid 监控页面,需要配置一个 Servlet 和一个 Filter:
java
@Configuration
public class DruidMonitorConfig {
@Bean
@ServletComponentScan
public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() {
ServletRegistrationBean<StatViewServlet> registrationBean =
new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
// 配置监控页面访问权限
registrationBean.addInitParameter("loginUsername", "admin");
registrationBean.addInitParameter("loginPassword", "your_password");
registrationBean.addInitParameter("resetEnable", "false");
return registrationBean;
}
@Bean
public FilterRegistrationBean<WebStatFilter> druidWebStatFilter() {
FilterRegistrationBean<WebStatFilter> registrationBean =
new FilterRegistrationBean<>(new WebStatFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.addInitParameter("exclusions",
"*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return registrationBean;
}
}Druid 监控页面提供的主要功能:
- DataSource 页面:展示连接池的核心指标,包括活跃连接数、空闲连接数、等待线程数、连接创建/销毁统计等。
- SQL 监控页面:列出所有执行过的 SQL 语句,包括执行时间、执行次数、平均时间、最慢时间等。
- SQL 防火墙页面:展示被 SQL 防火墙拦截的 SQL 语句,帮助发现潜在的 SQL 注入风险。
- Session 监控页面:展示当前活跃的 HTTP Session 信息。
- Spring 监控页面:展示 Spring Bean 的方法调用统计。
生产环境安全建议:
- 修改默认的用户名和密码,使用强密码
- 通过 Spring Security 或 Nginx 限制监控页面的访问 IP
- 在外部不可直接访问的内网环境中部署监控页面
- 考虑定期清理监控数据,避免内存占用过大
五、MyBatis 集成配置
5.1 SqlSessionFactory 配置详解
SqlSessionFactory 是 MyBatis 的核心对象,它是创建 SqlSession 的工厂。在 Spring Boot 多数据源配置中,每个数据源都需要一个独立的 SqlSessionFactory。
SqlSessionFactoryBean 的核心配置项:
java
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource); // 设置数据源
bean.setMapperLocations(resources); // 设置 Mapper XML 路径
bean.setTypeAliasesPackage("com.example.entity"); // 设置实体类别名包
bean.setConfiguration(mybatisConfiguration); // 设置 MyBatis 全局配置5.1.1 mapperLocations 配置
mapperLocations 指定了 MyBatis XML 映射文件的路径。在多数据源配置中,每个 SqlSessionFactory 应该只加载对应数据源的 Mapper XML 文件:
java
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/db1/*.xml"));路径语法说明:
classpath*::搜索所有 classpath 路径(包括 jar 包内部)。如果只使用classpath:,则不会搜索 jar 包内的资源。mapper/db1/:Mapper XML 文件的目录路径,与数据源标识对应。*.xml:匹配所有 XML 文件。
为什么使用 classpath*: 而不是 classpath:?
在多模块 Maven 项目中,DAO 模块编译后的 class 文件和资源文件会被打包成 jar 文件。Web 模块通过依赖引用 DAO 模块的 jar 包。如果使用 classpath:,Spring 只会在文件系统的 classpath 目录中搜索,而不会搜索 jar 包内的资源。使用 classpath*: 可以确保搜索所有 classpath 位置,包括依赖 jar 包内的资源。
5.1.2 typeAliasesPackage 配置
typeAliasesPackage 指定了 MyBatis 实体类的包路径。配置后,在 Mapper XML 中可以直接使用类名(首字母小写或首字母大小写均可)作为类型别名,而不需要写完整的类路径:
xml
<!-- 配置 typeAliasesPackage 前 -->
<resultMap type="com.example.dao.entity.db1.UserModel" id="BaseResultMap">
<!-- 配置 typeAliasesPackage 后 -->
<resultMap type="userModel" id="BaseResultMap">在多数据源场景中,可以为每个 SqlSessionFactory 配置不同的 typeAliasesPackage:
java
bean.setTypeAliasesPackage("com.example.dao.entity.db1");注意事项:
- 如果不同数据源的实体类包路径有重叠,可能会导致别名冲突。建议使用数据源标识作为包名的一部分来避免冲突。
typeAliasesPackage支持通配符,如com.example.dao.entity.*可以扫描所有子包。
5.1.3 MyBatis 全局配置
通过 org.apache.ibatis.session.Configuration 对象可以设置 MyBatis 的全局行为:
java
org.apache.ibatis.session.Configuration configuration =
new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true); // 驼峰命名映射
configuration.setCallSettersOnNulls(true); // null值调用setter
configuration.setJdbcTypeForNull(JdbcType.NULL); // null值的JDBC类型
configuration.setLazyLoadingEnabled(false); // 延迟加载
configuration.setAggressiveLazyLoading(false); // 按需加载
bean.setConfiguration(configuration);核心配置项详解:
mapUnderscoreToCamelCase(驼峰命名映射):开启后,MyBatis 会自动将数据库字段的下划线命名(如
user_name)映射到 Java 属性的驼峰命名(如userName)。这是最常用的配置之一,可以大幅减少<resultMap>中的手动映射工作。callSettersOnNulls(null值调用setter):默认情况下,当查询结果中某列的值为 null 时,MyBatis 不会调用对应的 setter 方法。如果实体类中有默认值,开启此选项可以确保 null 值覆盖默认值。在 DTO 对象中,如果某些字段需要区分"未设置"和"设置为null",这个选项尤为重要。
jdbcTypeForNull(null值的JDBC类型):当参数为 null 时,MyBatis 需要知道对应的 JDBC 类型才能正确处理。设置为
JdbcType.NULL可以避免 Oracle 等数据库在处理 null 参数时报错。lazyLoadingEnabled(延迟加载):开启后,MyBatis 的关联查询(
<association>、<collection>)会延迟加载,直到实际访问关联属性时才执行 SQL。在多数据源场景中,延迟加载需要特别注意:如果关联查询涉及不同的数据源,延迟加载可能会导致跨数据源查询的问题。
5.2 MapperScannerConfigurer vs @MapperScan
MyBatis 提供了两种方式来注册 Mapper 接口:MapperScannerConfigurer 和 @MapperScan 注解。
5.2.1 @MapperScan 注解方式
@MapperScan 是 MyBatis-Spring 提供的注解,可以直接标注在配置类上:
java
@Configuration
@MapperScan(
basePackages = {"com.example.dao.mapper.db1"},
sqlSessionFactoryRef = "db1SqlSessionFactory"
)
public class PrimaryDataSourceConfig {
// ...
}优点:
- 配置简洁,与
@Configuration类紧密结合 sqlSessionFactoryRef属性可以明确指定使用哪个 SqlSessionFactory- 在多数据源场景中,每个配置类上的
@MapperScan自然地隔离了不同数据源的 Mapper 接口
注意事项:
basePackages支持通配符,但建议使用精确的包路径以避免意外扫描- 如果同一个包下的 Mapper 接口需要被不同的 SqlSessionFactory 处理,需要将它们拆分到不同的包中
5.2.2 MapperScannerConfigurer 方式
MapperScannerConfigurer 是一个 BeanDefinitionRegistryPostProcessor,通过编程方式注册 Mapper 接口:
java
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.example.dao.mapper.db1");
configurer.setSqlSessionFactoryBeanName("db1SqlSessionFactory");
return configurer;
}优点:
- 更加灵活,可以通过编程方式动态配置
- 可以在运行时根据条件决定扫描哪些包
缺点:
- 配置相对冗长
- 在某些 Spring Boot 版本中,
MapperScannerConfigurer的初始化时机可能导致循环依赖问题
5.2.3 推荐方案
在 Spring Boot 3.x 多数据源配置中,推荐使用 @MapperScan 注解方式。原因是:
- 配置更简洁,与
@Configuration类的组织方式一致 sqlSessionFactoryRef属性可以明确绑定 SqlSessionFactory,避免歧义- Spring Boot 3.x 对注解方式的支持更加完善
- 在实际项目中,
@MapperScan的初始化时机更加可控,不容易出现循环依赖问题
5.3 Mapper XML 中的 ResultMap 设计
ResultMap 是 MyBatis 中将数据库查询结果映射到 Java 对象的核心机制。在多数据源项目中,ResultMap 的设计需要考虑以下几点:
基础 ResultMap 与 DTO ResultMap 的继承关系:
xml
<!-- 基础 ResultMap,映射到实体类 -->
<resultMap id="BaseResultMap" type="com.example.dao.entity.db1.UserModel">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="time_create" jdbcType="TIMESTAMP" property="timeCreate" />
<!-- ... 其他字段映射 ... -->
</resultMap>
<!-- DTO ResultMap,继承基础 ResultMap -->
<resultMap id="ResultMapWithDTO" extends="BaseResultMap"
type="com.example.dao.dto.db1.UserModelDTO" />这种设计模式的优势在于:
- 复用性:DTO ResultMap 继承基础 ResultMap,避免重复定义字段映射。
- 灵活性:基础 ResultMap 映射到实体类(用于插入/更新操作),DTO ResultMap 映射到 DTO 类(用于查询操作),两者可以有不同的字段。
- 可维护性:当数据库表结构变更时,只需要修改基础 ResultMap,DTO ResultMap 自动继承变更。
5.4 动态 SQL 与通用查询模式
在 Mapper XML 中,通过 MyBatis 的动态 SQL 标签可以实现灵活的查询逻辑。以下是项目中使用的通用查询模式:
通用 WHERE 条件片段:
xml
<sql id="Base_Where">
<if test="id != null">
and id = #{id, jdbcType=BIGINT}
</if>
<if test="name != null and name != ''">
<bind name="likeName" value="'%' + name + '%'" />
and name like #{likeName, jdbcType=VARCHAR}
</if>
<if test="status != null">
and status = #{status, jdbcType=TINYINT}
</if>
</sql>通用排序与分页:
xml
<select id="selectBy" parameterType="com.example.dto.UserModelQueryDTO"
resultMap="ResultMapWithDTO">
select
<include refid="Base_Column_List" />
from user_model
where 1=1
<include refid="Base_Where" />
order by
<choose>
<when test="fields == 'id'">id</when>
<otherwise>id</otherwise>
</choose>
<choose>
<when test="order == 'asc'">asc</when>
<when test="order == 'desc'">desc</when>
<otherwise>desc</otherwise>
</choose>
<if test="isPage">limit #{start}, #{end}</if>
</select>设计要点:
<sql>片段复用:将通用的 WHERE 条件抽取为<sql>片段,在多个查询中复用。- 模糊查询使用
<bind>:使用<bind>标签预先拼接模糊查询的通配符,避免 SQL 注入风险。不要在 Java 代码中拼接%,因为这样无法利用预编译的优势。 - 通用分页:通过
isPage标志位控制是否添加LIMIT子句,实现同一查询方法支持分页和非分页两种模式。 - 排序字段白名单:使用
<choose>标签限制可排序的字段,防止用户传入任意字段名导致 SQL 注入。
六、MyBatis XML Mapper 与 Java 接口同目录存放
6.1 传统方式的问题
在传统的 MyBatis 项目中,Mapper XML 文件通常存放在 src/main/resources/mapper/ 目录下,而 Mapper Java 接口存放在 src/main/java/ 目录下。这种分离存放的方式有以下问题:
- 维护不便:修改一个 Mapper 的逻辑时,需要在两个不同的目录之间切换,降低开发效率。
- 包路径不一致:XML 文件的目录结构与 Java 包路径不一致,容易产生混淆。
- 重构困难:当 Mapper 接口的包路径发生变化时,需要同步修改 XML 文件中的
namespace属性,但 IDE 的重构工具通常不会自动处理 XML 文件。
6.2 同目录存放方案
将 Mapper XML 文件与对应的 Java 接口放在同一个包目录下,是 MyBatis 官方推荐的做法:
src/main/java/
└── com/example/dao/mapper/db1/
├── UserModelMapper.java -- Mapper 接口
└── UserModelMapper.xml -- Mapper XML(与接口同目录)这种方式的好处是显而易见的:
- 就近原则:接口和 XML 在同一个目录下,修改时不需要切换目录。
- 包路径一致:XML 的
namespace与接口的全限定类名天然一致。 - IDE 友好:IDE 可以在接口和 XML 之间提供快速导航。
- 重构安全:移动 Mapper 接口时,IDE 可以同时移动对应的 XML 文件。
6.3 Maven 资源过滤配置
默认情况下,Maven 在编译时只会处理 src/main/resources/ 目录下的资源文件,而 src/main/java/ 目录下的非 Java 文件会被忽略。因此,需要额外配置 Maven 的 <resources> 元素来包含 Java 目录下的 XML 文件。
DAO 模块的 pom.xml 配置:
xml
<build>
<resources>
<!-- 包含 src/main/java 目录下的 XML 文件 -->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<!-- 包含 src/main/resources 目录下的所有文件 -->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>配置详解:
第一个
<resource>:将src/main/java目录下的所有 XML 文件包含到编译输出中。filtering=false表示不对这些文件进行变量替换(如${project.version}),因为 XML 文件中可能包含 MyBatis 的${}语法,如果开启过滤会导致冲突。第二个
<resource>:将src/main/resources目录下的所有文件包含到编译输出中。filtering=true表示对这些文件进行变量替换,适用于application.yml等配置文件。
filtering 参数的重要性:
这是一个容易踩坑的地方。如果将 src/main/java 下的 XML 文件的 filtering 设置为 true,Maven 会尝试替换 XML 文件中的 ${...} 表达式。而 MyBatis 的动态 SQL 中大量使用了 ${} 语法(如 ${tableName}、${order}),这会导致 Maven 在编译时报错或替换出错误的内容。
6.4 两种存放方式的对比
在 smart-scaffold-springboot 项目中,我们采用了混合方式:Mapper XML 文件存放在 src/main/resources/mapper/ 目录下,按数据源分子目录组织。这种方式在多数据源场景下也有其优势:
src/main/resources/
└── mapper/
├── db1/
│ └── UserModelMapper.xml
└── db2/
└── DepartmentInfoMapper.xml| 对比维度 | 同目录存放 | resources 目录存放 |
|---|---|---|
| 维护便利性 | 高(接口和 XML 在一起) | 中(需要切换目录) |
| 多数据源组织 | 需要按包路径区分 | 按子目录区分,更直观 |
| Maven 配置 | 需要额外配置 resource | 默认支持,无需额外配置 |
| IDE 支持 | 好(导航方便) | 一般 |
| 打包后位置 | 与 class 文件在一起 | 在 classpath 根目录下 |
两种方式各有优劣,选择哪种取决于团队偏好和项目规模。对于数据源较少(2-3个)的项目,同目录存放更方便;对于数据源较多的项目,resources 目录下按子目录组织更清晰。
七、事务管理
7.1 @Transactional 事务注解基础
Spring 的 @Transactional 注解是基于 AOP 实现的声明式事务管理。在多数据源场景中,事务管理变得更加复杂,因为需要明确指定事务使用哪个数据源。
@Transactional 的核心属性:
java
@Transactional(
rollbackFor = Exception.class, // 遇到哪些异常回滚
isolation = Isolation.DEFAULT, // 事务隔离级别
propagation = Propagation.REQUIRED, // 事务传播行为
timeout = 30, // 事务超时时间(秒)
readOnly = false // 是否只读事务
)
public void saveData(DataDTO dto) {
// 业务逻辑
}关键属性说明:
rollbackFor:默认情况下,Spring 只对
RuntimeException和Error进行回滚。对于受检异常(checked exception),需要显式指定rollbackFor = Exception.class来确保所有异常都触发回滚。这是实际开发中最容易遗漏的配置。isolation:事务隔离级别,从低到高依次为:
READ_UNCOMMITTED:读未提交,存在脏读问题READ_COMMITTED:读已提交,解决脏读,存在不可重复读REPEATABLE_READ:可重复读(MySQL InnoDB 默认),解决不可重复读SERIALIZABLE:串行化,解决幻读,但性能最差
propagation:事务传播行为,定义了方法被调用时如何参与事务:
REQUIRED(默认):如果当前有事务,加入该事务;如果没有,新建一个事务REQUIRES_NEW:总是新建事务,如果当前有事务,挂起当前事务NESTED:如果当前有事务,嵌套一个事务;如果没有,新建一个事务SUPPORTS:如果当前有事务,加入该事务;如果没有,以非事务方式执行NOT_SUPPORTED:以非事务方式执行,如果当前有事务,挂起当前事务MANDATORY:如果当前有事务,加入该事务;如果没有,抛出异常NEVER:以非事务方式执行,如果当前有事务,抛出异常
7.2 多数据源下的事务管理策略
在双数据源配置中,@Transactional 注解默认使用 @Primary 标记的数据源的事务管理器。这意味着:
- 标记了
@Transactional的方法,默认使用主库(db1)的事务管理器 - 从库(db2)的操作默认不在事务保护范围内
如何为从库指定事务管理器?
首先,需要为每个数据源创建独立的事务管理器:
java
// 主库事务管理器
@Primary
@Bean("db1TransactionManager")
public PlatformTransactionManager db1TransactionManager(
@Qualifier("db1DataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
// 从库事务管理器
@Bean("db2TransactionManager")
public PlatformTransactionManager db2TransactionManager(
@Qualifier("db2DataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}然后,在需要使用从库事务的方法上,通过 transactionManager 属性指定:
java
@Transactional(transactionManager = "db2TransactionManager",
rollbackFor = Exception.class)
public void saveDepartment(DepartmentDTO dto) {
// 操作从库(db2)的方法
departmentMapper.insertSelective(dto);
}7.3 跨数据源事务问题
当业务操作涉及多个数据源时,就面临跨数据源事务的问题。考虑以下场景:
java
public void transferData() {
// 操作主库
userModelMapper.insertSelective(userModel);
// 操作从库
departmentMapper.insertSelective(department);
}如果 departmentMapper.insertSelective 抛出异常,userModelMapper.insertSelective 的操作不会回滚,因为它们使用的是不同的事务管理器,属于不同的事务。这就是跨数据源事务的挑战。
解决方案分析:
7.3.1 最佳努力交付(Best Effort)
最简单的策略是接受跨数据源操作无法保证原子性的事实,通过业务层面的补偿机制来处理:
- 操作顺序设计:先执行可能失败的操作,后执行不容易失败的操作。
- 幂等设计:确保每个操作都是幂等的,失败后可以安全重试。
- 补偿操作:当后续操作失败时,通过补偿操作回滚之前的操作。
java
public void transferData() {
try {
// 先执行可能失败的操作
departmentMapper.insertSelective(department);
// 再执行不容易失败的操作
userModelMapper.insertSelective(userModel);
} catch (Exception e) {
// 补偿:删除已插入的 department 记录
departmentMapper.deleteByPrimaryKey(department.getId());
throw e;
}
}7.3.2 ChainedTransactionManager
Spring Data 提供了 ChainedTransactionManager,可以将多个事务管理器串联起来,实现"伪分布式事务":
java
@Bean("chainedTransactionManager")
public ChainedTransactionManager chainedTransactionManager(
@Qualifier("db1TransactionManager") PlatformTransactionManager tm1,
@Qualifier("db2TransactionManager") PlatformTransactionManager tm2) {
return new ChainedTransactionManager(tm1, tm2);
}工作原理:
- 提交时:按顺序提交每个事务管理器的事务。如果中间某个事务提交失败,已提交的事务不会回滚。
- 回滚时:按逆序回滚每个事务管理器的事务。
局限性:
- 不是真正的原子性事务,提交阶段可能部分成功部分失败
- 性能开销较大,因为需要依次操作多个数据库的事务
- Spring Data 2.x 之后,
ChainedTransactionManager已被标记为不推荐使用
7.3.3 分布式事务方案
对于要求强一致性的跨数据源事务,需要引入分布式事务框架:
Seata(推荐):
Seata 是阿里巴巴开源的分布式事务框架,支持 AT、TCC、Saga、XA 四种事务模式。其中 AT 模式是最易用的:
- AT 模式:自动补偿模式,对业务代码无侵入。Seata 会自动拦截 SQL 语句,生成回滚日志(undo_log),在需要回滚时自动执行补偿操作。
- TCC 模式:手动编写 Try、Confirm、Cancel 三个方法,适用于对性能要求较高的场景。
- Saga 模式:基于事件驱动的长事务解决方案,适用于业务流程较长的场景。
- XA 模式:基于数据库 XA 协议的两阶段提交,一致性最强但性能最差。
JTA(Java Transaction API):
JTA 是 Java 标准的分布式事务 API,通过 Atomikos 或 Bitronix 等 JTA 实现库可以在 Spring Boot 中使用。但 JTA 方案的性能开销较大,且配置复杂,在新项目中已不作为首选方案。
7.4 事务管理的实践建议
基于实际项目经验,以下是多数据源事务管理的建议:
- 优先避免跨数据源事务:通过合理的业务设计和数据分布,尽量让一个业务操作只涉及一个数据源。
- 使用"事件驱动"解耦:将跨数据源操作拆分为独立的异步事件,通过消息队列(如 Kafka、RabbitMQ)传递。
- 最终一致性优于强一致性:在大多数业务场景中,最终一致性已经足够。通过补偿机制和重试策略来保证数据最终一致。
- 只在必要时引入分布式事务:分布式事务引入了额外的复杂度和性能开销,应该在充分评估后谨慎引入。
八、数据库设计
8.1 数据库整体规划
在 smart-scaffold-springboot 项目中,我们设计了两个独立的 MySQL 数据库,分别服务于不同的业务域:
| 数据库 | 用途 | 字符集 | 核心表 |
|---|---|---|---|
| smart_scaffold_1 | AI 模型配置管理 | utf8mb4 | user_model |
| smart_scaffold_2 | 组织架构管理 | utf8mb4 | department_info |
8.2 smart_scaffold_1 数据库:user_model 表
user_model 表是 AI 模型配置表,存储用户配置的各种大语言模型参数。这是主库的核心业务表。
表结构设计:
sql
CREATE TABLE `user_model` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '模型ID,自增主键',
`time_create` datetime NOT NULL COMMENT '创建时间',
`time_update` datetime NOT NULL COMMENT '更新时间',
`admin_id` varchar(20) DEFAULT NULL COMMENT '操作人ID',
`admin_name` varchar(20) DEFAULT NULL COMMENT '操作人',
`user_id` bigint NOT NULL COMMENT '用户ID (0表示系统配置)',
`api_type` varchar(50) NOT NULL COMMENT 'AI提供商类型: OPENAI, OLLAMA等',
`config_name` varchar(100) NOT NULL COMMENT '配置名称',
`api_key` varchar(500) DEFAULT NULL COMMENT 'API密钥 (AES-256加密存储)',
`base_url` varchar(500) DEFAULT NULL COMMENT 'API基础URL',
`model_name` varchar(100) DEFAULT NULL COMMENT '模型名称',
`temperature` decimal(3,2) DEFAULT '0.70' COMMENT '温度参数 (0.0-2.0)',
`max_tokens` int DEFAULT '4096' COMMENT '最大Token数',
`embedding_model` varchar(100) DEFAULT NULL COMMENT '嵌入模型名称',
`is_default` tinyint(1) DEFAULT '0' COMMENT '是否为默认配置',
`status` tinyint(1) DEFAULT '1' COMMENT '状态: 1-禁用, 0-启用',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='用户大模型配置表';设计要点解析:
主键设计:使用
bigint unsigned自增主键。unsigned限定为非负整数,可以将正整数范围扩大一倍。自增主键在 B+ 树索引中有良好的插入性能,因为新记录总是追加到索引的末尾。时间字段:
time_create和time_update使用datetime类型(而非timestamp),避免 2038 年问题。datetime类型的存储范围是 '1000-01-01 00:00:00' 到 '9999-12-31 23:59:59',而timestamp的范围是 '1970-01-01 00:00:01' 到 '2038-01-19 03:14:07'(UTC)。审计字段:
admin_id和admin_name记录操作人信息,便于审计追踪。这两个字段使用varchar(20)而不是外键关联用户表,是为了避免跨库外键依赖。API 密钥安全:
api_key字段的注释中标注了"AES-256加密存储",这是一个重要的安全实践。API 密钥应该在应用层加密后存储,而不是明文存储。温度参数:
temperature使用decimal(3,2)类型,精确到小数点后两位,取值范围 0.00-9.99。这满足了大语言模型温度参数的精度要求。字符集选择:使用
utf8mb4字符集和utf8mb4_unicode_ci排序规则。utf8mb4是 MySQL 中真正的 UTF-8 编码,支持 4 字节的 Unicode 字符(包括 emoji 表情符号)。而 MySQL 中的utf8编码实际上只支持 3 字节,不是完整的 UTF-8。
8.3 smart_scaffold_2 数据库:department_info 表
department_info 表是部门信息表,采用树形结构设计,通过 up_id 字段实现自引用关联。
表结构设计:
sql
CREATE TABLE `department_info` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '部门ID,自增主键',
`time_create` datetime NOT NULL COMMENT '创建时间',
`time_update` datetime NOT NULL COMMENT '更新时间',
`admin_id` varchar(20) DEFAULT NULL COMMENT '操作人ID',
`admin_name` varchar(20) DEFAULT NULL COMMENT '操作人',
`up_id` bigint unsigned DEFAULT NULL COMMENT '上级部门ID',
`name` varchar(20) DEFAULT NULL COMMENT '部门名称',
`remark` longtext COMMENT '部门备注',
`freeze` tinyint NOT NULL COMMENT '是否冻结,0正常,1冻结',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;8.4 树形结构设计深度解析
树形结构是组织架构、分类目录、评论系统等场景中常见的数据模型。department_info 表通过 up_id 字段实现自引用,构建部门层级关系。
8.4.1 邻接表模式(Adjacency List)
本项目使用的就是邻接表模式,每个节点只记录其父节点的 ID:
根部门(up_id = NULL)
├── 技术部(up_id = 1)
│ ├── 后端组(up_id = 2)
│ └── 前端组(up_id = 2)
├── 产品部(up_id = 1)
│ └── 设计组(up_id = 4)
└── 运营部(up_id = 1)邻接表模式的优点:
- 结构简单:只需要一个
up_id字段,表结构清晰。 - 插入/更新高效:添加或移动节点只需要修改一条记录的
up_id。 - 直观易懂:父子关系一目了然。
邻接表模式的缺点:
- 查询子树需要递归:获取某个部门的所有子部门需要递归查询,在 MySQL 8.0 之前需要使用存储过程或应用层递归。
- 查询路径复杂:获取从根节点到某个节点的完整路径需要多次查询。
8.4.2 MySQL 8.0 的递归 CTE 支持
MySQL 8.0 引入了通用表表达式(Common Table Expression,CTE),包括递归 CTE,可以方便地查询树形数据:
sql
-- 查询某个部门的所有子部门(递归 CTE)
WITH RECURSIVE dept_tree AS (
-- 基础查询:找到起始部门
SELECT id, name, up_id, 1 AS level
FROM department_info
WHERE id = #{departmentId}
UNION ALL
-- 递归查询:找到所有子部门
SELECT d.id, d.name, d.up_id, dt.level + 1
FROM department_info d
INNER JOIN dept_tree dt ON d.up_id = dt.id
)
SELECT * FROM dept_tree ORDER BY level;sql
-- 查询从某个部门到根部门的完整路径
WITH RECURSIVE dept_path AS (
-- 基础查询:起始部门
SELECT id, name, up_id
FROM department_info
WHERE id = #{departmentId}
UNION ALL
-- 递归查询:向上查找父部门
SELECT d.id, d.name, d.up_id
FROM department_info d
INNER JOIN dept_path dp ON d.id = dp.up_id
)
SELECT * FROM dept_path;8.4.3 其他树形结构方案对比
除了邻接表模式,常见的树形结构方案还有:
| 方案 | 存储方式 | 查询子树 | 查询路径 | 插入/移动 | 适用场景 |
|---|---|---|---|---|---|
| 邻接表 | up_id | 递归/CTE | 递归/CTE | O(1) | 层级不深,读写均衡 |
| 路径枚举 | path字段 | LIKE查询 | 直接读取 | O(n)更新路径 | 读多写少 |
| 嵌套集 | left/right | 范围查询 | 范围查询 | O(n)更新 | 读多写少,层级深 |
| 闭包表 | 关系表 | JOIN查询 | JOIN查询 | O(log n) | 层级深,查询频繁 |
对于大多数企业应用中的部门管理场景,邻接表 + MySQL 8.0 递归 CTE 是最实用的方案。
8.5 字符集选择:utf8mb4 的必要性
在 MySQL 中选择字符集时,utf8mb4 是现代应用的标准选择。以下是 utf8mb4 与 MySQL utf8 的关键区别:
| 特性 | MySQL utf8 | MySQL utf8mb4 |
|---|---|---|
| 最大字节长度 | 3 字节 | 4 字节 |
| 支持的 Unicode 范围 | BMP(基本多文种平面) | 全部 Unicode |
| emoji 支持 | 不支持 | 支持 |
| 生僻字支持 | 部分不支持 | 完全支持 |
| 存储空间 | 略小 | 略大 |
| 索引长度限制 | 更宽松 | 需要注意 |
utf8mb4 的排序规则选择:
utf8mb4_unicode_ci:基于 Unicode 标准的排序规则,排序准确但速度稍慢。适合需要精确排序的场景。utf8mb4_general_ci:通用排序规则,速度较快但排序不够精确。适合对排序精度要求不高的场景。utf8mb4_0900_ai_ci:MySQL 8.0 新增的排序规则,基于 Unicode 9.0 标准,兼顾准确性和性能。推荐在 MySQL 8.0+ 环境中使用。
九、通用 DAO 层封装
9.1 设计思路
在传统的 MyBatis 开发中,每个 Mapper 接口都需要定义一套标准的 CRUD 方法(insert、delete、update、select),大量重复的代码降低了开发效率。smart-scaffold-springboot 项目通过泛型接口 BaseMapper<T, Q> 封装了通用的数据访问方法,大幅减少了重复代码。
设计目标:
- 零重复代码:所有通用的 CRUD 方法在 BaseMapper 中定义一次,具体 Mapper 接口只需继承即可。
- 类型安全:通过 Java 泛型确保编译时的类型安全。
- 可扩展:具体 Mapper 接口可以在继承通用方法的基础上添加自定义方法。
- 分页支持:内置分页查询能力,支持灵活的分页参数。
9.2 BaseMapper 泛型接口设计
java
public interface BaseMapper<T, Q extends PageDTO> {
// 新增记录(选择性插入,只插入非null字段)
Integer insertSelective(T record);
// 通过主键删除记录
Integer deleteByPrimaryKey(Long id);
// 通过主键查询记录
T selectByPrimaryKey(Long id);
// 修改记录(选择性更新,只更新非null字段)
Integer updateByPrimaryKeySelective(T record);
// 修改记录(全部字段更新)
Integer updateByPrimaryKey(T record);
// 根据查询条件获取记录列表
List<T> selectBy(Q queryDTO);
// 根据查询条件获取记录总数
Integer countBy(Q queryDTO);
// 根据查询条件获取唯一记录
T uniqueBy(Q queryDTO);
}泛型参数说明:
- T:实体类型 / DTO 类型,表示数据库记录对应的 Java 对象。
- Q:查询条件类型,继承自
PageDTO,封装了分页参数和查询条件。
为什么提供 insertSelective 和 updateByPrimaryKeySelective?
insertSelective 和 updateByPrimaryKeySelective 是 MyBatis Generator(MBG)生成的标准方法名。与全量插入/更新相比,选择性操作只处理非 null 的字段,有以下好处:
- 数据库默认值保护:如果某个字段在数据库中有默认值,选择性插入不会覆盖默认值。
- 部分更新:更新操作只修改传入的字段,不影响其他字段。
- 灵活性:同一个方法可以用于不同的业务场景,只需传入不同的字段组合。
9.3 PageDTO 分页参数封装
PageDTO 是所有查询条件 DTO 的基类,封装了通用的分页参数:
java
public class PageDTO implements Serializable {
private String fields = "id"; // 排序字段
private String order = "desc"; // 排序方向
private Boolean isPage = false; // 是否分页
private Integer page = 1; // 当前页码
private Integer pageSize = 20; // 每页大小
// 计算分页起始位置
public Integer getStart() {
return (page - 1) * pageSize;
}
// 获取每页大小(用于 LIMIT 子句)
public Integer getEnd() {
return pageSize;
}
}设计要点:
fields和order:通用的排序参数,在 Mapper XML 中通过<choose>标签实现白名单校验,防止 SQL 注入。isPage:控制是否启用分页。同一个查询方法可以通过isPage参数切换分页和非分页模式,减少 Mapper 方法的数量。start和end:计算属性,直接映射到 SQL 的LIMIT #{start}, #{end}子句。start是偏移量,end是每页大小。- 参数校验:
setPage和setPageSize方法中包含参数校验逻辑,确保页码和每页大小为正整数。
9.4 BaseService 通用服务层
与 BaseMapper 对应,BaseService 封装了通用的服务层逻辑:
java
public abstract class BaseService<M extends BaseMapper<T, Q>, T, Q extends PageDTO> {
@Autowired
protected M mapper;
// 通过主键查询
public T get(Long id) {
return handleQueryResult(mapper.selectByPrimaryKey(id), null);
}
// 通过主键删除
public void remove(Long id) {
mapper.deleteByPrimaryKey(id);
}
// 分页查询
public PageEntity<T> selectPageBy(Q queryDTO) {
queryDTO.setIsPage(true);
handleQueryParam(queryDTO);
PageEntity<T> pageEntity = new PageEntity<>(
handleQueryResult(mapper.selectBy(queryDTO)),
mapper.countBy(queryDTO)
);
pageEntity.setPage(queryDTO.getPage());
pageEntity.setPageSize(queryDTO.getPageSize());
return pageEntity;
}
// 列表查询(不分页)
public List<T> selectBy(Q queryDTO) {
queryDTO.setIsPage(false);
return handleQueryResult(mapper.selectBy(queryDTO));
}
// 查询前入参处理(子类可重写)
public Q handleQueryParam(Q queryDTO) {
return queryDTO;
}
// 查询后结果处理(子类可重写)
public T handleQueryResult(T dto) {
return dto;
}
}设计模式分析:
模板方法模式:
BaseService定义了查询的骨架流程(设置分页参数 -> 处理入参 -> 执行查询 -> 处理结果),子类通过重写handleQueryParam和handleQueryResult方法来定制具体行为。泛型约束:
M extends BaseMapper<T, Q>确保注入的 Mapper 实现了 BaseMapper 接口。三个泛型参数(M、T、Q)分别代表 Mapper 类型、实体类型和查询条件类型。分页查询实现:
selectPageBy方法先执行列表查询获取当前页数据,再执行countBy获取总记录数,然后封装为PageEntity对象。这种"两次查询"的分页方式在 MySQL 中是最高效的。
9.5 具体实现示例
以用户模型服务为例,展示 BaseMapper 和 BaseService 的使用方式:
Mapper 接口:
java
public interface UserModelMapper extends BaseMapper<UserModelDTO, UserModelQueryDTO> {
// 继承 BaseMapper 的所有通用方法
// 可以在此添加自定义方法
}Service 实现:
java
@Service
public class UserModelService extends BaseService<UserModelMapper, UserModelDTO, UserModelQueryDTO> {
public void save(UserModelDTO dto, String userId, String userName) {
dto.setAdminId(userId);
dto.setAdminName(userName);
dto.setTimeUpdate(new Date());
if (dto.getId() == null) {
dto.setTimeCreate(new Date());
mapper.insertSelective(dto);
} else {
mapper.updateByPrimaryKeySelective(dto);
}
}
@Override
public UserModelDTO handleQueryResult(UserModelDTO dto) {
// 可以在此对查询结果进行后处理
// 例如:脱敏、格式转换、关联数据加载等
return dto;
}
}Controller 使用:
java
@RestController
@RequestMapping("/user-model")
public class UserModelController {
@Autowired
private UserModelService userModelService;
@PostMapping("/list/page")
public BaseResult<?> listPage(@RequestBody UserModelQueryDTO queryDTO) {
queryDTO.setFields("id");
queryDTO.setOrder("desc");
PageEntity<UserModelDTO> result = userModelService.selectPageBy(queryDTO);
return BaseResult.success(result);
}
}9.6 通用封装的收益评估
通过 BaseMapper + BaseService 的通用封装,项目获得了以下收益:
| 维度 | 无封装 | 有封装 | 收益 |
|---|---|---|---|
| Mapper 方法数 | 每个Mapper 8+ 个方法 | 继承即可,0 行代码 | 减少 80%+ 重复代码 |
| Service 方法数 | 每个Service 5+ 个方法 | 继承即可,按需重写 | 减少 60%+ 重复代码 |
| 分页逻辑 | 每个Service重复实现 | 统一在BaseService中 | 一处修改全局生效 |
| 查询结果处理 | 分散在各Service | 通过模板方法统一处理 | 便于添加全局逻辑 |
十、踩坑与最佳实践
10.1 多数据源下的循环依赖问题
问题描述:
在 Spring Boot 多数据源配置中,循环依赖是一个常见且棘手的问题。典型表现是应用启动时报错:
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| db1SqlSessionFactory (field private javax.sql.DataSource ...)
↑ ↓
| db1DataSource (field private ...)
└─────┘根本原因:
循环依赖的根本原因是 Bean 之间的相互引用。在多数据源配置中,以下情况容易产生循环依赖:
- 配置类之间的依赖:如果两个配置类之间有直接的或间接的依赖关系。
- @MapperScan 的初始化时机:
@MapperScan触发的 Mapper 代理对象创建可能依赖于其他 Bean。 - 事务管理器的依赖:事务管理器依赖 DataSource,而某些 Bean 又依赖事务管理器。
解决方案:
- 使用
@Lazy注解延迟加载:
java
@Bean("db2SqlSessionFactory")
public SqlSessionFactory db2SqlSessionFactory(
@Lazy @Qualifier("db2DataSource") DataSource dataSource) throws Exception {
// ...
}@Lazy 注解会创建一个代理对象,延迟实际 Bean 的初始化,从而打破循环依赖链。
重构配置类,消除不必要的依赖:检查配置类之间是否存在不必要的依赖关系,通过重构消除循环引用。
使用
ObjectProvider延迟获取依赖:
java
@Bean
public SqlSessionFactory sqlSessionFactory(
ObjectProvider<DataSource> dataSourceProvider) throws Exception {
DataSource dataSource = dataSourceProvider.getIfAvailable();
// ...
}- 调整配置类的加载顺序:通过
@Order注解或@AutoConfigureBefore/@AutoConfigureAfter注解控制配置类的加载顺序。
10.2 Mapper 接口包路径隔离
问题描述:
如果两个数据源的 Mapper 接口不在不同的包路径下,或者 @MapperScan 的 basePackages 配置有重叠,会导致以下问题:
- Mapper 接口被错误的 SqlSessionFactory 处理,导致 SQL 执行时找不到对应的表。
- Spring 启动时报错,提示 Mapper 接口被多个 SqlSessionFactory 注册。
最佳实践:
- 严格的包路径隔离:每个数据源的 Mapper 接口必须位于独立的 Java 包中,包路径不能有任何重叠。
com.example.dao.mapper.db1/ -- 主库 Mapper 接口
com.example.dao.mapper.db2/ -- 从库 Mapper 接口- 精确的 @MapperScan 配置:
basePackages应该使用精确的包路径,避免使用通配符导致意外扫描。
java
// 正确:精确指定包路径
@MapperScan(basePackages = "com.example.dao.mapper.db1",
sqlSessionFactoryRef = "db1SqlSessionFactory")
// 错误:通配符可能扫描到其他包
@MapperScan(basePackages = "com.example.dao.mapper.*",
sqlSessionFactoryRef = "db1SqlSessionFactory")- 编译时检查:通过单元测试验证每个 Mapper 接口被正确的 SqlSessionFactory 处理。可以在测试中注入 Mapper 接口,执行简单的查询操作,确认数据源路由正确。
10.3 SqlSessionTemplate 的线程安全性
问题描述:
SqlSessionTemplate 是 MyBatis-Spring 提供的线程安全 SqlSession 实现。但在多数据源配置中,如果使用不当,仍然可能出现线程安全问题。
SqlSessionTemplate 的线程安全机制:
SqlSessionTemplate 内部维护了一个 SqlSessionProxy(动态代理),每次调用方法时都会从当前的 SqlSessionFactory 获取一个新的 SqlSession,方法执行完毕后自动关闭。因此,SqlSessionTemplate 本身是线程安全的,可以安全地在多个线程中共享。
需要注意的场景:
不要直接使用 SqlSession:如果通过
SqlSessionTemplate获取了原生的SqlSession对象并传递到其他方法中,可能会导致线程安全问题。应该始终通过 Mapper 接口或SqlSessionTemplate的方法来操作数据库。批量操作的线程安全:在使用
SqlSession的批量模式时,需要注意批量操作需要在同一个SqlSession中完成。SqlSessionTemplate默认每次调用创建新的SqlSession,不适合批量操作。如果需要批量操作,可以使用SqlSessionFactory.openSession(ExecutorType.BATCH)手动创建批量的SqlSession。
java
// 批量操作示例
try (SqlSession batchSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserModelMapper batchMapper = batchSession.getMapper(UserModelMapper.class);
for (UserModelDTO dto : dataList) {
batchMapper.insertSelective(dto);
}
batchSession.commit();
}- 事务与 SqlSession 的关系:在事务中,Spring 的事务管理器会确保整个事务使用同一个
SqlSession,这与SqlSessionTemplate的默认行为不同。事务中的SqlSession由SpringManagedTransaction管理,在事务提交或回滚时关闭。
10.4 连接池监控与调优
监控指标:
Druid 连接池提供了丰富的监控指标,以下是核心指标的说明:
| 指标 | 说明 | 健康范围 |
|---|---|---|
| ActiveCount | 当前活跃连接数 | < maxActive 的 70% |
| PoolingCount | 当前空闲连接数 | > minIdle |
| WaitThreadCount | 等待获取连接的线程数 | 0(理想状态) |
| ConnectCount | 累计创建连接数 | 持续增长是正常的 |
| CloseCount | 累计关闭连接数 | 与 ConnectCount 接近 |
| ConnectErrorCount | 连接创建失败次数 | 0 |
| ExecuteCount | SQL 执行次数 | - |
| ErrorCount | SQL 执行错误次数 | 0 |
| CommitCount | 事务提交次数 | - |
| RollbackCount | 事务回滚次数 | 越少越好 |
调优建议:
- 初始调优:从保守的参数开始(如
maxActive=20),根据监控数据逐步调整。 - 关注 WaitThreadCount:如果频繁出现等待线程,说明连接池容量不足,需要增加
maxActive。 - 关注连接创建频率:如果
ConnectCount持续快速增长,说明连接被频繁创建和销毁,需要检查minIdle和maxEvictableIdleTimeMillis的配置。 - 慢 SQL 分析:通过 Druid 监控页面的 SQL 统计功能,找出执行时间最长的 SQL,进行优化。
- 定期巡检:建议每周检查一次连接池监控数据,及时发现潜在问题。
10.5 Spring Boot 3.x 迁移注意事项
从 Spring Boot 2.x 升级到 3.x 时,多数据源配置需要注意以下变化:
javax 到 jakarta 的包名变更:Spring Boot 3.x 基于 Jakarta EE 9+,
javax.sql.DataSource需要改为jakarta.sql.DataSource。但实际上,java.sql.DataSource是 Java 标准库的一部分,不受此变更影响。在配置类中,import javax.sql.DataSource可以正常使用,因为javax.sql包属于 Java SE,不属于 Jakarta EE。MyBatis Starter 版本兼容性:MyBatis Spring Boot Starter 3.x 才支持 Spring Boot 3.x。使用 Starter 2.x 会出现兼容性问题。
自动配置变更:Spring Boot 3.x 的自动配置机制有所调整,某些自动配置类的名称和行为发生了变化。建议在多数据源配置中显式排除不需要的自动配置:
java
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class
})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}- Hibernate Validator 变更:Spring Boot 3.x 使用 Hibernate Validator 8.x(基于 Jakarta Bean Validation 3.0),验证注解的包名从
javax.validation变更为jakarta.validation。
10.6 其他常见问题
问题一:Druid 连接池无法启动
现象:应用启动时报错 Invalid connection string format。
原因:在多数据源配置中,YAML 属性名使用了 url 而不是 jdbc-url。DataSourceProperties 在多数据源场景下需要使用 jdbc-url 属性名。
解决方案:将 spring.datasource.db1.url 改为 spring.datasource.db1.jdbc-url。
问题二:Mapper 接口注入失败
现象:Spring 启动时报错 No qualifying bean of type 'xxxMapper'。
原因:Mapper 接口的包路径没有被 @MapperScan 扫描到。
解决方案:检查 @MapperScan 的 basePackages 配置,确保包含了 Mapper 接口所在的包路径。
问题三:事务不生效
现象:方法抛出异常后数据库操作没有回滚。
原因:
@Transactional注解的方法是private的(Spring AOP 无法拦截私有方法)- 异常被 catch 块吞掉了,没有重新抛出
- 没有配置
rollbackFor = Exception.class,抛出的是受检异常 - 同一个类中的方法调用,没有经过 Spring 代理
解决方案:
- 确保
@Transactional注解的方法是public的 - 在 catch 块中重新抛出异常,或使用
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() - 配置
rollbackFor = Exception.class - 避免同一个类中的自调用,如果必须自调用,可以通过
AopContext.currentProxy()获取代理对象
十一、总结与展望
11.1 方案总结
本文基于 smart-scaffold-springboot 真实项目,深入解析了 Spring Boot 3.x 环境下 MyBatis + Druid 双数据源的配置方案。核心要点回顾:
架构设计:每个数据源独立配置 DataSource、SqlSessionFactory、SqlSessionTemplate,通过
@MapperScan的包路径隔离 Mapper 接口,通过@Primary解决默认注入问题。连接池调优:Druid 连接池提供了丰富的参数配置选项,需要根据业务场景合理设置初始连接数、最大连接数、空闲回收策略和连接验证策略。
MyBatis 集成:通过
SqlSessionFactoryBean配置 Mapper XML 路径、实体类别名和全局配置,利用 MyBatis 的动态 SQL 能力实现灵活的查询逻辑。事务管理:多数据源下的事务管理需要特别注意,跨数据源操作无法使用本地事务保证原子性,需要根据业务需求选择合适的分布式事务方案。
通用封装:通过 BaseMapper + BaseService 的泛型封装,大幅减少重复的 CRUD 代码,提高开发效率。
生产实践:循环依赖、Mapper 包路径隔离、SqlSessionTemplate 线程安全、连接池监控等是实际项目中需要重点关注的问题。
11.2 架构演进方向
随着业务的发展,多数据源架构可能需要向以下方向演进:
动态数据源路由:基于
AbstractRoutingDataSource实现运行时动态切换数据源,配合自定义注解(如@TargetDataSource("db2"))实现声明式的数据源路由。读写分离增强:引入数据库中间件(如 ShardingSphere、MyCat)实现透明的读写分离,支持主从切换和负载均衡。
分库分表:在垂直分库的基础上,对热点表进行水平分表,使用 ShardingSphere 等中间件管理分片规则。
云原生数据库:考虑使用云服务商的托管数据库服务(如阿里云 PolarDB、AWS Aurora),利用其读写分离和自动扩缩容能力。
数据网格(Data Mesh):在微服务架构中,将数据访问封装为独立的数据服务,实现数据层的解耦和自治。
11.3 参考资源
版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。
本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。
文档内容提取自项目源码与配置文件,如需获取完整项目代码,请访问 bima.cc。