多数据源、读写分离、分片与 Schema 迁移:选库和改表
应用连接第二个数据库以后,每次访问都要知道目标:订单属于哪个库,事务已经拿到了哪条连接,刚写入的数据应到哪里读取。发布新版本时,还要让新旧程序能够共同使用正在变化的表结构。
多数据源配置、数据库复制、数据分片和 Schema 迁移解决不同问题。可以把它们放进同一套应用,但要分别观察连接目标、复制进度、数据归属和迁移版本。
两个数据库怎样进入一个应用
先区分固定资源和动态路由
| 方式 | 应用中的对象 | 适用情况 |
|---|---|---|
| 多个固定 DataSource | 每个库一个具名资源,通过 Qualifier 或明确参数选择 | 业务职责明确,库数量有限 |
| 多套持久化工厂 | 每个 SqlSessionFactory/EntityManagerFactory 对应自己的 DataSource 和映射 | 模型、方言或事务资源差异明显 |
| 一个路由 DataSource | getConnection 时按路由键选择目标池 | 相同访问模型需要按租户或任务选库 |
多个 DataSource 往往意味着多个连接池。每个池的最大连接数相加,才是一个应用实例可能占用的连接规模;实例数量增加后,还要再乘部署副本数。
一个本地事务管理器通常管理它拿到的那条数据库连接。代码先向 A 写入再向 B 写入,两个操作不会仅因出现在同一个 Java 方法里就获得跨库原子性。跨资源提交需要专门的协调方式,或者按照业务设计消息、补偿和重试。
路由选择发生在取得物理连接时
Spring AbstractRoutingDataSource 根据当前 lookup key 找到目标 DataSource,再获取连接。默认 lenientFallback=true,有默认资源时,未知键可以回退;设为 false 后,未知非空键会失败,但空键仍可能使用默认资源。AbstractRoutingDataSource
租户路由通常应在缺少租户时拒绝执行,而非悄悄落到某个库。本工程同时采用不配置默认目标、关闭宽松回退、缺路由键直接抛错三项设置。
路由要在实际物理借用前建立。普通 JdbcTransactionManager 常在事务开始时取得连接;LazyConnectionDataSourceProxy 则可以把实际借用推迟到创建 Statement 等动作。使用懒连接后,时点可能向后移动,但拿到物理连接后的事务依然使用它。LazyConnectionDataSourceProxy
建立路由、迁移与真实复制实验
环境结构与身份
下载完整实验 ZIP。工程采用 Boot 4.1.1 的依赖清单,使用 Spring JDBC/Context、HikariCP 7.0.2、Flyway 12.4.0 及 PostgreSQL 数据库模块、pgJDBC 42.7.13;默认 Maven 3.9.12 / JDK 25,源码目标 Java 17。Boot 依赖版本
应用实验容器
├─ Routing → pool A → db / route_a
├─ Routing → pool B → db / route_b
└─ 复制探针 ─────────→ replica / route_a
db:一个主库集群,包含 route_a、route_b 两个独立 database
└─ WAL 物理复制 → replica:该集群的只读副本route_a 与 route_b 用于验证选库和租户归属;它们不是互相复制的主从库。真正的复制来自 db 容器到 replica 容器,复制的是整个 PostgreSQL 集群。
compose.yaml 不映射宿主端口,两个 PostgreSQL 进程均以 999:999 运行,数据放在 tmpfs。Java 容器使用宿主普通用户身份。数据库角色分工如下:
| 角色 | 用途 | 实验权限 |
|---|---|---|
| lab_owner | 初始化集群、控制实验副本重放 | 管理员,仅用于这套一次性环境 |
| lab_migrator | 执行 Flyway | 创建自己管理的表,维护迁移历史 |
| lab_app | 应用查询和写入 | tenant_order 表的 DML,无 DDL 和历史表写权限 |
| lab_replicator | pg_basebackup 与 WAL 接收 | LOGIN、REPLICATION |
每次模式可能清空并恢复实验表,只能在新建的独立环境运行。
启动主库与副本
Linux Bash 需要 Docker Engine、Compose、unzip、openssl。普通用户执行:
test "$(id -u)" -ne 0 || exit 1
docker version
docker compose version
unzip multi-datasource-sharding-migration-lab.zip
cd multi-datasource-sharding-migration
export LAB_DIR="$PWD"
export LAB_CACHE="$LAB_DIR/.m2-cache"
mkdir -p "$LAB_CACHE"
export LAB_DB_PASSWORD="$(openssl rand -hex 24)"
export LAB_APP_PASSWORD="$(openssl rand -hex 24)"
export LAB_MIGRATION_PASSWORD="$(openssl rand -hex 24)"
export LAB_REPLICATION_PASSWORD="$(openssl rand -hex 24)"
export LAB_DB_A_URL='jdbc:postgresql://db:5432/route_a'
export LAB_DB_B_URL='jdbc:postgresql://db:5432/route_b'
export LAB_REPLICA_URL='jdbc:postgresql://replica:5432/route_a'
docker compose -p da10-multi --profile replica up -d --wait主库初始化脚本创建两个 database、角色和权限。副本在自己的空目录执行:
pg_basebackup -h db -U lab_replicator -D "$PGDATA" -R -X stream -c fast -w这里 PGDATA 和复制密码由 Compose 注入副本容器;不是要求在宿主机运行该命令。-R 写入 standby 配置,-X stream 在备份期间传输所需 WAL。完整动作在 replica.sh,数据库工具参数见 pg_basebackup。
副本必须从空目录开始。实验没有构建自动故障转移、长期 WAL 归档或生产级复制槽保留策略;长期服务应另外设计备份、WAL 保留和故障恢复。
验证身份:
docker compose -p da10-multi exec -T db \
psql -X -U lab_owner -d route_a -v ON_ERROR_STOP=1 \
-c "SELECT current_database(), pg_is_in_recovery();"
docker compose -p da10-multi exec -T replica \
psql -X -U lab_owner -d route_a -v ON_ERROR_STOP=1 \
-c "SELECT current_database(), pg_is_in_recovery();"主库应为 route_a / f,副本为 route_a / t。如果 replica 未就绪,先检查日志中的认证、HBA 或 base backup 错误:
docker compose -p da10-multi --profile replica logs --tail=100 db replica这些随机密码会进入实验容器环境,适合隔离实验;生产凭据应通过符合项目要求的秘密管理机制注入,不把完整容器环境或连接串转储到公开日志。
运行第一次选库写入
定义调用函数:
run_lab() {
docker run --rm --user "$(id -u):$(id -g)" --read-only \
--network da10-multi_default --tmpfs /tmp:rw,exec,mode=1777 \
-v "$LAB_DIR:/src:ro" -v "$LAB_CACHE:/cache" \
-e LAB_DB_A_URL -e LAB_DB_B_URL -e LAB_REPLICA_URL \
-e LAB_DB_PASSWORD -e LAB_APP_PASSWORD -e LAB_MIGRATION_PASSWORD \
-e MAVEN_CONFIG=/tmp/maven \
maven:3.9.12-eclipse-temurin-25 bash /src/run.sh "$1"
}
run_lab first运行脚本在 /tmp 编译,源码目录只读。程序首先用迁移角色对 A、B 分别执行 V1/V2 并 validate,再用应用角色运行指定模式。第一次会看到迁移创建表,后续运行则提示结构版本已到 v2。
关键输出:
routeA=route_a routeB=route_b writtenA=1 rowsB=0A 事务写入一行后提交,独立连接检查 A 为一行、B 为零行。模式结束前会清理该行。
核心文件可直接查看 Routing.java、RouteScope.java、MultiLab.java 和 Migrations.java。
事务绑定之后,改键为什么没有换库
路由上下文需要成对恢复
RouteScope 保存进入前的键,并在 close 中恢复:
try (var route = RouteScope.use("A")) {
// 当前工作选择 A。
try (var inner = RouteScope.use("B")) {
// 无已绑定事务连接时,此处可以选择 B。
}
// 恢复为 A。
}内层结束时恢复上层值,而不是一律 remove。最外层原本无值时才清除 ThreadLocal。这样既支持嵌套调用,也能在异常退出时恢复原来的上下文。
ThreadLocal 不应被当作请求参数的永久存储。线程池会复用线程,忘记清理可能把上一次租户带给下一次任务;InheritableThreadLocal 也无法自动解决线程池提交时的上下文传递。
运行:
run_lab scopesnestedRouteRestored=true exceptionRestoredRoute=true missingRouteRejected=true unknownRouteRejected=true asyncExplicitRoute=true workerContextCleared=true实验验证普通嵌套与异常退出都恢复 A;缺键和未知键都拒绝;异步线程不会继承当前键,显式传入 B 后可以查询 B,任务结束后再次使用该线程时无残留上下文。
Spring 先查已经绑定的 Connection
JdbcTemplate 和正确使用 DataSourceUtils 的代码会参与 Spring 的连接绑定。以 DataSource 为资源查找入口时,当前事务已有 ConnectionHolder,就复用其中连接。Spring JDBC 连接管理
路由 A → 开始事务 → 借用 A 的物理连接 → 绑定到 routingDataSource
│
ThreadLocal 临时改成 B
│
DataSourceUtils 找到已有连接
│
继续使用 A运行:
run_lab boundtransactionBoundToA=true changedRouteStillA=true nextWorkSelectedB=true实验在 A 事务中查询 current_database,改成 B 后再查询仍是 route_a;事务结束后,新工作选择 B,才查到 route_b。
代码必须通过参与事务的路径取连接:
Connection connection = DataSourceUtils.getConnection(routing);
try {
// 使用连接执行当前事务的 SQL。
} finally {
DataSourceUtils.releaseConnection(connection, routing);
}直接调用路由 DataSource 的 getConnection 可能新借出另一条连接,绕开当前事务绑定。它即使成功去了 B,也不能说明原来 A 的事务已经“动态切换”过去。
同样,两个不同 DataSource 对象即使指向相同 URL,也可能成为两个不同的资源入口。不要只比较连接串就认定它们共享本地事务。
多个工厂和事务管理器要配套
两个数据库使用不同模型或不同方言时,可以分别建立 SqlSessionFactory/EntityManagerFactory 和事务管理器,由业务入口明确选择。使用注解事务时,还要确认选择了对应的 transactionManager。
一个路由资源下面放两个数据库,适合“这次工作只去其中一个”的情况。它没有让同一事务的多条 SQL 按键分散到不同库并整体提交。对跨库业务,先确定需要强原子提交还是允许分阶段完成,再选择相应实现。
写后读怎样处理复制延迟
真实副本接收和重放 WAL
PostgreSQL 物理流复制把主库 WAL 传给 standby,副本重放后才能提供相应数据。接收到 WAL、持久化 WAL 和重放 WAL 是不同进度。PostgreSQL standby 与流复制
默认异步复制下,主库提交可以先于副本重放。刚创建订单就把查询路由到副本,可能暂时查不到;固定等待几十毫秒无法证明目标写入已经可见。
对强写后读需求,常用方法是将相关读取固定到主库;也可以在合适架构中等待副本重放到这次写入之后的 LSN,再用新的合适读取快照查询。LSN 比较必须对应正确集群、复制关系和时间线,不能把任意两台数据库的数值直接比较。
暂停重放,观察旧数据,再恢复
运行:
run_lab replicationrealStandby=true pausedReplicaOld=true primaryCommitted=true replayLsnCaughtUp=true replicaReadNew=trueReplication.java 执行真实时序:
先等待副本追上已有 WAL
→ 管理员请求暂停副本重放,确认状态 paused
→ 主库普通应用连接写入 id=900 并提交
→ 主库查询为 1 行,副本查询为 0 行
→ 恢复重放,等待 replay_lsn 达到目标 LSN
→ 副本的新查询读到 1 行
→ finally 恢复重放并清理实验行判断追上使用数据库的 pg_lsn 比较,而不是对 LSN 字符串排序:
SELECT COALESCE(
pg_last_wal_replay_lsn() >= CAST(? AS pg_lsn),
false
);问号由 PreparedStatement 绑定主库写入后的目标位置。主库和副本的读连接使用自动提交,后续查询可以取得新的 Read Committed 快照。长时间保持的旧事务快照还需要单独处理。
只有控制暂停/恢复的连接使用 lab_owner;读写数据仍由 lab_app 执行。函数权限和重放状态定义见 PostgreSQL 管理函数。
暂停重放可能让 WAL 持续积压,因此只在这套短期实验环境执行。若 Java 容器被强制终止,finally 可能来不及运行。恢复命令为:
docker compose -p da10-multi exec -T replica \
psql -X -U lab_owner -d route_a -v ON_ERROR_STOP=1 \
-c "SELECT pg_wal_replay_resume(); SELECT pg_get_wal_replay_pause_state();"输出应回到 not paused。恢复后再确认复制进度,或者结束并重建整套一次性环境。
只读标记与副本角色分开配置
Spring 事务的 readOnly 提供事务/资源提示;它本身没有复制数据库,也不会自动建立可读副本地址。是否按只读标记选副本,要由具体路由规则决定。
读库故障时回退主库可以提高可用性,但会把额外流量压向主库。回退策略应结合连接池容量、限流和业务重要性;不能把任意读取都默认变成无限次主库重试。
生产复制还涉及同步级别、复制槽、WAL 保留、只读冲突和故障转移。这里的受控实验确认了写后读与重放进度的关系,没有建立自动选主系统。
分片先固定数据归属
database、schema 和 table 是不同分布方式
按租户拆到不同 database,能够分开连接目标;拆到不同 schema,共用数据库实例但使用不同命名空间;拆到不同 table,则还要处理动态表选择、索引和查询组合。
物理上是否跨服务器、是否独立故障域,要由部署决定。实验里的 route_a 和 route_b 在同一主库集群中,展示实际分库路由,却没有获得两台独立数据库服务器的容灾能力。
常见归属方式包括固定查找表、范围、哈希与按业务租户显式分配。选择时需要考虑数据增长和热点:所有大租户落到同一片,哈希分布在“租户数量”上均匀也可能在存储和请求量上失衡。
用两个租户验证实际存放位置
运行:
run_lab shardtenant7StoredA=true tenant8StoredB=true explicitPlacement=true实验使用明确查找规则:
static String shardFor(long tenant) {
if (tenant == 7) return "A";
if (tenant == 8) return "B";
throw new IllegalArgumentException("Unknown tenant placement");
}tenant=7 的行写到 A,tenant=8 的行写到 B,再用独立连接检查。它是一个可核对的分布规则示例,不包含分库中间件的 SQL 解析、跨片归并或在线重分片实现。
分片键还应进入查询条件和权限判断。连接已经去了租户所属库,不意味着同库内其他租户的数据可以省略过滤。
跨片查询与扩容需要额外设计
| 需求 | 单库中常见做法 | 分片后新增的问题 |
|---|---|---|
| 主键生成 | 数据库序列或自增 | 全局唯一与数据来源识别 |
| JOIN | 同库连接两张表 | 数据是否共置、是否需要应用归并 |
| 排序分页 | 一条 ORDER BY/LIMIT | 各片候选数量、全局排序和游标 |
| 唯一约束 | 单表唯一索引 | 唯一性是否跨片成立 |
| 事务 | 单资源提交回滚 | 跨片协调或分阶段恢复 |
| 扩容 | 增加实例资源 | 旧数据归属和迁移过程中读写位置 |
把 tenantId % 2 改成 tenantId % 3 会改变大量键的归属,却不会搬动数据库中的旧行。应用按照新规则查询时,就可能去错位置。
迁移期间要有可查询的旧归属、新归属和当前迁移状态。按实际需要设计增量同步、读路径兼容、校验与切换;不是所有迁移都必须双写,但所有受影响请求都需要知道数据此刻在哪里。
同一个租户的迁移通常还要处理正在进行的事务与任务。切换路由配置之前,至少确认新位置已有完整数据、增量已追上、旧写入口已停止或受控,以及失败时能返回哪一个可用状态。
Flyway 怎样记录和校验结构变更
V1 建表,V2 扩展字段
工程的迁移文件分别是 V1__orders.sql 与 V2__expand_label.sql。
V1 建表并只给应用这张业务表的 DML 权限:
CREATE TABLE tenant_order (
id bigint PRIMARY KEY,
tenant_id bigint NOT NULL,
legacy_label varchar(120) NOT NULL
);
GRANT SELECT, INSERT, UPDATE, DELETE ON tenant_order TO lab_app;V2 增加可空的新列:
ALTER TABLE tenant_order ADD COLUMN new_label varchar(120);迁移工厂使用固定 locations 和禁止 clean 的设置:
Flyway flyway = Flyway.configure()
.dataSource(url, "lab_migrator", migrationPassword)
.locations("classpath:db/migration")
.cleanDisabled(true)
.load();
flyway.migrate();
flyway.validate();Flyway 的 PostgreSQL 支持由对应数据库模块提供,不能只添加 core 就假设所有数据库适配都已存在。Flyway PostgreSQL 模块
运行:
run_lab migratetwoVersionedMigrations=true validatePassed=true查看 A 的历史和结构:
docker compose -p da10-multi exec -T db \
psql -X -U lab_owner -d route_a -v ON_ERROR_STOP=1 \
-c "SELECT version, description, type, success
FROM flyway_schema_history ORDER BY installed_rank;"
docker compose -p da10-multi exec -T db \
psql -X -U lab_owner -d route_a -v ON_ERROR_STOP=1 \
-c "\d tenant_order"历史应包含成功的 V1、V2,结构中同时存在 legacy_label 和 new_label。B 有自己的迁移历史,应分别检查。
历史记录和实际结构分别检查
Versioned migration 按版本执行并记录校验和。已经在持久环境应用的脚本应保持不变,后续修改通过新版本前向迁移。Flyway versioned migrations
Repeatable migration 则按不同规则在内容变化后重新运行,常用于视图等可重建对象。不要把会不断累加业务数据的 INSERT 写成不具备重复执行语义的 repeatable 脚本。
Schema history 保存执行版本、状态和校验和,帮助判断脚本与历史是否一致;它没有逐项比较当前数据库所有表、索引和权限。如果有人在数据库手工加了列,validate 仍需结合实际结构检查才有完整解释。Flyway schema history
已有数据库接入 Flyway 时,baseline 表达从哪个版本开始接管。错误 baseline 可能使必要迁移被跳过,不能用它绕过“库里已经有对象”的告警。
改写已应用脚本会发生什么
运行:
run_lab checksumchangedChecksumRejected=true originalRestoredValidatePassed=true实验把两份脚本复制到容器自己的临时目录,在 V1 副本里追加内容,然后对已应用 V1 的数据库执行真实 validate。校验和不符时失败;恢复原始字节后再次 validate 通过。
变更只发生在容器中的临时副本,原始脚本保持不变,临时目录在 finally 中删除。校验结果取决于脚本副本与数据库历史中保存的校验和。Flyway Validate
遇到真实环境的 checksum 不符,应先找出使用了哪份制品、哪个 locations 和哪条已应用历史。repair 会修改历史,包括对齐校验和等动作;它不会自动把数据库对象恢复成你期望的结构。必须先明确差异和修复方案,再决定是否需要 repair。Flyway Repair
失败 DDL 的恢复取决于语句类型
运行:
run_lab ddlbadMigrationRejected=true createdTableRolledBack=true historyV3Rows=0临时 V3 先建 ddl_probe,再查询一个不存在的列。PostgreSQL 在这组事务性 DDL 失败后回滚;实验检查 ddl_probe 不存在,历史中没有 V3 行,原始脚本集合再次 validate 通过。
并非所有 PostgreSQL DDL 都能放进普通事务块。例如 CREATE INDEX CONCURRENTLY 需要单独的执行策略,失败后还可能需要检查无效索引。迁移工具是否按事务执行,要与具体 SQL 对应。PostgreSQL CREATE INDEX
长表改列、回填数据与建索引还会消耗 I/O、WAL 并影响复制进度。迁移“语法正确”与“适合在当前负载下执行”是两个判断,应在接近数据规模的环境观察锁、执行时间和恢复方式。
应用账号不能改表和迁移历史
运行:
run_lab permissionsapplicationDdlDenied=true sqlState=42501 migrationRoleDdlAllowed=true applicationHistoryWriteDenied=true应用角色 ALTER TABLE 得到 42501,迁移角色可以添加并移除自己的测试列。实验还检查应用角色没有 flyway_schema_history 的 UPDATE 权限。
权限应按业务表授予,避免为迁移角色创建的所有未来表默认开放应用写入,从而连迁移历史表也一起开放。数据库角色权限的具体含义见 PostgreSQL GRANT。
新旧程序并存时怎样回填和切换
Expand、回填、切读、Contract
增加字段通常可以分成几个兼容阶段:
| 阶段 | 数据库结构 | 旧程序 | 新程序 |
|---|---|---|---|
| 扩展 | 保留旧列,新增可空列 | 继续使用旧列 | 部署兼容读写逻辑 |
| 回填 | 新旧列同时存在 | 旧写入口需受控 | 新写入维护必要的新旧值 |
| 切读 | 校验新列覆盖率和差异 | 逐步退出 | 从新列读取 |
| 收缩 | 在确认旧依赖退出后删除旧结构 | 已停止 | 仅使用新结构 |
这里的兼容首先依赖程序读写方式。例如旧程序使用无列名 INSERT 或依赖 SELECT * 的固定列序,新增列也可能影响它。发布前要用真实旧制品验证,不能仅凭“只加列”就宣布兼容。
一个单库列迁移不一定需要复杂双写平台;新程序在同一事务中更新新旧列、配合受控回填,可能已经满足需求。跨库迁移又要额外处理两个写入的提交与故障恢复,不能直接套用同一个保证。
有界回填不要覆盖在线新值
运行:
run_lab backfillboundedBackfillRows=2 nullRows=0 onlineValuePreserved=true实验准备三行:前两行 new_label 为空,第三行已经被在线逻辑写成 online-new。回填只选稳定主键顺序中的有限缺失行,并在 UPDATE 中再次保留空值条件:
WITH batch AS (
SELECT id
FROM tenant_order
WHERE new_label IS NULL
ORDER BY id
LIMIT 2
)
UPDATE tenant_order o
SET new_label = o.legacy_label
FROM batch b
WHERE o.id = b.id
AND o.new_label IS NULL;这次更新两行,第三行的在线新值保留。实际大表应按稳定主键推进进度、分批提交,并记录受影响行数和重试位置。并发回填工作者还需要避免重复竞争,不能无限地同时扫描全部缺失行。
只填 NULL 解决的是“不要覆盖已有新值”。如果旧版本仍只更新 legacy_label,new_label 仍可能再次过期。因此要先使在线写路径兼容,再回填,并在切读前复查缺失与业务差异。
判断何时能够删除旧结构
切读前应检查新列缺失、转换失败、抽样业务值,以及仍在运行的旧进程、延迟任务和离线脚本。总行数相同只是其中一个信息。
删除旧列以后,回滚旧应用可能不再可行。回退方案要分别说明应用版本和数据库结构能回到哪里;在兼容窗口内保留旧列,通常能获得更简单的回退路径。
对分片迁移也一样:读流量已切换不表示旧库可以立即删除。仍需等待旧写入停止、复制或增量任务结束,并确认查询与异步处理不再依赖旧位置。
连接选错、复制滞后和迁移失败的排查
| 现象 | 第一项检查 | 下一步 |
|---|---|---|
| 路由键改了,SQL 仍去旧库 | current_database 与当前事务连接绑定 | 在下次工作单元选库,勿绕过事务另借连接 |
| 缺租户却落入默认库 | lenientFallback、默认目标、空键处理 | 拒绝缺失/未知归属并补上下文清理 |
| 异步任务偶发访问其他租户 | 线程池中的上下文传递与残留 | 显式传值,用作用域 finally 恢复 |
| 主库写成功,副本查不到 | pg_is_in_recovery、重放进度与读取快照 | 主库读,或按正确复制关系等待目标位置 |
| Flyway checksum 不符 | 制品、locations、历史版本和原始脚本 | 恢复正确制品,或制定明确前向修复 |
| 迁移失败后某些对象还在 | 语句是否支持事务、实际对象状态 | 按数据库事实清理或修复,不直接洗掉历史 |
| 回填完成后新列又出现差异 | 是否仍有旧写入口 | 修正兼容写路径,再补数据并重新校验 |
Spring DataSourceUtils 会把连接获取失败包装成 CannotGetJdbcConnectionException。排查缺路由时应继续查看 cause 中的 IllegalStateException,不把所有外层连接异常都当成数据库宕机。
完整运行:
run_lab all这包含真实副本暂停/恢复,必须先启动 replica profile。程序将临时脚本、线程、连接和连接池按作用域释放;异常或断言失败会非零退出。迁移负例日志中的失败是预期分支,仍需看到相应恢复断言。
结束并确认副本未留在暂停状态
docker compose -p da10-multi exec -T replica \
psql -X -U lab_owner -d route_a -v ON_ERROR_STOP=1 \
-c "SELECT pg_wal_replay_resume();"
docker compose -p da10-multi --profile replica down
unset LAB_DB_PASSWORD LAB_APP_PASSWORD LAB_MIGRATION_PASSWORD LAB_REPLICATION_PASSWORD
unset LAB_DB_A_URL LAB_DB_B_URL LAB_REPLICA_URL LAB_DIR LAB_CACHE
unset -f run_lab命令只移除 da10-multi 的主库、副本和网络;tmpfs 中的实验数据随容器消失,源码和 Maven 缓存保留。这里没有执行 Flyway clean,也没有移除任何外部数据库。
权威资料与规范地址
路由时点查 Spring 连接与代理文档,复制状态查 PostgreSQL,迁移历史及校验操作查 Flyway。
| 资料 | 完整地址 |
|---|---|
| AbstractRoutingDataSource | https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.html |
| LazyConnectionDataSourceProxy | https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.html |
| Boot 依赖版本 | https://docs.spring.io/spring-boot/appendix/dependency-versions/coordinates.html |
| pg_basebackup | https://www.postgresql.org/docs/18/app-pgbasebackup.html |
| Spring JDBC 连接管理 | https://docs.spring.io/spring-framework/reference/data-access/jdbc/connections.html |
| PostgreSQL standby 与流复制 | https://www.postgresql.org/docs/18/warm-standby.html |
| PostgreSQL 管理函数 | https://www.postgresql.org/docs/18/functions-admin.html |
| Flyway PostgreSQL 模块 | https://documentation.red-gate.com/flyway/reference/database-driver-reference/postgresql-database |
| Flyway versioned migrations | https://documentation.red-gate.com/flyway/flyway-concepts/migrations/versioned-migrations |
| Flyway schema history | https://documentation.red-gate.com/flyway/flyway-concepts/migrations/flyway-schema-history-table |
| Flyway Validate | https://documentation.red-gate.com/flyway/reference/commands/validate |
| Flyway Repair | https://documentation.red-gate.com/flyway/reference/commands/repair |
| PostgreSQL CREATE INDEX | https://www.postgresql.org/docs/18/sql-createindex.html |
| PostgreSQL GRANT | https://www.postgresql.org/docs/18/sql-grant.html |
