多数据源
在实际项目开发中,单一数据源往往无法满足复杂业务需求。例如,读写分离、跨库查询、新旧系统数据迁移等场景都需要多数据源支持。本章将介绍如何在 MyBatis Plus 中配置和使用多数据源。
为什么需要多数据源?
多数据源的使用场景主要包括:
| 场景 | 说明 | 数据源特点 |
|---|---|---|
| 读写分离 | 写操作走主库,读操作走从库 | 一主多从 |
| 业务分库 | 不同业务模块使用独立数据库 | 多个主库 |
| 数据迁移 | 新旧系统数据同步 | 新旧两个库 |
| 跨系统查询 | 查询其他系统的数据 | 主业务库 + 第三方库 |
dynamic-datasource 简介
dynamic-datasource-spring-boot-starter 是苞米豆生态下的开源项目,专门为 Spring Boot 提供多数据源支持。它具有以下特性:
- 支持数据源分组,适用于读写分离、一主多从等场景
- 支持敏感信息加密,保护数据库密码安全
- 支持独立初始化,每个数据库可以独立初始化表结构
- 支持动态增减数据源,运行时可以添加或移除数据源
- 支持分布式事务,基于 Seata 实现
- 简化集成 Druid、HikariCP 等连接池
快速开始
添加依赖
Spring Boot 2.x:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
Spring Boot 3.x:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>4.3.0</version>
</dependency>
配置数据源
spring:
datasource:
dynamic:
# 设置默认数据源
primary: master
# 严格模式,未匹配到数据源时是否报错
strict: false
# 数据源配置
datasource:
# 主库
master:
url: jdbc:mysql://localhost:3306/main_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 从库
slave_1:
url: jdbc:mysql://localhost:3307/main_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 订单库
order:
url: jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
使用 @DS 注解切换数据源
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 默认使用 master 数据源
*/
public List<Map<String, Object>> listFromMaster() {
return jdbcTemplate.queryForList("SELECT * FROM user");
}
/**
* 使用 slave_1 数据源
*/
@DS("slave_1")
public List<Map<String, Object>> listFromSlave() {
return jdbcTemplate.queryForList("SELECT * FROM user");
}
/**
* 使用 order 数据源
*/
@DS("order")
public List<Map<String, Object>> listFromOrder() {
return jdbcTemplate.queryForList("SELECT * FROM user");
}
}
详细配置
完整配置项
spring:
datasource:
dynamic:
# 默认数据源名称
primary: master
# 严格模式:true 表示未匹配到数据源时抛异常,false 表示使用默认数据源
strict: false
# 是否启用严格模式匹配数据源(对方法名的匹配)
strict-match: false
# 懒加载数据源(首次使用时才初始化)
lazy: false
# 全局通用配置
aop:
# 是否启用 @DS 注解 AOP 切面
enabled: true
# 允许嵌套切换数据源
allowed-public-only: true
# 数据源连接池配置(全局配置,可被具体数据源覆盖)
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 具体数据源配置
datasource:
master:
url: jdbc:mysql://localhost:3306/main_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 该数据源独有的连接池配置
druid:
initial-size: 10
max-active: 50
slave_1:
url: jdbc:mysql://localhost:3307/main_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
数据源分组
使用下划线 _ 命名可以创建数据源分组。分组适用于读写分离场景:
spring:
datasource:
dynamic:
primary: master
datasource:
# 主库组
master:
url: jdbc:mysql://master-host:3306/db
# 从库组(slave_ 开头自动归为 slave 组)
slave_1:
url: jdbc:mysql://slave1-host:3306/db
slave_2:
url: jdbc:mysql://slave2-host:3306/db
切换到从库组时,会自动负载均衡到 slave_1 或 slave_2:
@Service
@DS("slave") // 使用从库组,自动负载均衡
public class UserQueryService {
// 所有方法默认使用从库
}
注解使用详解
@DS 注解位置
@DS 注解可以放在类或方法上,方法上的注解优先级更高:
@Service
@DS("order") // 类级别:默认使用 order 数据源
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 使用类级别的 order 数据源
*/
public Order getById(Long id) {
return orderMapper.selectById(id);
}
/**
* 方法级别:覆盖类级别,使用 master 数据源
*/
@DS("master")
public int createOrder(Order order) {
return orderMapper.insert(order);
}
/**
* 使用从库组读取
*/
@DS("slave")
public List<Order> listOrders() {
return orderMapper.selectList(null);
}
}
在 Mapper 接口上使用
@Mapper
@DS("order") // 整个 Mapper 使用 order 数据源
public interface OrderMapper extends BaseMapper<Order> {
@DS("master") // 特定方法使用 master 数据源
int insert(Order order);
@DS("slave") // 查询方法使用 slave 数据源
List<Order> selectByUserId(Long userId);
}
在事务中使用
在事务中切换数据源需要注意事务传播行为:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 事务中使用多数据源
* 注意:不同数据源的事务是独立的,不能回滚
*/
@Transactional
@DS("master")
public void createOrder(Order order) {
orderMapper.insert(order);
// 这里如果调用其他数据源的方法,它们的事务是独立的
}
/**
* 嵌套数据源切换
*/
@DS("master")
public void complexOperation() {
// 主库操作
orderMapper.insert(new Order());
// 切换到从库读取(新事务)
readFromSlave();
}
@DS("slave")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void readFromSlave() {
// 独立事务,使用从库
orderMapper.selectList(null);
}
}
编程式切换数据源
除了注解方式,还可以通过代码动态切换数据源:
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.springframework.stereotype.Service;
@Service
public class DynamicDataSourceService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 手动切换数据源
*/
public void manualSwitch() {
try {
// 切换到 order 数据源
DynamicDataSourceContextHolder.push("order");
jdbcTemplate.execute("SELECT * FROM orders");
// 切换到 master 数据源
DynamicDataSourceContextHolder.push("master");
jdbcTemplate.execute("SELECT * FROM users");
} finally {
// 清除当前线程的数据源设置
DynamicDataSourceContextHolder.poll();
}
}
/**
* 强制清除数据源设置
*/
public void forceClear() {
DynamicDataSourceContextHolder.clear();
}
}
动态添加和删除数据源
在运行时可以动态添加或删除数据源:
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.druid.DruidDataSourceCreator;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.sql.DataSource;
import java.util.Set;
@RestController
@RequestMapping("/datasource")
public class DataSourceController {
@Autowired
private DataSource dataSource;
@Autowired
private DruidDataSourceCreator druidDataSourceCreator;
/**
* 获取所有数据源名称
*/
@GetMapping("/list")
public Set<String> list() {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
return ds.getDataSources().keySet();
}
/**
* 动态添加数据源
*/
@PostMapping("/add")
public String add(@RequestParam String name,
@RequestParam String url,
@RequestParam String username,
@RequestParam String password) {
DataSourceProperty property = new DataSourceProperty();
property.setUrl(url);
property.setUsername(username);
property.setPassword(password);
property.setDriverClassName("com.mysql.cj.jdbc.Driver");
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
ds.addDataSource(name, druidDataSourceCreator.createDataSource(property));
return "添加成功";
}
/**
* 动态删除数据源
*/
@DeleteMapping("/remove")
public String remove(@RequestParam String name) {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
ds.removeDataSource(name);
return "删除成功";
}
}
敏感信息加密
数据库密码等敏感信息可以使用加密存储:
生成加密密码
import com.baomidou.dynamic.datasource.toolkit.CryptoUtils;
public class PasswordEncryptor {
public static void main(String[] args) {
String password = "123456";
String encrypted = CryptoUtils.encrypt(password);
System.out.println("加密后: " + encrypted);
// 解密验证
String decrypted = CryptoUtils.decrypt(encrypted);
System.out.println("解密后: " + decrypted);
}
}
配置加密密码
spring:
datasource:
dynamic:
datasource:
master:
url: jdbc:mysql://localhost:3306/main_db
username: root
# 使用 ENC() 包裹加密后的密码
password: ENC(加密后的密码字符串)
读写分离完整示例
下面是一个完整的读写分离配置示例:
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
# 主库(写操作)
master:
url: jdbc:mysql://master.db.example.com:3306/app_db
username: app_user
password: ENC(xxxxx)
driver-class-name: com.mysql.cj.jdbc.Driver
# 从库组(读操作)
slave_1:
url: jdbc:mysql://slave1.db.example.com:3306/app_db
username: app_user
password: ENC(xxxxx)
driver-class-name: com.mysql.cj.jdbc.Driver
slave_2:
url: jdbc:mysql://slave2.db.example.com:3306/app_db
username: app_user
password: ENC(xxxxx)
driver-class-name: com.mysql.cj.jdbc.Driver
// 基础 Mapper 接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
// Service 实现
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
// 写操作使用主库
@DS("master")
@Transactional
@Override
public boolean save(User user) {
return userMapper.insert(user) > 0;
}
@DS("master")
@Transactional
@Override
public boolean update(User user) {
return userMapper.updateById(user) > 0;
}
@DS("master")
@Transactional
@Override
public boolean removeById(Long id) {
return userMapper.deleteById(id) > 0;
}
// 读操作使用从库组(自动负载均衡)
@DS("slave")
@Override
public User getById(Long id) {
return userMapper.selectById(id);
}
@DS("slave")
@Override
public List<User> list() {
return userMapper.selectList(null);
}
@DS("slave")
@Override
public IPage<User> page(int pageNum, int pageSize) {
return userMapper.selectPage(new Page<>(pageNum, pageSize), null);
}
}
分布式事务
当涉及多个数据源的事务时,需要使用分布式事务方案。dynamic-datasource 支持集成 Seata:
添加依赖
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
配置 Seata
seata:
enabled: true
application-id: app-server
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
registry:
type: nacos
nacos:
server-addr: localhost:8848
namespace: ""
group: SEATA_GROUP
使用分布式事务
import io.seata.spring.annotation.GlobalTransactional;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private ProductMapper productMapper;
@Autowired
private AccountMapper accountMapper;
/**
* 分布式事务:跨多个数据源的事务
*/
@GlobalTransactional(rollbackFor = Exception.class)
public void placeOrder(OrderDTO orderDTO) {
// 操作订单库
@DS("order")
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
orderMapper.insert(order);
// 操作商品库
@DS("product")
Product product = productMapper.selectById(orderDTO.getProductId());
product.setStock(product.getStock() - orderDTO.getQuantity());
productMapper.updateById(product);
// 操作账户库
@DS("account")
Account account = accountMapper.selectByUserId(orderDTO.getUserId());
account.setBalance(account.getBalance().subtract(orderDTO.getAmount()));
accountMapper.updateById(account);
}
}
常见问题
问题 1:数据源切换不生效
原因:可能是 AOP 切面未生效或方法调用方式问题。
解决:
- 确保方法是 public 的
- 不要在类内部直接调用(绕过了代理)
- 检查
spring.datasource.dynamic.aop.enabled是否为 true
问题 2:事务中切换数据源失败
原因:Spring 事务在方法开始时就确定了数据源连接。
解决:使用独立事务或编程式事务管理。
问题 3:连接池配置不生效
原因:全局配置和局部配置的优先级问题。
解决:检查配置层级,具体数据源的配置会覆盖全局配置。
小结
本章我们学习了:
- 多数据源场景:读写分离、业务分库等需求
- dynamic-datasource:苞米豆生态的多数据源启动器
- 配置方式:YAML 配置多数据源和连接池参数
- @DS 注解:类级别和方法级别的数据源切换
- 编程式切换:代码动态切换数据源
- 动态管理:运行时添加和删除数据源
- 读写分离:完整的配置和代码示例
- 分布式事务:集成 Seata 实现跨数据源事务