SpringBoot + MyBatis多数据源实战:从读写分离到业务解耦的三种配置方案
1. 为什么你的项目需要多数据源?从单库到多库的演进之路
大家好,我是老张,在互联网公司摸爬滚打了十来年,从单体应用到微服务,数据库这块的坑踩了不少。今天咱们不聊那些高大上的概念,就聊聊一个几乎所有后端项目都会遇到的、实实在在的问题:一个应用,怎么同时连多个数据库?
你可能觉得,一个项目连一个数据库不是天经地义吗?没错,项目初期确实如此。但业务就像孩子,它会成长。我经历过一个典型的项目演进过程,和大家分享一下:
最开始,我们做一个电商后台,所有数据——用户、商品、订单、库存——都放在一个MySQL库里。简单直接,开发也快。但很快,问题来了。随着用户量暴涨,这个库的读写压力变得巨大,尤其是促销活动时,大量的读请求(查商品、查订单)把数据库CPU打满,写操作(下单、支付)也跟着变慢,整个系统卡顿。
这时候,架构师提出了第一个方案:读写分离。简单说,就是搞一个主库(Master)专门负责写(增删改),再搞一个或多个从库(Slave)专门负责读(查询)。主库的数据通过binlog同步到从库。这样,读的压力就被分摊了。对于业务代码来说,你需要在执行查询时,告诉框架:“这次请走从库”;在执行写入时,说:“这次请走主库”。这就是多数据源配置最经典、最普遍的应用场景。
业务继续发展,公司开始推行微服务化。原来的大单体被拆成了用户中心、商品中心、订单中心、支付中心等多个服务。理想情况下,每个服务都有自己的数据库,彻底解耦。但现实往往有历史包袱,可能因为某些原因(比如遗留系统、数据强关联),某个服务暂时还需要访问其他服务的数据库,或者需要连接一个专门用于数据分析的OLAP数据库(比如ClickHouse)。这时候,一个服务里就需要配置多个数据源,连接多个不同的数据库实例,这就是业务解耦或多库访问的场景。
还有一种情况是多租户(SaaS),不同租户的数据需要物理隔离,存放在不同的数据库里。应用需要根据当前登录的租户ID,动态决定连接到哪个租户的数据库。
所以你看,从简单的读写分离,到复杂的多业务库访问,再到多租户隔离,“多数据源”不是一个炫技的功能,而是业务发展到一定阶段后,为了解决性能、架构、安全隔离等问题而必须引入的技术方案。接下来,我就结合SpringBoot和MyBatis,给你详细拆解三种最主流的实现方案,每种方案我都会配上我实际踩过的坑和填坑经验,保证你能看懂、能上手。
2. 方案一:分包配置——清晰隔离,适合初期拆分
当你第一次需要引入第二个数据源时,我首推这种“分包”的方式。它的核心思想非常简单粗暴:为每个数据源创建独立的配置类、独立的Mapper接口包路径、独立的XML映射文件目录。让Spring为它们各自管理一套完整的MyBatis执行环境,互不干扰。
这种方式概念清晰,隔离彻底,特别适合数据源相对固定、不会频繁动态切换的场景,比如我们前面说的微服务拆分初期,一个服务固定访问A和B两个数据库。
2.1 项目结构与配置实战
我们先来看项目结构,这是理解分包方式的关键:
src/main/java
├── com.example.demo
│ ├── config
│ │ ├── Db1DataSourceConfig.java
│ │ └── Db2DataSourceConfig.java
│ ├── dao
│ │ ├── db1 // 数据源1的Mapper接口
│ │ │ └── UserMapper.java
│ │ └── db2 // 数据源2的Mapper接口
│ │ └── OrderMapper.java
│ └── service
│ └── SomeService.java
src/main/resources
├── application.yml
└── mapper
├── db1 // 数据源1的XML文件
│ └── UserMapper.xml
└── db2 // 数据源2的XML文件
└── OrderMapper.xml
第一步,在application.yml里配置两个数据源的连接信息:
spring:
datasource:
db1:
jdbc-url: jdbc:mysql://localhost:3306/user_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
db2:
jdbc-url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
这里我用了jdbc-url,因为在高版本SpringBoot中,url属性可能和默认数据源配置冲突。两个库我模拟了用户库和订单库。
第二步,也是最核心的一步,编写两个数据源的配置类。
我们以Db1DataSourceConfig为例,你需要为每个数据源都写一个这样的类:
@Configuration
// 重点1:指定这个数据源对应的Mapper接口包路径
@MapperScan(basePackages = "com.example.demo.dao.db1",
sqlSessionFactoryRef = "db1SqlSessionFactory")
public class Db1DataSourceConfig {
// 重点2:创建数据源Bean,绑定yml中db1的配置
@Bean(name = "db1DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db1")
@Primary // 重点3:标记一个默认数据源,Spring启动时需要
public DataSource db1DataSource() {
return DataSourceBuilder.create().build();
}
// 重点4:创建该数据源专属的SqlSessionFactory
@Bean(name = "db1SqlSessionFactory")
@Primary
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 重点5:指定该数据源对应的Mapper XML文件位置
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/db1/*.xml"));
return bean.getObject();
}
// 重点6:配置该数据源专属的事务管理器
@Bean(name = "db1TransactionManager")
@Primary
public DataSourceTransactionManager db1TransactionManager(@Qualifier("db1DataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
// 可选:创建SqlSessionTemplate
@Bean(name = "db1SqlSessionTemplate")
@Primary
public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
对于Db2DataSourceConfig,代码几乎一样,只需要把所有的db1改成db2,对应的包路径和XML路径也改掉,并且切记去掉@Primary注解。Spring容器中必须且只能有一个默认的主数据源Bean、一个主SqlSessionFactory等,通常我们会将最常用的那个数据源设为主数据源。
2.2 如何使用与事务管理坑点
配置好后,使用就非常直观了。假设UserMapper在com.example.demo.dao.db1包下,它就会自动使用db1数据源;OrderMapper在db2包下,就自动使用db2数据源。在Service层注入它们即可:
@Service
public class BusinessService {
@Autowired
private UserMapper userMapper; // 来自db1包
@Autowired
private OrderMapper orderMapper; // 来自db2包
public void doBusiness() {
User user = userMapper.selectById(1L); // 操作user_db
Order order = orderMapper.selectById(100L); // 操作order_db
}
}
这里有一个至关重要的坑:事务管理。 如果你在Service方法上加了@Transactional注解,它默认会使用那个标记了@Primary的事务管理器(比如db1TransactionManager)。这意味着,整个方法的所有数据库操作,都会在db1数据源的事务上下文中进行。
那么问题来了,如果这个方法里同时操作了db1和db2,会发生什么?db2的操作将不受这个@Transactional管理,它会是自动提交的。如果后续db1的操作失败回滚,db2的操作已经提交,无法回滚,这就导致了数据不一致。
解决方案:对于需要跨数据源保证一致性的操作,分包方式显得力不从心。这时通常需要引入分布式事务(如Seata),或者从业务设计上避免这种跨库事务。对于大多数读写分离或业务隔离场景,跨库写事务并不常见,所以分包方式依然适用。如果只是查询多个库,则完全没有问题。
分包方案小结:
- 优点:结构清晰,隔离性好,配置直观,易于理解和排查问题。
- 缺点:数据源切换不灵活,无法在运行时动态决定;跨数据源事务支持复杂。
- 适用场景:数据源固定、切换不频繁的场景,如微服务访问多个其他服务的数据库、连接不同功能的数据库(业务库+日志库)。
3. 方案二:注解驱动——轻量灵活,MyBatis-Plus的利器
如果你觉得为每个数据源都写一堆配置类太麻烦,或者你的项目需要更灵活地在方法级别切换数据源(比如根据参数决定查主库还是从库),那么“注解驱动”的方案会更适合你。这里我们结合国内非常流行的MyBatis-Plus和连接池Druid来实现。
这个方案的核心是一个叫@DS的注解。你可以把它加在Service类、Mapper接口或者具体的方法上,来指定该方法使用哪个数据源。框架会在执行数据库操作前,根据这个注解动态切换到对应的数据源。
3.1 快速集成与配置
首先,在pom.xml中引入关键依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version> <!-- 请使用最新稳定版 -->
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
dynamic-datasource-spring-boot-starter这个starter包封装了多数据源的动态路由逻辑,而Druid则是一个强大的数据库连接池。接下来是application.yml配置:
spring:
datasource:
dynamic:
primary: master # 设置默认数据源名为master
strict: false # 是否严格匹配数据源,默认false。未匹配到指定数据源时使用默认数据源
datasource:
master: # 数据源名,对应@DS("master")
url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave: # 数据源名,对应@DS("slave")
url: jdbc:mysql://localhost:3306/slave_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 5
max-active: 20
min-idle: 5
配置看起来比方案一简洁多了,对吧?所有数据源都在dynamic.datasource下面定义,并给每个起个名字(如master, slave)。primary指定了默认使用的数据源名。
这里有个巨坑,我当年排查了半天:如果你同时使用了Druid的自动配置,可能会因为Druid的DataSourceAutoConfiguration去加载默认路径下的数据源配置而导致冲突。所以,我们通常需要在启动类上排除它:
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
3.2 @DS注解的使用技巧与“失效”陷阱
配置好后,使用起来就非常简单了。你不需要再写那些冗长的配置类。
1. 在Service层使用:
@Service
@DS("slave") // 类级别注解:这个Service下所有方法默认使用slave数据源
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
@DS("master") // 方法级别注解:优先级高于类注解,此方法使用master数据源
public boolean save(User user) {
return super.save(user);
}
@Override
// 此方法没有注解,继承类级别的@DS("slave"),使用slave数据源
public User getById(Long id) {
return super.getById(id);
}
}
2. 在Mapper层使用:
@Mapper
@DS("slave") // 该Mapper接口所有方法默认使用slave
public interface OrderMapper extends BaseMapper<Order> {
@DS("master") // 此方法使用master
int insertOrder(Order order);
// 此方法使用slave
Order selectByOrderNo(String orderNo);
}
最佳实践建议:我个人的经验是,将
@DS注解统一放在Service层。因为Service层是业务逻辑的入口,在这里控制数据源切换,逻辑更清晰。Mapper层只负责SQL执行,保持其纯粹性。避免在Service和Mapper上同时使用@DS,以免造成混乱。
3. 最棘手的坑:@DS与@Transactional的冲突
这是使用注解方案时,几乎所有人都会踩的坑。现象是:你在一个加了@Transactional的方法里,调用了另一个有@DS("slave")注解的方法,期望它去查从库,但实际上它仍然跑了主库,导致@DS“失效”。
原因剖析:这不是@DS失效,而是事务机制和动态数据源切换的时机问题。当你在方法上使用@Transactional时,Spring会在方法执行前就开启事务。开启事务的本质,是从数据源连接池中获取一个数据库连接(Connection),并绑定到当前线程。而@DS的动态切换,是在MyBatis执行SQL之前才发生的拦截行为。此时,事务早已开始,线程绑定的连接已经是默认数据源(比如master)的连接了。后续@DS尝试切换,但无法改变这个已经绑定的事务连接,所以SQL依然通过这个连接执行在了主库上。
解决方案:核心思路是让@DS注解的方法在一个新的事务中运行,从而获取新的连接。修改内部方法的@Transactional传播行为:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public void masterOperation() {
// 操作主库...
// 调用一个需要查询从库的方法
slaveOperation();
}
// 解决方案:使用REQUIRES_NEW传播级别
@DS("slave")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void slaveOperation() {
// 这个方法会挂起外部事务,开启一个新事务。
// 开启新事务时,会重新决定数据源,此时@DS("slave")生效,获取到从库连接。
Order order = orderMapper.selectByOrderNo("NO123");
}
}
Propagation.REQUIRES_NEW的意思是:无论当前是否存在事务,都创建一个新的事务。执行到这个方法时,外部事务会被挂起,方法内部在新事务中执行。新事务的开启,会触发重新获取数据库连接,这时@DS注解就能正确拦截并切换到从库数据源了。
注意:
REQUIRES_NEW虽然解决了问题,但它让两个操作不在同一个事务里了。如果slaveOperation()失败回滚,不会影响masterOperation()里已提交的操作;反之,如果masterOperation()在slaveOperation()之后失败回滚,slaveOperation()已经提交的数据也不会回滚。这需要根据你的业务一致性要求来权衡。
注解方案小结:
- 优点:配置简单,使用灵活,可以在方法级别精细控制数据源,与MyBatis-Plus集成度极高。
- 缺点:需要理解事务传播机制以避免
@DS失效问题,对业务代码有一定侵入性(需要加注解)。 - 适用场景:需要灵活切换数据源的场景,如读写分离(读方法加
@DS("slave"),写方法加@DS("master"))、按业务模块分库(不同Service使用不同数据源)。
4. 方案三:动态路由——高度自主,应对复杂场景
前面两种方案,数据源的选择要么是静态的(分包),要么是通过注解在编码时确定的。但如果你的数据源切换逻辑非常动态,比如前面提到的多租户场景,需要根据每次请求携带的租户ID来动态决定连接哪个数据库,或者根据分片键来计算数据源,那么你就需要更强大的武器——继承AbstractRoutingDataSource实现动态路由。
AbstractRoutingDataSource是Spring提供的一个抽象类,它允许你根据一个“查找键”(lookup key)来动态决定返回哪个真实的数据源。这个“查找键”可以来自任何地方:ThreadLocal、请求参数、甚至是从Redis里查出来的。
4.1 核心原理与自定义实现
这个方案需要我们手动编写一个配置类和一个继承AbstractRoutingDataSource的路由类。我们先看路由类,这是大脑:
/**
* 动态数据源路由器
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 使用ThreadLocal来保存当前线程使用的数据源键。
* ThreadLocal是线程隔离的,完美契合Web请求(每个请求一个线程)的场景。
*/
private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>();
/**
* 构造方法,传入默认数据源和所有目标数据源Map
*/
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
// 设置默认数据源(当determineCurrentLookupKey返回null时使用)
super.setDefaultTargetDataSource(defaultTargetDataSource);
// 设置目标数据源Map,key是查找键(如"master","slave","tenant_1"),value是真实的DataSource对象
super.setTargetDataSources(targetDataSources);
// 初始化属性
super.afterPropertiesSet();
}
/**
* 核心方法:决定当前要使用哪个数据源的查找键。
* 这个方法会在每次执行数据库操作前被调用。
*/
@Override
protected Object determineCurrentLookupKey() {
// 直接从ThreadLocal中取出之前设置好的数据源键
String key = getCurrentDataSourceKey();
log.debug("当前路由到的数据源键: {}", key);
return key;
}
/**
* 提供给外部使用的静态方法:设置当前线程的数据源键
*/
public static void setCurrentDataSourceKey(String dataSourceKey) {
DATA_SOURCE_KEY.set(dataSourceKey);
}
/**
* 提供给外部使用的静态方法:获取当前线程的数据源键
*/
public static String getCurrentDataSourceKey() {
return DATA_SOURCE_KEY.get();
}
/**
* 提供给外部使用的静态方法:清空当前线程的数据源键
* 非常重要!必须在请求处理完成后清理,防止内存泄漏和脏数据。
*/
public static void clearCurrentDataSourceKey() {
DATA_SOURCE_KEY.remove();
}
}
这个类理解起来不难:它内部维护了一个ThreadLocal变量来存数据源的名字(key)。当Spring要获取数据库连接时,会调用determineCurrentLookupKey()方法,这个方法就返回ThreadLocal里存的名字,然后AbstractRoutingDataSource就会根据这个名字,去它管理的targetDataSources这个Map里找到对应的真实DataSource返回。
4.2 装配数据源与使用方式
接下来,我们需要在配置类里,把多个真实的数据源(比如主库、从库、租户A库、租户B库)创建好,并交给DynamicDataSource管理。
@Configuration
public class DataSourceConfig {
// 配置主库数据源
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
// 配置从库数据源
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
// 配置动态数据源,并设置为主数据源
@Bean
@Primary // 必须声明为Primary,让Spring容器知道这是主要的数据源
public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
// 创建动态数据源,默认使用master
return new DynamicDataSource(masterDataSource, targetDataSources);
}
// 配置事务管理器,使用动态数据源
@Bean
public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
配置完成后,我们就可以在代码中使用了。最直接的方式是在每个需要切换数据源的地方手动设置:
@RestController
public class TestController {
@Autowired
private UserMapper userMapper; // 这个Mapper现在使用的是dynamicDataSource
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id, @RequestParam String ds) { // ds参数指定数据源
// 1. 在执行数据库操作前,设置数据源键
DynamicDataSource.setCurrentDataSourceKey(ds); // 例如 ds="slave"
try {
// 2. 执行查询,此时会路由到slave数据源
return userMapper.selectById(id);
} finally {
// 3. 非常重要!一定要清理,否则后续操作可能错误地使用这个数据源
DynamicDataSource.clearCurrentDataSourceKey();
}
}
}
这种方式虽然灵活,但代码侵入性强,且容易忘记清理ThreadLocal。更优雅的做法是结合自定义注解和AOP。
4.3 进阶:使用AOP与自定义注解优雅切换
我们可以定义一个@DataSource注解,然后通过切面(AOP)在方法执行前后自动设置和清理数据源键。
1. 定义注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "master"; // 默认数据源
}
2. 实现切面:
@Aspect
@Component
@Order(1) // 确保切面在事务切面之前执行,这点很重要!
public class DataSourceAspect {
@Pointcut("@annotation(com.example.demo.annotation.DataSource) || @within(com.example.demo.annotation.DataSource)")
public void dataSourcePointCut() {}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取方法上的注解
DataSource dataSourceAnnotation = method.getAnnotation(DataSource.class);
// 如果方法上没有,则获取类上的注解
if (dataSourceAnnotation == null) {
dataSourceAnnotation = joinPoint.getTarget().getClass().getAnnotation(DataSource.class);
}
String dataSourceKey = (dataSourceAnnotation != null) ? dataSourceAnnotation.value() : "master";
// 设置数据源
DynamicDataSource.setCurrentDataSourceKey(dataSourceKey);
try {
// 执行原方法
return joinPoint.proceed();
} finally {
// 清理数据源
DynamicDataSource.clearCurrentDataSourceKey();
}
}
}
3. 使用注解:
@Service
public class UserService {
@DataSource("master") // 整个类默认用master
public void updateUser(User user) {
// 操作master库
}
@DataSource("slave") // 这个方法覆盖类注解,使用slave
public User getUser(Long id) {
// 操作slave库
return userMapper.selectById(id);
}
}
这样,数据源的切换就变得非常优雅和声明式了。对于多租户场景,你甚至可以在AOP里从请求头或Session中解析出租户ID,然后动态计算出对应的数据源键(如tenant_123),再设置到ThreadLocal中。
4.4 动态路由方案的事务考量
动态路由方案同样面临和注解方案类似的事务问题。如果@Transactional注解的方法在AOP切面设置数据源之前就开启了事务,那么数据源切换也会失效。解决方案依然是利用事务传播机制。
此外,动态路由方案对事务管理器的配置有更高要求。我们必须确保事务管理器使用的是我们自定义的DynamicDataSource,而不是某个固定的数据源。上面的配置类中已经展示了如何正确配置PlatformTransactionManager。
动态路由方案小结:
- 优点:灵活性最高,可以基于任何逻辑(参数、租户、分片键)动态切换数据源,是应对复杂多数据源场景的终极方案。
- 缺点:实现相对复杂,需要自己管理
ThreadLocal的清理,否则会导致严重的内存泄漏和数据源污染问题。事务管理需要格外小心。 - 适用场景:多租户SaaS应用、按分片键访问不同数据库、根据复杂业务规则动态选择数据源。
5. 方案对比与选型指南:别再纠结了
聊了这么多,估计你已经有点眼花缭乱了。别急,我最后给你整理一个清晰的对比表格,并附上我的选型建议,帮你根据实际项目情况做出最佳选择。
| 特性维度 | 分包配置 | 注解驱动 (@DS) | 动态路由 (AbstractRoutingDataSource) |
|---|---|---|---|
| 核心思想 | 物理隔离,不同数据源对应不同配置、包、Mapper | 声明式切换,通过注解在方法/类上标记数据源 | 编程式路由,运行时根据逻辑动态决定数据源 |
| 配置复杂度 | 高,每个数据源需完整配置类、包扫描、Mapper路径 | 低,yml集中配置,引入starter即可 | 中,需编写路由类和配置类,但结构清晰 |
| 代码侵入性 | 低,Service按需注入不同包的Mapper即可 | 中,需要在Service/Mapper上添加注解 | 高,需手动调用API或结合AOP |
| 灵活性 | 低,数据源在启动时固定,无法运行时动态切换 | 中,可在编码时静态指定,但不够动态 | 高,可基于请求参数、用户信息等动态切换 |
| 事务支持 | 跨数据源事务复杂,需借助分布式事务 | 需注意@DS与@Transactional的传播行为 | 需注意事务开启时机与数据源切换的协调 |
| 可维护性 | 好,结构清晰,排查问题容易 | 较好,注解一目了然 | 中,逻辑分散在AOP和业务代码中,需仔细设计 |
| 典型场景 | 微服务访问多个固定外部库、业务模块物理隔离 | 读写分离、按业务模块逻辑分库 | 多租户、按分片键路由、灰度发布引流 |
我的实战选型建议:
-
如果你是新手,或者项目刚引入第二个数据源,数据源固定不变(比如一个服务连自己的业务库和一个外部的日志库),我强烈推荐分包方式。它虽然配置稍多,但概念最清晰,出了问题也最好排查,不会有意想不到的动态行为。
-
如果你的项目已经用了MyBatis-Plus,并且主要是为了做读写分离,那么注解驱动(@DS) 方案是你的不二之选。它和MP集成得天衣无缝,配置简单,使用方便。你只需要在写操作的方法上加
@DS("master"),在读操作的方法上加@DS("slave")(或者整个读Service类上加),就能轻松实现读写分离。记住处理好@Transactional的传播行为就好。 -
如果你的业务场景非常动态,比如:
- 做一个SaaS平台,每个企业客户的数据存在独立的数据库里。
- 数据做了水平分片,根据用户ID的哈希值决定访问哪个数据库。
- 需要根据请求的某些特征(如渠道、版本)将流量导向不同的数据库。
这种情况下,动态路由方案是唯一的选择。它给了你最大的控制权,但同时也要求你对Spring的数据源和事务机制有更深的理解,并且要非常小心地处理
ThreadLocal的清理,避免内存泄漏。
最后,无论选择哪种方案,请务必在项目初期就做好充分的测试,特别是涉及事务和并发场景的测试。多数据源配置是系统架构中的重要一环,设计得好能提升性能和扩展性,设计不好可能就是灾难的开始。希望我这些年的踩坑经验,能帮你少走些弯路。如果在实践中遇到具体问题,欢迎随时交流。
更多推荐
所有评论(0)