Flyway / Liquibase 数据库迁移 + 多数据源配置
概述
数据库版本管理(Migration-Based Database Versioning)和多数据源(Multiple DataSources)是企业级应用的标配能力。前者保证数据库 Schema 的版本化交付,后者支撑读写分离、分库分表、多租户等场景。
本文目标
- 掌握 Flyway / Liquibase 的集成与对比
- 实现
AbstractRoutingDataSource动态路由 + AOP 切面 - 完成支付系统读写分离 + Flyway 增量脚本管理
- 为分库分表(ShardingSphere)前置抽象
一、Flyway 集成
1.1 Flyway 核心概念
Flyway 通过版本化 SQL 脚本管理数据库变更:
| 概念 | 说明 |
|---|---|
| Migration | 每次 Schema 变更 = 一个版本脚本 |
| Versioned Migration | V1__init.sql、V2__add_user.sql(有版本号,仅执行一次) |
| Repeatable Migration | R__view_order.sql(内容变化时重新执行) |
| Flyway Schema History Table | 默认 flyway_schema_history,记录已执行脚本 |
1.2 Spring Boot 集成
xml
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>yaml
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: 0
validate-on-migrate: true目录结构:
src/main/resources/db/migration/
├── V1__init_schema.sql
├── V1_1__seed_data.sql
├── V2__add_order_table.sql
├── V2_1__add_order_index.sql
├── V3__add_payment_table.sql
└── R__revenue_view.sql1.3 多数据源下的 Flyway
当存在多个 DataSource,每个 DataSource 需要独立的迁移脚本:
java
@Configuration
public class FlywayConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() { return DataSourceBuilder.create().build(); }
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() { return DataSourceBuilder.create().build(); }
@Bean
public FlywayMigrationInitializer masterFlywayInitializer(DataSource masterDataSource) {
return FlywayMigrationInitializer(Flyway.configure()
.dataSource(masterDataSource)
.locations("classpath:db/migration/master")
.load());
}
@Bean
public FlywayMigrationInitializer slaveFlywayInitializer(DataSource slaveDataSource) {
return FlywayMigrationInitializer(Flyway.configure()
.dataSource(slaveDataSource)
.locations("classpath:db/migration/slave")
.load());
}
}二、Liquibase 集成
2.1 Liquibase 核心概念
Liquibase 使用 Changelog 文件(XML / YAML / JSON / SQL)描述变更,支持回滚(rollback)。
xml
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>yaml
spring:
liquibase:
enabled: true
change-log: classpath:db/changelog/db.changelog-master.xmlChangelog 示例:
xml
<!-- db/changelog/db.changelog-master.xml -->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog">
<include file="db/changelog/v1.0.0/001-init-schema.xml" />
<include file="db/changelog/v1.0.0/002-seed-data.xml" />
</databaseChangeLog>xml
<!-- 001-init-schema.xml -->
<changeSet id="1" author="qy">
<createTable tableName="payment_order">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" />
</column>
<column name="order_no" type="VARCHAR(64)">
<constraints unique="true" nullable="false" />
</column>
<column name="amount" type="DECIMAL(10,2)" />
<column name="status" type="TINYINT" defaultValue="0" />
</createTable>
</changeSet>2.2 Flyway vs Liquibase 对比
| 维度 | Flyway | Liquibase |
|---|---|---|
| 变更格式 | SQL 原生 | XML/YAML/JSON + SQL |
| 回滚 | 不支持自动回滚 | 支持 rollback 标签 |
| 学习成本 | 低(就是 SQL) | 中(需学习 Changelog DSL) |
| 多环境 | 基线 + 版本号 | Context + 多文件 |
| 社区生态 | Spring Boot 默认推荐 | 企业级场景更多 |
选型建议:团队 SQL 能力强选 Flyway;需要规范化变更管理、回滚需求多的选 Liquibase。本文以 Flyway 为例。
三、AbstractRoutingDataSource 动态路由
3.1 核心原理
AbstractRoutingDataSource 通过 determineCurrentLookupKey() 决定当前线程使用哪个 DataSource:
java
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}3.2 完整实现
java
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void set(String ds) { CONTEXT.set(ds); }
public static String get() { return CONTEXT.get(); }
public static void clear() { CONTEXT.remove(); }
public static final String MASTER = "master";
public static final String SLAVE = "slave";
}java
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@Primary
public DataSource routingDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceContextHolder.MASTER, masterDataSource());
targetDataSources.put(DataSourceContextHolder.SLAVE, slaveDataSource());
DynamicDataSource routing = new DynamicDataSource();
routing.setDefaultTargetDataSource(masterDataSource());
routing.setTargetDataSources(targetDataSources);
return routing;
}
}3.3 事务路由问题
事务与数据源切换
@Transactional 开启后,整个事务内的所有操作都在同一个 Connection 上执行。如果在事务中切换 DataSource,实际 Connection 不会变。
解决方案: 使用 @Transactional(readOnly = true) 自动路由到从库:
java
@Configuration
public class TransactionRoutingConfig {
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager txm) {
return new TransactionTemplate(txm);
}
}四、AOP 切面实现读写分离
4.1 注解定义
java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default DataSourceContextHolder.MASTER;
}4.2 AOP 切面
java
@Aspect
@Component
@Order(0)
public class DataSourceAspect {
@Around("@annotation(ds)")
public Object around(ProceedingJoinPoint pjp, DataSource ds) throws Throwable {
try {
DataSourceContextHolder.set(ds.value());
return pjp.proceed();
} finally {
DataSourceContextHolder.clear();
}
}
}4.3 利用 @Transactional(readOnly) 自动路由
java
@Service
public class OrderService {
// 写操作 → 主库
@Transactional
public void createOrder(Order order) {
orderDao.insert(order);
paymentDao.insert(order.getPayment());
}
// 读操作 → 自动路由到从库
@Transactional(readOnly = true)
public Order getOrder(Long id) {
return orderDao.selectById(id);
}
// 复杂报表 - 从库
@Transactional(readOnly = true)
public List<OrderReport> queryReport(Date start, Date end) {
return orderDao.selectReport(start, end);
}
}结合 AOP 拦截 @Transactional(readOnly = true):
java
@Around("@annotation(transactional)")
public Object routeByTransactional(ProceedingJoinPoint pjp, Transactional transactional) throws Throwable {
if (transactional.readOnly()) {
DataSourceContextHolder.set(DataSourceContextHolder.SLAVE);
} else {
DataSourceContextHolder.set(DataSourceContextHolder.MASTER);
}
try {
return pjp.proceed();
} finally {
DataSourceContextHolder.clear();
}
}五、实战:支付系统读写分离 + Flyway
5.1 场景描述
支付系统需要:
- 写操作(创建订单、支付回调)→ 主库
- 读操作(查询订单、对账报表)→ 从库
- Schema 变更通过 Flyway 版本化管理
5.2 Flyway 脚本设计
sql
-- V1__init_payment_schema.sql
CREATE TABLE `payment_order` (
`id` BIGINT AUTO_INCREMENT,
`order_no` VARCHAR(64) NOT NULL,
`user_id` BIGINT NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`status` TINYINT DEFAULT 0 COMMENT '0:待支付 1:支付中 2:成功 3:失败',
`channel` VARCHAR(32) COMMENT 'alipay/wechat/unionpay',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `payment_transaction` (
`id` BIGINT AUTO_INCREMENT,
`order_no` VARCHAR(64) NOT NULL,
`transaction_id` VARCHAR(128) COMMENT '三方支付流水号',
`amount` DECIMAL(10,2),
`status` TINYINT,
`notify_raw` JSON COMMENT '回调原始报文',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;sql
-- V2__add_refund_table.sql
CREATE TABLE `payment_refund` (
`id` BIGINT AUTO_INCREMENT,
`order_no` VARCHAR(64) NOT NULL,
`refund_no` VARCHAR(64) NOT NULL,
`amount` DECIMAL(10,2),
`reason` VARCHAR(512),
`status` TINYINT,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_refund_no` (`refund_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;5.3 完整配置
yaml
spring:
datasource:
master:
jdbc-url: jdbc:mysql://master-host:3306/payment?useSSL=false
username: root
password: ${DB_MASTER_PWD}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
slave:
jdbc-url: jdbc:mysql://slave-host:3306/payment?useSSL=false
username: root
password: ${DB_SLAVE_PWD}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 50 # 读多写少,从库连接池更大
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true5.4 服务代码
java
@Service
public class PaymentService {
// 写 - 创建订单 → 主库
@Transactional
public PaymentOrder createOrder(Long userId, BigDecimal amount, String channel) {
PaymentOrder order = new PaymentOrder();
order.setOrderNo(generateOrderNo());
order.setUserId(userId);
order.setAmount(amount);
order.setChannel(channel);
order.setStatus(OrderStatus.PENDING);
orderDao.insert(order);
return order;
}
// 读 - 查询订单 → 从库
@Transactional(readOnly = true)
public PaymentOrder queryOrder(String orderNo) {
return orderDao.selectByOrderNo(orderNo);
}
// 读 - 对账报表 → 从库
@Transactional(readOnly = true)
public List<PaymentReport> generateDailyReport(LocalDate date) {
return reportDao.summaryByDate(date);
}
}六、分库分表前置抽象
6.1 从读写分离到分库分表
读写分离的 AbstractRoutingDataSource 模式天然可扩展为分库分表路由:
java
public class ShardDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 根据分片键计算目标库
String shardKey = ShardContextHolder.getShardKey();
int dbIndex = Math.abs(shardKey.hashCode()) % DB_COUNT;
return "shard_" + dbIndex;
}
}6.2 设计原则
| 原则 | 说明 |
|---|---|
| 事务边界清晰 | 跨库事务需分布式事务(Seata) |
| 分片键必传 | 不传分片键的全表扫描要禁止 |
| 读写分离+分片共存 | 每个分片都有主从,路由需组合 |
七、总结
| 知识点 | 要点 |
|---|---|
| Flyway | SQL 脚本版本化,V 前缀有版本号、R 前缀可重复执行 |
| Liquibase | Changelog DSL 管理变更,支持回滚 |
| 多数据源 | AbstractRoutingDataSource + ThreadLocal 上下文 |
| 读写分离 | AOP 拦截 @Transactional(readOnly) 自动路由 |
| 事务路由注意 | 同一事务内 Connection 不变,事务开启前确定数据源 |
参考链接: