在实际开发中,单数据源往往无法满足复杂的业务场景 —— 比如读写分离、分库分表、不同业务模块对接不同数据库等。springboot 作为主流的开发框架,提供了多种多数据源整合方案,从简单的静态切换到灵活的动态路由,每种方案都有其适用场景。本文将从实际业务需求出发,拆解 springboot 中多数据源的核心实现方式,并附上可直接运行的代码示例。
一、多数据源核心场景与技术选型
先明确多数据源的常见使用场景,避免盲目选型:
- 静态多数据源:不同业务模块固定对接不同数据库(如订单库、用户库),启动时加载,运行时不切换;
- 动态切换数据源:运行时根据条件(如用户 id、业务标识)动态选择数据源(如读写分离、分库);
- 分布式事务:多数据源操作需保证事务一致性(本文暂不展开,后续单独讲解)。
核心依赖(基于 springboot 2.7.x):
<dependencies>
<!-- springboot核心 -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<!-- 数据库连接 -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-jdbc</artifactid>
</dependency>
<dependency>
<groupid>com.baomidou</groupid>
<artifactid>mybatis-plus-boot-starter</artifactid>
<version>3.5.3.1</version>
</dependency>
<!-- 数据库驱动(以mysql为例) -->
<dependency>
<groupid>mysql</groupid>
<artifactid>mysql-connector-java</artifactid>
<scope>runtime</scope>
</dependency>
<!-- 连接池 -->
<dependency>
<groupid>com.alibaba</groupid>
<artifactid>druid-spring-boot-starter</artifactid>
<version>1.2.16</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-test</artifactid>
<scope>test</scope>
</dependency>
</dependencies>二、方案 1:静态多数据源(基于配置类分离)
适用场景
不同业务模块完全隔离,比如「用户模块」对接 user_db,「订单模块」对接 order_db,运行时无需切换数据源。
实现步骤
配置文件(application.yml)
spring:
datasource:
# 数据源1:用户库
user:
url: jdbc:mysql://localhost:3306/user_db?useunicode=true&characterencoding=utf8&usessl=false&servertimezone=asia/shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.driver
type: com.alibaba.druid.pool.druiddatasource
# 数据源2:订单库
order:
url: jdbc:mysql://localhost:3306/order_db?useunicode=true&characterencoding=utf8&usessl=false&servertimezone=asia/shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.driver
type: com.alibaba.druid.pool.druiddatasource
# mybatis-plus配置
mybatis-plus:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true数据源配置类
分别配置两个数据源的 bean,指定不同的扫描路径:
用户数据源配置(userdatasourceconfig.java):
package com.example.demo.config;
import com.baomidou.mybatisplus.core.mybatisconfiguration;
import com.baomidou.mybatisplus.core.mybatisxmllanguagedriver;
import com.baomidou.mybatisplus.core.toolkit.stringpool;
import com.baomidou.mybatisplus.extension.plugins.mybatisplusinterceptor;
import com.baomidou.mybatisplus.extension.spring.mybatissqlsessionfactorybean;
import org.apache.ibatis.session.sqlsessionfactory;
import org.apache.ibatis.type.jdbctype;
import org.mybatis.spring.sqlsessiontemplate;
import org.mybatis.spring.annotation.mapperscan;
import org.springframework.beans.factory.annotation.qualifier;
import org.springframework.boot.context.properties.configurationproperties;
import org.springframework.boot.jdbc.datasourcebuilder;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.context.annotation.primary;
import org.springframework.core.io.support.pathmatchingresourcepatternresolver;
import javax.sql.datasource;
/**
* 用户库数据源配置
* 扫描user模块的mapper
*/
@configuration
@mapperscan(basepackages = "com.example.demo.mapper.user", sqlsessiontemplateref = "usersqlsessiontemplate")
public class userdatasourceconfig {
/**
* 配置用户库数据源
*/
@bean(name = "userdatasource")
@configurationproperties(prefix = "spring.datasource.user")
@primary // 主数据源(必须指定一个主数据源)
public datasource userdatasource() {
return datasourcebuilder.create().type(com.alibaba.druid.pool.druiddatasource.class).build();
}
/**
* 配置用户库sqlsessionfactory
*/
@bean(name = "usersqlsessionfactory")
@primary
public sqlsessionfactory usersqlsessionfactory(@qualifier("userdatasource") datasource datasource,
mybatisplusinterceptor interceptor) throws exception {
mybatissqlsessionfactorybean sqlsessionfactory = new mybatissqlsessionfactorybean();
sqlsessionfactory.setdatasource(datasource);
// 配置mybatis-plus
mybatisconfiguration configuration = new mybatisconfiguration();
configuration.setdefaultscriptinglanguage(mybatisxmllanguagedriver.class);
configuration.setjdbctypefornull(jdbctype.null);
sqlsessionfactory.setconfiguration(configuration);
// 分页插件(可选)
sqlsessionfactory.setplugins(interceptor);
// mapper文件路径
sqlsessionfactory.setmapperlocations(new pathmatchingresourcepatternresolver()
.getresources("classpath:mapper/user/*.xml"));
return sqlsessionfactory.getobject();
}
/**
* 配置用户库sqlsessiontemplate
*/
@bean(name = "usersqlsessiontemplate")
@primary
public sqlsessiontemplate usersqlsessiontemplate(@qualifier("usersqlsessionfactory") sqlsessionfactory sqlsessionfactory) {
return new sqlsessiontemplate(sqlsessionfactory);
}
}订单数据源配置(orderdatasourceconfig.java):
package com.example.demo.config;
import com.baomidou.mybatisplus.core.mybatisconfiguration;
import com.baomidou.mybatisplus.core.mybatisxmllanguagedriver;
import com.baomidou.mybatisplus.extension.plugins.mybatisplusinterceptor;
import com.baomidou.mybatisplus.extension.spring.mybatissqlsessionfactorybean;
import org.apache.ibatis.session.sqlsessionfactory;
import org.apache.ibatis.type.jdbctype;
import org.mybatis.spring.sqlsessiontemplate;
import org.mybatis.spring.annotation.mapperscan;
import org.springframework.beans.factory.annotation.qualifier;
import org.springframework.boot.context.properties.configurationproperties;
import org.springframework.boot.jdbc.datasourcebuilder;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.core.io.support.pathmatchingresourcepatternresolver;
import javax.sql.datasource;
/**
* 订单库数据源配置
* 扫描order模块的mapper
*/
@configuration
@mapperscan(basepackages = "com.example.demo.mapper.order", sqlsessiontemplateref = "ordersqlsessiontemplate")
public class orderdatasourceconfig {
@bean(name = "orderdatasource")
@configurationproperties(prefix = "spring.datasource.order")
public datasource orderdatasource() {
return datasourcebuilder.create().type(com.alibaba.druid.pool.druiddatasource.class).build();
}
@bean(name = "ordersqlsessionfactory")
public sqlsessionfactory ordersqlsessionfactory(@qualifier("orderdatasource") datasource datasource,
mybatisplusinterceptor interceptor) throws exception {
mybatissqlsessionfactorybean sqlsessionfactory = new mybatissqlsessionfactorybean();
sqlsessionfactory.setdatasource(datasource);
mybatisconfiguration configuration = new mybatisconfiguration();
configuration.setdefaultscriptinglanguage(mybatisxmllanguagedriver.class);
configuration.setjdbctypefornull(jdbctype.null);
sqlsessionfactory.setconfiguration(configuration);
sqlsessionfactory.setplugins(interceptor);
sqlsessionfactory.setmapperlocations(new pathmatchingresourcepatternresolver()
.getresources("classpath:mapper/order/*.xml"));
return sqlsessionfactory.getobject();
}
@bean(name = "ordersqlsessiontemplate")
public sqlsessiontemplate ordersqlsessiontemplate(@qualifier("ordersqlsessionfactory") sqlsessionfactory sqlsessionfactory) {
return new sqlsessiontemplate(sqlsessionfactory);
}
}
业务代码示例
用户 mapper(usermapper.java):放在com.example.demo.mapper.user包下
package com.example.demo.mapper.user;
import com.baomidou.mybatisplus.core.mapper.basemapper;
import com.example.demo.entity.user;
import org.apache.ibatis.annotations.mapper;
@mapper
public interface usermapper extends basemapper<user> {
}
订单 mapper(ordermapper.java):放在com.example.demo.mapper.order包下
package com.example.demo.mapper.order;
import com.baomidou.mybatisplus.core.mapper.basemapper;
import com.example.demo.entity.order;
import org.apache.ibatis.annotations.mapper;
@mapper
public interface ordermapper extends basemapper<order> {
}
业务层调用:
package com.example.demo.service.impl;
import com.example.demo.entity.order;
import com.example.demo.entity.user;
import com.example.demo.mapper.order.ordermapper;
import com.example.demo.mapper.user.usermapper;
import com.example.demo.service.datasourceservice;
import org.springframework.stereotype.service;
import javax.annotation.resource;
@service
public class datasourceserviceimpl implements datasourceservice {
@resource
private usermapper usermapper;
@resource
private ordermapper ordermapper;
@override
public user getuserbyid(long id) {
// 自动使用用户库数据源
return usermapper.selectbyid(id);
}
@override
public order getorderbyid(long id) {
// 自动使用订单库数据源
return ordermapper.selectbyid(id);
}
}
优缺点
优点:配置简单、无侵入性、性能高,适合模块隔离的场景;
缺点:无法动态切换,新增数据源需新增配置类,灵活性低。
三、方案 2:动态切换数据源(基于 abstractroutingdatasource)
适用场景
需要根据业务逻辑动态切换数据源,比如「读写分离」(读库 / 写库切换)、「分库」(按用户 id 路由到不同库)。
核心思路:继承 spring 提供的abstractroutingdatasource,重写determinecurrentlookupkey方法,通过 threadlocal 存储当前线程的数据源标识,实现动态路由。
实现步骤
配置文件(application.yml)
spring:
datasource:
# 主库(写)
master:
url: jdbc:mysql://localhost:3306/master_db?useunicode=true&characterencoding=utf8&usessl=false&servertimezone=asia/shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.driver
# 从库(读)
slave:
url: jdbc:mysql://localhost:3306/slave_db?useunicode=true&characterencoding=utf8&usessl=false&servertimezone=asia/shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.driver
type: com.alibaba.druid.pool.druiddatasource核心工具类
数据源上下文 holder(dynamicdatasourcecontextholder.java):
package com.example.demo.config.dynamic;
/**
* 数据源上下文 holder,基于threadlocal存储当前线程的数据源标识
*/
public class dynamicdatasourcecontextholder {
/**
* 线程本地变量:存储当前线程使用的数据源标识
*/
private static final threadlocal<string> context_holder = new threadlocal<>();
/**
* 设置数据源标识
*/
public static void setdatasourcekey(string key) {
context_holder.set(key);
}
/**
* 获取数据源标识
*/
public static string getdatasourcekey() {
return context_holder.get();
}
/**
* 清除数据源标识
*/
public static void cleardatasourcekey() {
context_holder.remove();
}
}
动态数据源路由类(dynamicroutingdatasource.java):
package com.example.demo.config.dynamic;
import org.springframework.jdbc.datasource.lookup.abstractroutingdatasource;
/**
* 动态数据源路由:重写determinecurrentlookupkey方法,返回当前线程的数据源标识
*/
public class dynamicroutingdatasource extends abstractroutingdatasource {
@override
protected object determinecurrentlookupkey() {
// 从threadlocal中获取当前线程的数据源标识
return dynamicdatasourcecontextholder.getdatasourcekey();
}
}
数据源配置类
package com.example.demo.config.dynamic;
import com.baomidou.mybatisplus.extension.plugins.mybatisplusinterceptor;
import com.baomidou.mybatisplus.extension.spring.mybatissqlsessionfactorybean;
import org.apache.ibatis.session.sqlsessionfactory;
import org.mybatis.spring.annotation.mapperscan;
import org.springframework.beans.factory.annotation.qualifier;
import org.springframework.boot.context.properties.configurationproperties;
import org.springframework.boot.jdbc.datasourcebuilder;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.context.annotation.primary;
import org.springframework.core.io.support.pathmatchingresourcepatternresolver;
import javax.sql.datasource;
import java.util.hashmap;
import java.util.map;
/**
* 动态数据源配置
*/
@configuration
@mapperscan(basepackages = "com.example.demo.mapper", sqlsessionfactoryref = "dynamicsqlsessionfactory")
public class dynamicdatasourceconfig {
/**
* 配置主库数据源
*/
@bean(name = "masterdatasource")
@configurationproperties(prefix = "spring.datasource.master")
public datasource masterdatasource() {
return datasourcebuilder.create().type(com.alibaba.druid.pool.druiddatasource.class).build();
}
/**
* 配置从库数据源
*/
@bean(name = "slavedatasource")
@configurationproperties(prefix = "spring.datasource.slave")
public datasource slavedatasource() {
return datasourcebuilder.create().type(com.alibaba.druid.pool.druiddatasource.class).build();
}
/**
* 配置动态数据源(核心)
*/
@bean(name = "dynamicdatasource")
@primary
public datasource dynamicdatasource(@qualifier("masterdatasource") datasource masterdatasource,
@qualifier("slavedatasource") datasource slavedatasource) {
dynamicroutingdatasource dynamicroutingdatasource = new dynamicroutingdatasource();
// 1. 配置默认数据源(主库)
dynamicroutingdatasource.setdefaulttargetdatasource(masterdatasource);
// 2. 配置所有数据源(key为数据源标识,value为数据源)
map<object, object> datasourcemap = new hashmap<>();
datasourcemap.put("master", masterdatasource);
datasourcemap.put("slave", slavedatasource);
dynamicroutingdatasource.settargetdatasources(datasourcemap);
return dynamicroutingdatasource;
}
/**
* 配置sqlsessionfactory
*/
@bean(name = "dynamicsqlsessionfactory")
public sqlsessionfactory dynamicsqlsessionfactory(@qualifier("dynamicdatasource") datasource datasource,
mybatisplusinterceptor interceptor) throws exception {
mybatissqlsessionfactorybean sqlsessionfactory = new mybatissqlsessionfactorybean();
sqlsessionfactory.setdatasource(datasource);
sqlsessionfactory.setmapperlocations(new pathmatchingresourcepatternresolver()
.getresources("classpath:mapper/**/*.xml"));
sqlsessionfactory.setplugins(interceptor);
return sqlsessionfactory.getobject();
}
}
自定义注解 + aop 实现自动切换
数据源注解(datasource.java):
package com.example.demo.annotation;
import java.lang.annotation.*;
/**
* 自定义数据源注解:标注在方法/类上,指定使用的数据源
*/
@target({elementtype.method, elementtype.type})
@retention(retentionpolicy.runtime)
@documented
public @interface datasource {
/**
* 数据源标识,对应dynamicdatasource中的key
*/
string value() default "master";
}
aop 切面(datasourceaspect.java):
package com.example.demo.aspect;
import com.example.demo.annotation.datasource;
import com.example.demo.config.dynamic.dynamicdatasourcecontextholder;
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.around;
import org.aspectj.lang.annotation.aspect;
import org.aspectj.lang.annotation.pointcut;
import org.aspectj.lang.reflect.methodsignature;
import org.springframework.core.annotation.order;
import org.springframework.stereotype.component;
import java.lang.reflect.method;
/**
* 数据源切换切面:拦截@datasource注解,设置对应的数据源标识
*/
@aspect
@component
@order(-1) // 保证切面优先级高于事务
public class datasourceaspect {
/**
* 切入点:拦截所有标注@datasource的方法/类
*/
@pointcut("@annotation(com.example.demo.annotation.datasource) || @within(com.example.demo.annotation.datasource)")
public void datasourcepointcut() {}
/**
* 环绕通知:切换数据源
*/
@around("datasourcepointcut()")
public object around(proceedingjoinpoint point) throws throwable {
// 1. 获取注解中的数据源标识
string datasourcekey = getdatasourcekey(point);
// 2. 设置数据源标识到threadlocal
dynamicdatasourcecontextholder.setdatasourcekey(datasourcekey);
try {
// 3. 执行目标方法
return point.proceed();
} finally {
// 4. 清除数据源标识,避免线程复用导致的问题
dynamicdatasourcecontextholder.cleardatasourcekey();
}
}
/**
* 获取方法/类上的数据源标识
*/
private string getdatasourcekey(proceedingjoinpoint point) {
methodsignature signature = (methodsignature) point.getsignature();
method method = signature.getmethod();
// 优先获取方法上的注解
datasource methodannotation = method.getannotation(datasource.class);
if (methodannotation != null) {
return methodannotation.value();
}
// 方法上没有则获取类上的注解
class<?> targetclass = point.gettarget().getclass();
datasource classannotation = targetclass.getannotation(datasource.class);
if (classannotation != null) {
return classannotation.value();
}
// 默认使用主库
return "master";
}
}
业务代码示例
package com.example.demo.service.impl;
import com.example.demo.annotation.datasource;
import com.example.demo.entity.user;
import com.example.demo.mapper.usermapper;
import com.example.demo.service.userservice;
import org.springframework.stereotype.service;
import javax.annotation.resource;
@service
public class userserviceimpl implements userservice {
@resource
private usermapper usermapper;
/**
* 写操作:使用主库
*/
@override
@datasource("master")
public void saveuser(user user) {
usermapper.insert(user);
}
/**
* 读操作:使用从库
*/
@override
@datasource("slave")
public user getuserbyid(long id) {
return usermapper.selectbyid(id);
}
}
优缺点
优点:灵活性高,支持运行时动态切换,可扩展至 n 个数据源;
缺点:需要手动管理 threadlocal,切面优先级需高于事务,否则切换失效。
四、方案 3:基于 mybatis-plus 插件(dynamic-datasource-spring-boot-starter)
适用场景
追求极简配置,快速实现多数据源切换(推荐生产环境使用)。
dynamic-datasource-spring-boot-starter 是 mybatis-plus 团队提供的多数据源插件,封装了 abstractroutingdatasource 的底层逻辑,支持注解、spel 表达式、分布式场景等,无需手动编写切面和路由类。
实现步骤
引入依赖
<!-- 动态数据源插件(核心) -->
<dependency>
<groupid>com.baomidou</groupid>
<artifactid>dynamic-datasource-spring-boot-starter</artifactid>
<version>3.6.1</version>
</dependency>配置文件(application.yml)
spring:
# 动态数据源配置
dynamic:
datasource:
# 主库(默认)
master:
url: jdbc:mysql://localhost:3306/master_db?useunicode=true&characterencoding=utf8&usessl=false&servertimezone=asia/shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.driver
# 从库1
slave1:
url: jdbc:mysql://localhost:3306/slave1_db?useunicode=true&characterencoding=utf8&usessl=false&servertimezone=asia/shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.driver
# 从库2
slave2:
url: jdbc:mysql://localhost:3306/slave2_db?useunicode=true&characterencoding=utf8&usessl=false&servertimezone=asia/shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.driver
# 配置默认数据源
primary: master
# 配置druid连接池
type: com.alibaba.druid.pool.druiddatasource业务代码示例
直接使用插件提供的@ds注解切换数据源:
package com.example.demo.service.impl;
import com.baomidou.dynamic.datasource.annotation.ds;
import com.example.demo.entity.user;
import com.example.demo.mapper.usermapper;
import com.example.demo.service.userservice;
import org.springframework.stereotype.service;
import javax.annotation.resource;
@service
public class userserviceimpl implements userservice {
@resource
private usermapper usermapper;
/**
* 默认使用主库(可不加注解)
*/
@override
public void saveuser(user user) {
usermapper.insert(user);
}
/**
* 使用从库1
*/
@override
@ds("slave1")
public user getuserbyid(long id) {
return usermapper.selectbyid(id);
}
/**
* 使用从库2
*/
@override
@ds("slave2")
public user getuserbyphone(string phone) {
return usermapper.selectone(new lambdaquerywrapper<user>().eq(user::getphone, phone));
}
}
进阶用法:spel 表达式动态路由
支持根据方法参数动态选择数据源(比如按用户 id 取模分库):
/**
* 根据用户id取模,路由到不同从库
*/
@override
@ds("#{id % 2 == 0 ? 'slave1' : 'slave2'}")
public user getuserbyid(long id) {
return usermapper.selectbyid(id);
}
优缺点
优点:零配置成本、功能丰富(支持读写分离、负载均衡、分布式锁)、官方维护;
缺点:依赖第三方插件,深度定制化场景需二次开发。
五、方案对比与选型建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 静态多数据源(配置类) | 配置简单、性能高、无侵入 | 无法动态切换、扩展性差 | 模块隔离的固定多数据源场景 |
| 自定义动态数据源 | 高度自定义、灵活性高 | 需手动编写代码、易出问题 | 特殊定制化的动态切换场景 |
| dynamic-datasource | 极简配置、功能丰富、稳定性高 | 依赖第三方插件 | 大部分生产环境(推荐) |
六、注意事项
- 事务问题:动态数据源切换需保证切面优先级高于事务(@order (-1)),否则事务内切换数据源失效;
- 线程安全:threadlocal 需在方法执行完毕后清除,避免线程池复用导致数据源串用;
- 连接池配置:多数据源场景下需合理配置连接池大小,避免连接耗尽;
- 读写分离:从库建议设置为只读,避免写入数据导致主从同步异常。
总结
springboot 整合多数据源的核心思路是「数据源路由」,不同方案只是封装程度不同。对于大部分开发者来说,优先选择 dynamic-datasource 插件,兼顾效率和稳定性;特殊定制化场景可基于 abstractroutingdatasource 手动实现;模块隔离场景则用静态多数据源即可。
本文所有代码均可直接复制运行,建议根据实际业务场景调整数据源配置和切换逻辑。
以上就是springboot实现整合多数据源的全攻略的详细内容,更多关于springboot整合多数据源的资料请关注代码网其它相关文章!
发表评论