数据库基础体系 · 第 12/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
数据库复制、分片与高可用:一致性、路由、故障转移和扩容
在单机数据库中,数据、事务日志、连接入口和故障处理通常集中在一个实例内。复制、分片和高可用把这些职责拆到多个节点后,系统获得了更高的可用性和容量,却也引入了新的问题:
- 副本上的数据什么时候可见?
- 客户端应该访问哪个节点?
- 主节点故障时谁可以接管?
- 故障切换后,旧主节点恢复上线是否会造成脑裂?
- 分片后一个事务涉及多个分片怎么办?
- 增加节点时,已有数据如何迁移且不破坏路由?
- “写成功”究竟意味着数据已经存储在哪里?
这些问题不能仅靠“部署主从”“加一个代理”解决。需要先区分复制、分片和高可用的目标,再分析它们对一致性、事务和运维流程的影响。
一、三个概念解决三类不同问题
1. 复制:同一份逻辑数据保存多份
**数据库复制(replication)**是把一个数据集或其变更传播到多个数据库实例,使多个实例拥有相同或近似相同的数据副本。
最常见的形式是:
客户端
|
v
主库(接受写入)
|
| 复制日志、事务变更
v
副本库(通常提供读取或故障接管)
复制主要解决:
- 节点故障时保留数据副本;
- 通过副本分担部分读取;
- 跨可用区或跨地域保存数据;
- 在某些系统中支持在线升级或备份。
复制不等于自动高可用。只有副本并不能自动完成:
- 故障检测;
- 选主;
- 修改访问入口;
- 防止旧主节点继续接收写入;
- 确认新主节点拥有足够完整的数据;
- 让旧主节点安全地重新加入。
2. 分片:把一份逻辑数据拆成多份
**分片(sharding)**是按照分片键,把一个逻辑数据集拆分到多个独立的数据分区中。
例如有四个分片:
shard-0: user_id % 4 = 0
shard-1: user_id % 4 = 1
shard-2: user_id % 4 = 2
shard-3: user_id % 4 = 3
与复制不同,分片节点通常保存的是不同数据,而不是同一数据的完整副本。分片主要解决:
- 单节点存储容量不够;
- 单节点写入吞吐不够;
- 单节点索引、锁或连接资源成为瓶颈。
分片会把原来由单个数据库内部保证的事情,部分转移给应用、代理或分布式数据库层:
- 路由;
- 跨分片事务;
- 全局唯一 ID;
- 跨分片查询;
- 数据迁移;
- 分片故障恢复。
3. 高可用:在故障下维持可接受的服务
**高可用(high availability,HA)**是系统在部分组件故障时,仍能在规定时间和功能范围内提供服务。
高可用至少涉及四个状态:
- 正常提供服务;
- 发现故障;
- 选择或确认接管者;
- 将客户端流量切换到接管者,并处理旧节点恢复。
因此:
复制 = 数据如何有多个副本
分片 = 数据如何拆开存储
高可用 = 故障时如何继续提供服务
三者可以组合。例如,每个分片内部采用一主两副本:
逻辑数据库
├── shard-0
│ ├── primary-0
│ ├── replica-0a
│ └── replica-0b
└── shard-1
├── primary-1
├── replica-1a
└── replica-1b
这不是“分片复制二选一”,而是两个维度的组合。
二、复制传播的核心:变更从哪里来,什么时候算完成
1. 物理复制与逻辑复制
数据库复制通常有两种抽象。
物理复制
物理复制传播数据库存储层或预写日志(WAL、redo log 等)的变化。副本按照日志重放,使存储页或内部结构达到与源库相同的状态。
特点:
- 通常复制整个实例或数据库集群;
- 对表结构和 SQL 透明;
- 常用于高可用热备;
- 副本一般不能独立写入;
- 主库和副本的数据库版本、平台能力通常有较强约束。
PostgreSQL 的流复制属于典型的物理复制机制:主库产生 WAL,备用服务器接收并重放 WAL。备用服务器可以在 hot standby 模式下提供只读查询,但不能像普通主库一样接受写事务。
逻辑复制
逻辑复制传播“插入一行”“更新某些列”“删除一行”等逻辑变更,而不是直接复制底层页面。
特点:
- 可以选择表或数据集;
- 可能支持不同实例承担不同职责;
- 适合数据集成、版本迁移、部分表同步;
- 需要处理表结构、主键、DDL 和初始数据同步;
- 不能简单等同于高可用备库。
逻辑复制的一个常见误解是:“订阅端数据最终一致,所以它就是热备。”实际上,订阅端是否能完整接管源库,取决于未复制的对象、DDL、序列、权限、扩展、未确认事务以及切换流程,不能只看表数据是否相同。
2. 同步复制与异步复制
设客户端提交事务 ,主库先将日志写入本地。若日志尚未复制到其他节点,主库立即返回成功,这通常是异步复制。
定义:
- :主库本地持久化完成;
- :副本接收到日志;
- :副本已应用日志;
- :客户端收到提交成功响应。
异步复制可能满足:
也就是说,客户端已经看到成功,但副本还没有收到该事务。若随后主库永久损坏,事务可能丢失。
同步复制要求在返回成功前等待指定副本达到某个确认点。概念上可以表示为:
其中 是要求确认的副本数量。注意“副本收到日志”和“副本已经执行并对查询可见”不是同一件事。不同产品、不同配置对同步确认点的定义不同,必须以具体数据库语义为准。
同步复制降低了故障丢失数据的概率或窗口,但会增加提交延迟,并且同步副本不可用时可能影响写入可用性。它没有自动解决网络分区下的双主问题。
3. PostgreSQL 示例:观察复制状态
在 PostgreSQL 主库上,可以查看发送端状态:
SELECT
application_name,
client_addr,
state,
sync_state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn
FROM pg_stat_replication;
这些字段反映不同进度:
sent_lsn:主库已经发送到副本的位置;write_lsn:副本已经写入的位置;flush_lsn:副本已经刷盘的位置;replay_lsn:副本已经重放的位置;sync_state:该副本是否被配置为同步副本。
可以用 WAL 位置估算滞后:
SELECT
application_name,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)
) AS replay_lag
FROM pg_stat_replication;
这段查询只能说明主库与副本的 WAL 位置差距,不能直接证明所有业务查询都已经获得相同结果。副本还可能存在查询快照、长事务或恢复延迟。
在副本上:
SELECT
pg_is_in_recovery() AS is_standby,
pg_last_wal_receive_lsn() AS received_lsn,
pg_last_wal_replay_lsn() AS replayed_lsn;
若 pg_is_in_recovery() 返回 true,说明当前实例仍处于恢复状态,通常是备用节点。received_lsn 与 replayed_lsn 的差距表示已接收但尚未应用的日志。
4. MySQL 示例:复制位置与 GTID
MySQL 8.4 的 InnoDB 复制通常以 source/replica 术语描述。使用 GTID 时,事务由全局事务标识表示,副本可以根据已执行的 GTID 判断哪些事务已经应用,较容易支持断点恢复和拓扑切换。
副本状态可使用:
SHOW REPLICA STATUS\G
应重点关注:
Replica_IO_Running:复制接收线程是否运行;Replica_SQL_Running:复制应用线程是否运行;Seconds_Behind_Source:估算的应用延迟;Retrieved_Gtid_Set:已从源端取得的 GTID 集合;Executed_Gtid_Set:已执行的 GTID 集合;Last_IO_Error、Last_SQL_Error:接收或应用错误。
Seconds_Behind_Source 不是精确的端到端延迟指标。例如源端长事务、时钟不准确、复制线程停止时,它可能无法准确反映业务读到旧数据的程度。诊断时应结合 GTID 集合、事务提交时间、应用查询延迟和副本负载。
三、一致性不是一个开关
“副本一致”至少有三种不同含义。
1. 数据最终一致
若停止新的写入并等待足够长时间,所有健康副本最终达到同一数据状态,可以称为最终一致的一个基本表现。
它不保证:
- 写入成功后立即从任意副本读到;
- 每次读取都单调前进;
- 读取结果反映最近一次提交;
- 主库故障后副本一定包含最后提交的事务。
2. 读己之写
**读己之写(read-your-writes)**要求:
客户端成功写入 x
随后该客户端读取 x
不能读到比自己的写入更旧的状态
异步主从架构中,最常见的违反方式是:
1. 写请求发送到主库,返回成功
2. 读取请求被负载均衡到延迟中的副本
3. 读取不到刚写入的数据
这不是数据库事务隔离级别失效,而是跨节点路由和复制时序造成的可见性问题。
可行的解决方式包括:
- 写后的一段时间内固定读主库;
- 将会话绑定到主库;
- 携带复制位置或 GTID,读取时等待副本追上该位置;
- 对关键读取始终访问主库。
“等待固定 1 秒”不是严格保证,因为复制延迟可能超过 1 秒。
3. 线性一致性与串行化
**线性一致性(linearizability)**要求每个操作看起来在其调用和返回之间某一时刻原子发生,并且 real-time 顺序得到保持。
若写操作 已经向客户端返回成功,之后开始的读操作 必须看到 的结果:
**串行化(serializability)**则是事务执行结果等价于某个串行执行顺序。它主要描述并发事务之间的结果,不自动保证跨副本读取的时间顺序。
因此:
- 数据库可以提供串行化事务,但异步副本仍可能读到旧数据;
- 数据库可以有同步复制,但跨分片事务仍可能没有全局串行化;
- “副本延迟为 0”也不等于所有客户端请求都满足线性一致性。
四、事务边界:复制不扩大事务,分片会改变事务范围
1. 单库事务的边界
在同一个数据库实例、同一个事务上下文内:
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE account_id = 2;
COMMIT;
数据库可以保证两条更新作为一个事务提交或回滚。具体可见性和并发行为还取决于隔离级别。
如果 account_id=1 和 account_id=2 被分到不同数据库,原来的本地事务就不能自然覆盖两个节点。此时有三种典型选择:
- 禁止跨分片事务:要求业务让一次事务只访问一个分片;
- 两阶段提交(2PC):引入协调者和准备、提交两个阶段;
- 业务级最终一致性:用消息、状态机、补偿操作表达跨分片流程。
2. 两阶段提交的故障点
两阶段提交大致经过:
协调者 -> 分片 A:PREPARE
协调者 -> 分片 B:PREPARE
分片 A、B:持久化“已准备”状态
协调者 -> 分片 A、B:COMMIT
如果协调者在所有分片准备完成后、发送 COMMIT 前崩溃,参与者可能长期处于 prepared 状态。此时参与者不能自行判断应该提交还是回滚,必须恢复协调者状态或由人工依据事务日志处理。
因此 2PC 可以提供较强的原子性,但代价是:
- 协调者可用性影响事务完成;
- 锁或 prepared 状态可能长期占用资源;
- 延迟增加;
- 故障恢复流程复杂。
“分片后仍然使用一个全局事务”不是免费能力。
五、路由:请求如何找到正确的数据节点
复制和分片都需要路由,但路由依据不同。
1. 复制路由
复制架构中的典型角色:
写入 -> 当前主库
强一致读取 -> 当前主库,或满足读取条件的同步副本
允许旧数据的读取 -> 只读副本
故障切换 -> 新主库
常见错误是只配置一个读写代理,却没有定义它如何判断主库身份。代理如果只看到 TCP 端口可连接,并不能证明节点有资格接受写入。
路由层至少要知道:
- 当前主库是谁;
- 节点是否处于恢复状态;
- 节点是否已经被隔离;
- 副本延迟是否超过业务阈值;
- 当前连接中的事务是否需要保持在同一节点。
连接池会放大路由问题。应用切换主库后,已有连接可能仍指向旧主库,因此故障转移不仅是修改 DNS 或 VIP,还需要:
- 关闭或驱逐旧连接;
- 让应用重建连接;
- 处理事务中断;
- 对提交结果未知的请求进行重试决策。
2. 分片路由
分片路由函数可以抽象为:
其中:
- :分片键;
- :当前分片映射;
- :目标分片。
取模分片:
优点是简单,缺点是改变 时大量数据映射改变。
例如从 4 个分片扩展到 5 个分片:
user_id = 6
6 % 4 = 2
6 % 5 = 1
同一个用户会被路由到不同分片,若没有迁移和双读策略,数据就会“消失”在旧路由或产生重复写入。
更常见的做法是使用一致性哈希、范围分片或显式分片表。以范围分片为例:
range-0: user_id < 1,000,000
range-1: 1,000,000 <= user_id < 2,000,000
range-2: user_id >= 2,000,000
范围分片便于按范围迁移,但可能出现热点。例如新用户 ID 持续增长时,最大的范围分片承受大部分写入。
3. 分片键的选择
一个合适的分片键通常具有以下性质:
- 查询经常携带它;
- 数据分布较均匀;
- 写入不会集中到单个分片;
- 相关事务尽量共享它;
- 业务生命周期内不经常变化。
例如订单系统按 user_id 分片,以下查询可以定向到一个分片:
SELECT *
FROM orders
WHERE user_id = 1001
AND order_id = 90001;
但以下查询无法天然定向:
SELECT *
FROM orders
WHERE status = 'pending'
ORDER BY created_at
LIMIT 100;
它可能需要访问所有分片,再进行合并排序。随着分片数量增长,查询成本和结果不确定性都会增加。
4. 路由元数据本身也需要高可用
如果业务依赖一张映射表:
逻辑分片 -> 物理数据库节点
那么这张表不能只保存在某个应用进程的内存中。路由元数据需要:
- 版本号;
- 原子发布;
- 变更审计;
- 回滚能力;
- 与数据迁移状态关联。
迁移时不能简单地先修改路由再搬数据。否则新路由已经指向目标分片,但数据仍在源分片,会造成读不到数据。
六、故障转移:从发现故障到安全接管
高可用切换不是一个动作,而是一条状态机。
1. 正常状态
primary P 接受写入
replica A、B 接收并应用复制日志
路由层将写请求发送到 P
系统需要持续记录:
- 主库是否可连接;
- 主库是否仍可写;
- 副本复制是否正常;
- 副本落后多少;
- 副本是否具备接管所需的日志;
- 监控节点之间是否能够互相通信。
2. 故障检测
如果监控节点无法连接 P,不能立即断言 P 已故障。可能的情况包括:
- P 真故障;
- 监控节点到 P 的网络中断;
- 监控节点自身故障;
- 认证、DNS 或代理异常;
- P 仍在运行但暂时阻塞。
因此自动选主通常需要仲裁或多数派。设有 个投票成员,法定人数为:
只有获得多数派的候选者才允许改变集群领导权,可以降低网络分区导致双主的风险。
不过,数据库复制本身未必提供这样的共识机制。外部编排系统、数据库集群协议或人工流程必须明确谁有权提升副本。
3. 选择接管者
接管者至少要考虑:
- 已接收并持久化的日志;
- 已应用的事务位置;
- 是否存在未完成恢复;
- 与其他候选者的连通性;
- 节点所在故障域;
- 数据丢失目标(RPO)。
异步复制下,候选副本可能缺少主库最后提交的事务。若强行提升,则系统进入新任期,但这些事务可能永远丢失。
4. 提升与路由切换
典型顺序是:
1. 确认旧主库不能继续接受写入,或将其隔离
2. 选择副本 P'
3. 让 P' 完成恢复并提升为可写节点
4. 更新服务发现、VIP、代理或连接串
5. 驱逐旧连接
6. 让应用重连并执行健康检查
7. 记录新拓扑和数据位置
“先切流、后隔离旧主库”可能产生双主:
客户端 A -> 新主库 P'
客户端 B -> 旧主库 P
如果两边都接受写入,冲突可能无法通过普通 WAL 重放解决。即使两边写入的是不同记录,也可能在唯一键、余额、库存等业务约束上产生不可逆分歧。
5. PostgreSQL 手动提升示例
在确认旧主库已经停止写入并且不会重新接收客户端流量后,可在 PostgreSQL 备用节点上执行提升:
pg_ctl -D /var/lib/postgresql/data promote
也可以使用:
SELECT pg_promote();
预期结果是该节点结束恢复状态,成为可写主库。验证:
SELECT pg_is_in_recovery();
返回 false 只表示该实例不再处于恢复状态,并不等于:
- 应用流量已经切换;
- 旧主库已经隔离;
- 所有副本都跟随新主库;
- 数据丢失符合 RPO;
- 业务事务可以安全重试。
提升前应保存并比较主库和副本的 WAL 位置。提升之后,旧主库通常不能直接继续作为新主库的副本,因为两者历史已经分叉,需要重新同步,例如重新构建备用库或使用适当的时间线跟随流程。
6. MySQL 复制切换的关键风险
MySQL 复制切换也不能只执行“停止旧源、把 replica 指向新源”。需要确认:
- 新源是否已执行旧源的最后事务;
- 复制使用的 GTID 集合是否连续;
- 旧源是否已被隔离;
- 新源是否能被所有应用连接;
- 其他副本是否需要重新指向新源;
- 提交结果未知的客户端事务如何处理。
使用 GTID 可以减少基于文件名和位置手工计算的错误,但不自动保证应用层请求没有重复执行。比如客户端发送 COMMIT 后连接断开,客户端无法知道服务器是否已经提交。重试前必须依赖幂等键、业务唯一约束或事务状态查询。
七、RPO、RTO 与复制配置的关系
**RPO(Recovery Point Objective)**表示故障后最多允许丢失多近的数据时间点。
**RTO(Recovery Time Objective)**表示从故障发生到恢复服务允许经过的最长时间。
异步复制中,若主库在事务 提交后、该事务到达副本前永久损坏,则:
这意味着 RPO 至少覆盖这段复制延迟,实际还取决于是否有其他日志归档、备份或恢复手段。
同步复制可以缩小“已确认但副本没有”的窗口,但需要注意:
- 同步确认的节点数量;
- 确认是写入内核、刷盘还是应用完成;
- 同步副本是否位于同一故障域;
- 网络分区时是否阻塞写入;
- 数据库提交和外部系统副作用是否同一事务。
复制不能代替备份。复制错误会被复制:
误删数据 -> 主库产生删除日志 -> 副本也执行删除
因此全量备份、增量备份、WAL/binlog 归档和 PITR 仍然用于处理逻辑错误、误操作、勒索和时间点恢复;复制主要用于节点级故障下的快速接管。
八、分片后的查询和数据模型
1. 定向查询与广播查询
定向查询能够根据分片键确定一个分片:
请求:查询 user_id = 1001 的订单
路由:f(1001) -> shard-2
访问:只访问 shard-2
广播查询必须访问多个分片:
请求:查询所有用户中最近创建的 100 个订单
路由:shard-0、shard-1、shard-2、shard-3
每个分片取候选结果
协调层合并、排序、截断
如果每个分片都执行:
SELECT order_id, user_id, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 100;
协调层不能简单拼接四个结果,因为全局前 100 条可能集中在某个分片,也可能出现在各分片的局部候选中。常见合并步骤是:
- 每个分片返回局部前 100;
- 协调层按
created_at做归并排序; - 取全局前 100。
当查询包含过滤、聚合、分页和 JOIN 时,协调层需要承担更多计算,且错误处理从单节点错误变成部分分片成功、部分分片失败。
2. 跨分片 JOIN
若两张表按同一分片键、同一映射规则分片:
users(user_id)
orders(user_id)
那么:
SELECT *
FROM users u
JOIN orders o ON o.user_id = u.user_id
WHERE u.user_id = 1001;
可以路由到同一分片,接近本地 JOIN。
如果一张表按 user_id 分片,另一张表按 merchant_id 分片,JOIN 就需要:
- 广播;
- 数据重分布;
- 复制维度表;
- 在应用层分步查询;
- 使用支持分布式执行的数据库。
这也是为什么数据模型和分片键必须一起设计,而不是建表后再由代理自动解决所有问题。
3. 全局唯一 ID
分片后,数据库自增列不再天然保证全局唯一。可选方案包括:
- 使用带时间和节点信息的 ID;
- 使用独立 ID 服务;
- 预分配号段;
- 使用 UUID 等随机或半随机标识。
选择时要考虑:
- 是否需要按时间排序;
- 是否会导致索引页热点;
- 是否需要隐藏业务规模;
- 是否必须短整数;
- 时钟回拨如何处理。
不能仅因为字段类型是 BIGINT,就认为多个分片自动共享一个自增序列。
九、扩容:增加节点不等于增加容量
扩容至少有两种完全不同的情况。
1. 复制副本扩容
增加一个只读副本通常不会增加写入容量。主库仍然需要:
- 产生全部日志;
- 发送日志;
- 承担全部写事务;
- 维护主库索引和锁。
副本扩容主要改善读取能力和故障冗余,但复制链路、主库网络和存储仍可能成为瓶颈。
2. 分片扩容
分片扩容需要同时改变:
数据位置 + 路由映射 + 迁移状态 + 读写行为
一个安全的范围迁移流程可以是:
阶段一:建立目标分片
创建目标表、索引、权限和复制配置,验证空目标分片可用。
阶段二:复制存量数据
按主键范围分批复制:
INSERT INTO target.orders (...)
SELECT ...
FROM source.orders
WHERE order_id >= :low
AND order_id < :high;
实际系统应使用应用参数绑定,而不是把输入直接拼接进 SQL。批量复制还要记录进度、处理唯一键冲突,并控制对源库的读压力。
阶段三:追踪增量变更
在存量复制期间,源分片仍可能发生 INSERT、UPDATE、DELETE。需要使用数据库日志、变更数据捕获或业务双写机制追踪增量。
“双写”并不天然可靠:
写源成功
写目标失败
如果没有重试、幂等键和对账机制,两边就会产生分歧。
阶段四:短暂切换或双读校验
先让新路由继续读源,后台比较源和目标;确认增量追平后,执行短暂切换,使新写入进入目标。
若必须双写,切换期间应明确哪个副本是权威来源,避免两个方向互相覆盖。
阶段五:更新路由并验证
更新带版本的路由映射:
range [0, 1,000,000) -> shard-0
range [1,000,000, 1,500,000) -> shard-1
range [1,500,000, 2,000,000) -> shard-2
验证内容包括:
- 新旧边界附近的记录;
- 写入是否只进入目标;
- 读取是否命中新分片;
- 全局计数和局部计数是否符合预期;
- 失败重试是否产生重复数据。
阶段六:保留回滚窗口
不要刚切换就删除源数据。应在确认应用、报表、异步任务和补偿流程都已使用新路由后,再清理旧数据。清理前必须有备份和可验证的恢复方案。
十、常见误解与失败表现
误解一:副本延迟低,所以读副本等于强一致
失败表现:
写主库成功
紧接着读副本,读不到新数据
原因是低延迟不是零延迟,也不是线性一致性保证。需要使用主库路由、会话粘滞或基于复制位置的等待。
误解二:有两个数据库节点,就能避免脑裂
如果两个节点都能继续接受写入,网络分区时可能出现:
节点 A 认为 B 已故障
节点 B 认为 A 已故障
A、B 都被提升为主库
必须有明确的仲裁、隔离和任期机制。复制通道本身只能传播数据,不能总是决定谁拥有唯一写入权。
误解三:切换主库只需要改 DNS
DNS 缓存、连接池和已有 TCP 连接可能使客户端继续访问旧主库。并且 DNS 变更不负责:
- 回收旧连接;
- 中断旧事务;
- 处理客户端提交结果未知;
- 修复复制拓扑。
服务发现只是流量入口的一部分。
误解四:复制能代替备份
主库误删数据后,副本会忠实复制删除。必须通过备份、归档日志和 PITR 恢复到删除前的时间点。
误解五:把表拆到多个库就是分布式数据库
如果应用自己决定分片、拼接查询、实现重试和跨库事务,它确实构成了分布式数据访问层,但不代表数据库自动提供:
- 全局事务;
- 全局约束;
- 全局唯一索引;
- 全局一致快照;
- 自动重平衡。
这些能力必须明确由哪个组件负责。
十一、诊断方法:先判断是数据问题、路由问题还是拓扑问题
1. 先确认客户端实际连接的节点
在 PostgreSQL 中:
SELECT
inet_server_addr(),
inet_server_port(),
pg_is_in_recovery(),
current_setting('transaction_read_only');
在 MySQL 中可以查看:
SELECT
@@hostname,
@@read_only,
@@super_read_only,
@@server_uuid;
不要只根据代理配置推断请求去了哪里。
2. 再确认复制进度
需要区分:
已产生日志
-> 已发送
-> 已接收
-> 已刷盘
-> 已应用
-> 查询可见
只看一个“延迟秒数”通常不够。应结合日志位置、GTID、长事务、磁盘 I/O、复制线程状态和错误信息。
3. 最后确认路由和业务重试
若数据库显示事务已提交,但客户端报连接错误,结果属于“提交状态未知”。此时直接重试可能造成:
- 重复订单;
- 重复扣款;
- 重复消息;
- 唯一键冲突。
正确处理需要业务幂等键。例如:
CREATE TABLE payment_requests (
request_id varchar(64) PRIMARY KEY,
payment_id bigint NOT NULL,
status varchar(16) NOT NULL
);
同一个 request_id 重试时,应用先查询已有状态,而不是无条件再次执行扣款。
十二、如何把复制、分片和高可用组合起来
一个实际系统可以按以下层次理解:
应用
|
| 业务路由、幂等、重试、跨分片协调
v
分片路由层
|
+--> shard-0 的 HA 集群
| primary + replicas
|
+--> shard-1 的 HA 集群
primary + replicas
一次请求的完整路径可能是:
1. 应用提取 user_id
2. 路由层根据当前映射选择 shard-1
3. shard-1 内部将写请求发送给当前 primary
4. primary 提交本地事务并复制日志
5. 返回提交结果
6. 读取请求根据一致性要求选择 primary 或合适副本
任何一层都可能失败:
- 路由映射过期;
- 分片主库故障;
- 副本延迟;
- 复制链路中断;
- 代理仍缓存旧主库;
- 客户端在提交后断开;
- 迁移期间新旧分片数据不一致。
所以高可用设计必须同时描述:
- 数据副本如何产生;
- 提交成功的确认语义;
- 请求如何路由;
- 故障由谁判定;
- 谁拥有提升权限;
- 旧节点如何隔离和重新加入;
- 跨分片事务如何处理;
- 扩容迁移如何验证和回滚;
- 备份如何覆盖复制无法处理的逻辑错误。
最终,复制回答“数据还有没有另一份”,分片回答“数据如何分布以获得容量”,高可用回答“节点故障时谁继续提供服务”。一致性则回答“客户端在这些变化中究竟能观察到什么”。只有把这四个问题分别定义清楚,数据库集群的架构、路由和故障流程才具有可验证的行为,而不是依赖“主从正常”“代理在线”这类过于粗略的状态描述。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:数据库备份与恢复:全量、增量、PITR、RPO/RTO 和恢复演练
- 下一篇:数据库安全治理:最小权限、加密、审计、脱敏与注入防护
- 延伸:数据库事务完整指南:ACID、隔离级别、异常现象与正确边界
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论