SpringBoot實(shí)現(xiàn)整合多數(shù)據(jù)源的全攻略
在實(shí)際開發(fā)中,單數(shù)據(jù)源往往無法滿足復(fù)雜的業(yè)務(wù)場景 —— 比如讀寫分離、分庫分表、不同業(yè)務(wù)模塊對接不同數(shù)據(jù)庫等。SpringBoot 作為主流的開發(fā)框架,提供了多種多數(shù)據(jù)源整合方案,從簡單的靜態(tài)切換到靈活的動態(tài)路由,每種方案都有其適用場景。本文將從實(shí)際業(yè)務(wù)需求出發(fā),拆解 SpringBoot 中多數(shù)據(jù)源的核心實(shí)現(xiàn)方式,并附上可直接運(yùn)行的代碼示例。
一、多數(shù)據(jù)源核心場景與技術(shù)選型
先明確多數(shù)據(jù)源的常見使用場景,避免盲目選型:
- 靜態(tài)多數(shù)據(jù)源:不同業(yè)務(wù)模塊固定對接不同數(shù)據(jù)庫(如訂單庫、用戶庫),啟動時(shí)加載,運(yùn)行時(shí)不切換;
- 動態(tài)切換數(shù)據(jù)源:運(yùn)行時(shí)根據(jù)條件(如用戶 ID、業(yè)務(wù)標(biāo)識)動態(tài)選擇數(shù)據(jù)源(如讀寫分離、分庫);
- 分布式事務(wù):多數(shù)據(jù)源操作需保證事務(wù)一致性(本文暫不展開,后續(xù)單獨(dú)講解)。
核心依賴(基于 SpringBoot 2.7.x):
<dependencies>
<!-- SpringBoot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 數(shù)據(jù)庫連接 -->
<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>
<!-- 數(shù)據(jù)庫驅(qū)動(以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:靜態(tài)多數(shù)據(jù)源(基于配置類分離)
適用場景
不同業(yè)務(wù)模塊完全隔離,比如「用戶模塊」對接 user_db,「訂單模塊」對接 order_db,運(yùn)行時(shí)無需切換數(shù)據(jù)源。
實(shí)現(xiàn)步驟
配置文件(application.yml)
spring:
datasource:
# 數(shù)據(jù)源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
# 數(shù)據(jù)源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數(shù)據(jù)源配置類
分別配置兩個(gè)數(shù)據(jù)源的 Bean,指定不同的掃描路徑:
用戶數(shù)據(jù)源配置(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;
/**
* 用戶庫數(shù)據(jù)源配置
* 掃描user模塊的mapper
*/
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.user", sqlSessionTemplateRef = "userSqlSessionTemplate")
public class UserDataSourceConfig {
/**
* 配置用戶庫數(shù)據(jù)源
*/
@Bean(name = "userDataSource")
@ConfigurationProperties(prefix = "spring.datasource.user")
@Primary // 主數(shù)據(jù)源(必須指定一個(gè)主數(shù)據(jù)源)
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);
}
}訂單數(shù)據(jù)源配置(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;
/**
* 訂單庫數(shù)據(jù)源配置
* 掃描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);
}
}
業(yè)務(wù)代碼示例
用戶 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> {
}
業(yè)務(wù)層調(diào)用:
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) {
// 自動使用用戶庫數(shù)據(jù)源
return userMapper.selectById(id);
}
@Override
public Order getOrderById(Long id) {
// 自動使用訂單庫數(shù)據(jù)源
return orderMapper.selectById(id);
}
}
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):配置簡單、無侵入性、性能高,適合模塊隔離的場景;
缺點(diǎn):無法動態(tài)切換,新增數(shù)據(jù)源需新增配置類,靈活性低。
三、方案 2:動態(tài)切換數(shù)據(jù)源(基于 AbstractRoutingDataSource)
適用場景
需要根據(jù)業(yè)務(wù)邏輯動態(tài)切換數(shù)據(jù)源,比如「讀寫分離」(讀庫 / 寫庫切換)、「分庫」(按用戶 ID 路由到不同庫)。
核心思路:繼承 Spring 提供的AbstractRoutingDataSource,重寫determineCurrentLookupKey方法,通過 ThreadLocal 存儲當(dāng)前線程的數(shù)據(jù)源標(biāo)識,實(shí)現(xiàn)動態(tài)路由。
實(shí)現(xiàn)步驟
配置文件(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核心工具類
數(shù)據(jù)源上下文 Holder(DynamicDataSourceContextHolder.java):
package com.example.demo.config.dynamic;
/**
* 數(shù)據(jù)源上下文 Holder,基于ThreadLocal存儲當(dāng)前線程的數(shù)據(jù)源標(biāo)識
*/
public class DynamicDataSourceContextHolder {
/**
* 線程本地變量:存儲當(dāng)前線程使用的數(shù)據(jù)源標(biāo)識
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 設(shè)置數(shù)據(jù)源標(biāo)識
*/
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
/**
* 獲取數(shù)據(jù)源標(biāo)識
*/
public static String getDataSourceKey() {
return CONTEXT_HOLDER.get();
}
/**
* 清除數(shù)據(jù)源標(biāo)識
*/
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
}
動態(tài)數(shù)據(jù)源路由類(DynamicRoutingDataSource.java):
package com.example.demo.config.dynamic;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 動態(tài)數(shù)據(jù)源路由:重寫determineCurrentLookupKey方法,返回當(dāng)前線程的數(shù)據(jù)源標(biāo)識
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 從ThreadLocal中獲取當(dāng)前線程的數(shù)據(jù)源標(biāo)識
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
數(shù)據(jù)源配置類
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;
/**
* 動態(tài)數(shù)據(jù)源配置
*/
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper", sqlSessionFactoryRef = "dynamicSqlSessionFactory")
public class DynamicDataSourceConfig {
/**
* 配置主庫數(shù)據(jù)源
*/
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();
}
/**
* 配置從庫數(shù)據(jù)源
*/
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();
}
/**
* 配置動態(tài)數(shù)據(jù)源(核心)
*/
@Bean(name = "dynamicDataSource")
@Primary
public DataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
// 1. 配置默認(rèn)數(shù)據(jù)源(主庫)
dynamicRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
// 2. 配置所有數(shù)據(jù)源(key為數(shù)據(jù)源標(biāo)識,value為數(shù)據(jù)源)
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 實(shí)現(xiàn)自動切換
數(shù)據(jù)源注解(DataSource.java):
package com.example.demo.annotation;
import java.lang.annotation.*;
/**
* 自定義數(shù)據(jù)源注解:標(biāo)注在方法/類上,指定使用的數(shù)據(jù)源
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 數(shù)據(jù)源標(biāo)識,對應(yīng)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;
/**
* 數(shù)據(jù)源切換切面:攔截@DataSource注解,設(shè)置對應(yīng)的數(shù)據(jù)源標(biāo)識
*/
@Aspect
@Component
@Order(-1) // 保證切面優(yōu)先級高于事務(wù)
public class DataSourceAspect {
/**
* 切入點(diǎn):攔截所有標(biāo)注@DataSource的方法/類
*/
@Pointcut("@annotation(com.example.demo.annotation.DataSource) || @within(com.example.demo.annotation.DataSource)")
public void dataSourcePointCut() {}
/**
* 環(huán)繞通知:切換數(shù)據(jù)源
*/
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 1. 獲取注解中的數(shù)據(jù)源標(biāo)識
String dataSourceKey = getDataSourceKey(point);
// 2. 設(shè)置數(shù)據(jù)源標(biāo)識到ThreadLocal
DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey);
try {
// 3. 執(zhí)行目標(biāo)方法
return point.proceed();
} finally {
// 4. 清除數(shù)據(jù)源標(biāo)識,避免線程復(fù)用導(dǎo)致的問題
DynamicDataSourceContextHolder.clearDataSourceKey();
}
}
/**
* 獲取方法/類上的數(shù)據(jù)源標(biāo)識
*/
private String getDataSourceKey(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 優(yōu)先獲取方法上的注解
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();
}
// 默認(rèn)使用主庫
return "master";
}
}
業(yè)務(wù)代碼示例
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);
}
}
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):靈活性高,支持運(yùn)行時(shí)動態(tài)切換,可擴(kuò)展至 N 個(gè)數(shù)據(jù)源;
缺點(diǎn):需要手動管理 ThreadLocal,切面優(yōu)先級需高于事務(wù),否則切換失效。
四、方案 3:基于 MyBatis-Plus 插件(dynamic-datasource-spring-boot-starter)
適用場景
追求極簡配置,快速實(shí)現(xiàn)多數(shù)據(jù)源切換(推薦生產(chǎn)環(huán)境使用)。
dynamic-datasource-spring-boot-starter 是 MyBatis-Plus 團(tuán)隊(duì)提供的多數(shù)據(jù)源插件,封裝了 AbstractRoutingDataSource 的底層邏輯,支持注解、spel 表達(dá)式、分布式場景等,無需手動編寫切面和路由類。
實(shí)現(xiàn)步驟
引入依賴
<!-- 動態(tài)數(shù)據(jù)源插件(核心) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>配置文件(application.yml)
spring:
# 動態(tài)數(shù)據(jù)源配置
dynamic:
datasource:
# 主庫(默認(rèn))
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
# 配置默認(rèn)數(shù)據(jù)源
primary: master
# 配置Druid連接池
type: com.alibaba.druid.pool.DruidDataSource業(yè)務(wù)代碼示例
直接使用插件提供的@DS注解切換數(shù)據(jù)源:
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;
/**
* 默認(rèn)使用主庫(可不加注解)
*/
@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));
}
}
進(jìn)階用法:spel 表達(dá)式動態(tài)路由
支持根據(jù)方法參數(shù)動態(tài)選擇數(shù)據(jù)源(比如按用戶 ID 取模分庫):
/**
* 根據(jù)用戶ID取模,路由到不同從庫
*/
@Override
@DS("#{id % 2 == 0 ? 'slave1' : 'slave2'}")
public User getUserById(Long id) {
return userMapper.selectById(id);
}
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):零配置成本、功能豐富(支持讀寫分離、負(fù)載均衡、分布式鎖)、官方維護(hù);
缺點(diǎn):依賴第三方插件,深度定制化場景需二次開發(fā)。
五、方案對比與選型建議
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場景 |
|---|---|---|---|
| 靜態(tài)多數(shù)據(jù)源(配置類) | 配置簡單、性能高、無侵入 | 無法動態(tài)切換、擴(kuò)展性差 | 模塊隔離的固定多數(shù)據(jù)源場景 |
| 自定義動態(tài)數(shù)據(jù)源 | 高度自定義、靈活性高 | 需手動編寫代碼、易出問題 | 特殊定制化的動態(tài)切換場景 |
| dynamic-datasource | 極簡配置、功能豐富、穩(wěn)定性高 | 依賴第三方插件 | 大部分生產(chǎn)環(huán)境(推薦) |
六、注意事項(xiàng)
- 事務(wù)問題:動態(tài)數(shù)據(jù)源切換需保證切面優(yōu)先級高于事務(wù)(@Order (-1)),否則事務(wù)內(nèi)切換數(shù)據(jù)源失效;
- 線程安全:ThreadLocal 需在方法執(zhí)行完畢后清除,避免線程池復(fù)用導(dǎo)致數(shù)據(jù)源串用;
- 連接池配置:多數(shù)據(jù)源場景下需合理配置連接池大小,避免連接耗盡;
- 讀寫分離:從庫建議設(shè)置為只讀,避免寫入數(shù)據(jù)導(dǎo)致主從同步異常。
總結(jié)
SpringBoot 整合多數(shù)據(jù)源的核心思路是「數(shù)據(jù)源路由」,不同方案只是封裝程度不同。對于大部分開發(fā)者來說,優(yōu)先選擇 dynamic-datasource 插件,兼顧效率和穩(wěn)定性;特殊定制化場景可基于 AbstractRoutingDataSource 手動實(shí)現(xiàn);模塊隔離場景則用靜態(tài)多數(shù)據(jù)源即可。
本文所有代碼均可直接復(fù)制運(yùn)行,建議根據(jù)實(shí)際業(yè)務(wù)場景調(diào)整數(shù)據(jù)源配置和切換邏輯。
以上就是SpringBoot實(shí)現(xiàn)整合多數(shù)據(jù)源的全攻略的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot整合多數(shù)據(jù)源的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java中Cookie和Session詳解及區(qū)別總結(jié)
這篇文章主要介紹了Java中Cookie和Session詳解,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,感興趣的小伙伴可以參考一下2022-06-06
Java中實(shí)現(xiàn)文件上傳下載的三種解決方案(推薦)
這篇文章主要介紹了Java中實(shí)現(xiàn)文件上傳下載的三種解決方案的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07
Spring?Cloud?Eureka基礎(chǔ)應(yīng)用及原理
這篇文章主要介紹了Spring?Cloud?Eureka基礎(chǔ)應(yīng)用,Eureka?Client中內(nèi)置一個(gè)負(fù)載均衡器,用來進(jìn)行基本的負(fù)載均衡,下面我們將通過搭建一個(gè)簡單的Eureka例子來了解Eureka的運(yùn)作原理,感興趣的朋友一起看看吧2022-05-05
解決redisTemplate中l(wèi)eftPushAll隱性bug的問題
這篇文章主要介紹了解決redisTemplate中l(wèi)eftPushAll隱性bug的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02
Java?實(shí)戰(zhàn)項(xiàng)目之家政服務(wù)平臺系統(tǒng)的實(shí)現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實(shí)現(xiàn)家政服務(wù)平臺系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2021-11-11

