PostgreSQL 复制机制与复制冲突

一、PostgreSQL 复制的核心概念与主流复制类型

PostgreSQL 的复制,本质是将主库(Primary)的数据变更,同步到一个 / 多个备库(Standby) 的过程,核心目的是:实现高可用(主库宕机备库秒级切换)、读写分离(备库承接查询请求,减轻主库压力)、数据容灾(多节点存储数据,防止单点丢失)。
 
PostgreSQL 官方原生支持两种核心复制方式,也是生产环境最常用的,二者的实现原理、使用场景、冲突概率完全不同,复制冲突的产生和解决,也和复制类型强绑定,这是理解冲突的基础,必须先吃透:
 

 1. 物理复制(也叫 流复制 / Streaming Replication)

 
这是 PostgreSQL 最主流、最常用、默认推荐 的复制方式,90% 的生产环境用的都是它。
 
  • 核心原理:主库产生的所有数据变更,都会写入「预写日志 WAL」(Write-Ahead Log),备库通过网络实时拉取主库的 WAL 日志,在备库本地原样重放 WAL 日志,实现数据和主库完全一致。
  • 核心特点:基于数据库物理块的复制,复制粒度是「数据页」,对业务透明、无需额外配置,同步效率极高、稳定性极强。
  • 关键规则:标准物理复制的备库,默认是【只读模式】 ,备库上只能执行 SELECT 查询,无法执行 INSERT/UPDATE/DELETE/ALTER 等任何写入操作。
 

2. 逻辑复制(Logical Replication)

 
PG 10 开始原生支持的轻量级复制方式,属于表级复制,灵活性极高,是近几年的主流方案之一。
 
  • 核心原理:主库的 WAL 日志不再是「原样重放」,而是通过插件解析成逻辑层面的行变更数据(比如:给 user 表新增了 id=100 的行、更新了 id=20 的 name 字段),然后把这些行变更,同步到备库指定的表中。
  • 核心特点:复制粒度是「表 / 行」,可以只复制指定表、指定字段;支持跨 PostgreSQL 版本、跨数据库(甚至跨异构数据库)、跨服务器的复制;备库支持「读写双模式」 —— 备库可以正常执行写入操作,这也是逻辑复制的核心优势,也是冲突高发的根源。
 

 

二、复制冲突的「核心前提」与「根本原因」(重中之重)

 

【核心结论】只读备库,永远不会有复制冲突!

 
很多人遇到复制冲突的第一反应是「复制配置错了」,但99% 的复制冲突,根源只有一个:
 
备库上存在「写入行为」 ,主库同步过来的变更日志,和备库本地已写入的数据,产生了数据不一致、操作互斥的矛盾,最终导致备库无法正常执行同步操作,触发「复制冲突」。
 
 

复制冲突的本质

 
复制冲突 = 主库的同步指令 + 备库本地的写入数据 → 二者矛盾,执行失败。
 
  • 物理复制:只有在「备库开启只读查询模式(Hot Standby)」时,会出现特殊的轻量冲突(无数据不一致,只是查询和同步互斥);纯只读备库无任何冲突。
  • 逻辑复制:因为备库支持「自由写入」,冲突是高频且多样的,是逻辑复制的核心痛点。
 

 补充:复制冲突的影响范围

 
非常重要的一点:PostgreSQL 的复制冲突,只会影响「备库」,绝对不会影响主库!
 
主库的业务写入、查询、事务,一切正常不受影响;冲突只会导致「备库同步中断 / 同步延迟」、备库的查询报错,最坏的情况是备库复制进程终止,不会对主库产生任何连带故障。
 

 

三、物理复制(流复制)的冲突:仅发生在 Hot Standby 模式,类型单一

 

1. 物理复制的冲突前提

 
物理复制的备库,有两种运行状态:
 
  • 纯备库模式:备库只能启动 / 关闭,无法连接查询,零冲突;
  • Hot Standby 模式:备库只读,但可以正常连接、执行 SELECT 查询,这是生产环境的标配(毕竟要做读写分离),只有这种模式下,物理复制才会产生冲突。
 

2. 物理复制的「2 种核心冲突类型」(全部高频遇到)

 
物理复制的冲突,不是「数据不一致」的冲突,而是 「备库的长查询」与「主库同步过来的变更操作」的资源互斥冲突,本质是:备库在重放主库的 WAL 日志(执行变更)时,备库本地有正在运行的查询事务,占用了对应的资源,导致同步操作无法执行。
 

✔ 冲突 1:主库执行 DROP / TRUNCATE / ALTER 表,备库有长查询访问该表

 
主库删表、清空表、修改表结构,这个变更会同步到备库;如果此时备库刚好有一个长时间运行的 SELECT 查询,正在访问这个被修改的表,备库就会触发冲突。
 

✔ 冲突 2:主库执行 VACUUM 清理死元组,备库有长查询引用该元组

 
PostgreSQL 的 VACUUM 是清理过期数据、释放磁盘空间的核心命令,主库执行后会同步到备库;如果备库的长查询,还在读取这些即将被清理的「过期数据」,备库重放 WAL 时就会触发冲突。
 

3. 物理复制冲突的「典型现象」

  • 备库日志(postgresql.log)会出现明显的报错关键词:canceling statement due to conflict with recovery
  • 备库的查询被强制终止,正在执行的长 SELECT 直接报错中断
  • 备库的复制延迟突然增大,pg_stat_replication 视图中看到同步延迟数值飙升
  • 极端情况:复制进程被杀死,备库停止同步
 

4. 物理复制冲突的「最优解决方案」(3 个方案,从易到难,根治 + 缓解)

 
都是生产环境实操可用的方案,按优先级排序,建议组合使用:
 

 方案 1:修改备库核心参数(治标,最简单,优先配置)

 
在备库的 postgresql.conf 中修改两个核心参数,重启备库生效,能解决 90% 的物理复制冲突:
# 允许备库把长查询的信息反馈给主库,主库会暂时不清理被备库引用的元组,从根源避免VACUUM冲突
hot_standby_feedback = on

# 设置备库重放WAL时,等待备库长查询结束的最大延迟时间(单位:毫秒),超时才会终止查询
# 建议设为 30000(30秒),给长查询足够的执行时间,避免频繁被终止
max_standby_streaming_delay = 30000
 

方案 2:业务侧优化(治本,最关键)

 
物理复制的冲突,本质是「备库的长查询」引发的,所以减少备库的超长查询,是彻底解决冲突的核心:
 
  • 避免在备库执行耗时超过 10 秒的复杂 SELECT、多表关联、大表全扫;
  • 对备库的查询 SQL 做优化、加索引,缩短查询执行时间;
  • 读写分离的场景下,把批量报表、数据统计这类超长查询,迁移到专门的统计库,不要在备库执行。
 

方案 3:备库参数兜底(极端情况用)

 
如果业务无法优化长查询,可在备库设置:max_standby_streaming_delay = -1,表示备库会无限等待备库的查询结束,再执行同步操作。
 
 注意:这个参数会导致备库的复制延迟无限增大,可能出现备库数据和主库差几个小时的情况,适合「数据一致性要求低、查询优先」的场景。
 

 

四、逻辑复制的冲突:最常见、最复杂,备库可写是核心诱因

 
逻辑复制是 PostgreSQL 复制冲突的重灾区,也是大家问的最多的场景,没有之一。
 

1. 逻辑复制的冲突前提

 
逻辑复制的核心特性:备库是可写的。
 
备库不仅能执行 SELECT,还能正常执行 INSERT/UPDATE/DELETE,主库只会把「指定表的变更」同步到备库,备库本地的写入操作完全不受限制。
 
正是这个特性,让逻辑复制的灵活性拉满,但也让「主库同步的数据」和「备库本地写入的数据」极易产生冲突,只要备库有写入,大概率会遇到冲突。
 

2. 逻辑复制的「4 种核心冲突类型」(全部高频,按发生概率排序)

 
逻辑复制的冲突,是实打实的「数据不一致冲突」,本质是:主库同步过来的行变更操作,在备库执行时,违反了数据库的约束规则或数据唯一性规则,执行失败。所有冲突都基于一个核心:备库的目标表,已经存在相关数据。
 

✔ 冲突 1:唯一键冲突(主键 / 唯一索引,发生概率 99%)【最常见】

 
这是逻辑复制中遇到最多的冲突,没有之一。
 
  • 场景 1:主库插入一条 id=100 的数据,同步到备库时,备库的表中已经有了 id=100 的数据(备库本地写入的),违反主键唯一性,插入失败;
  • 场景 2:主库更新一条数据的唯一键(比如手机号),同步到备库时,备库中已经存在该手机号,更新失败。
 

✔ 冲突 2:更新 / 删除「不存在的行」(发生概率 90%)

 
  • 场景 1:主库更新 id=200 的数据,同步到备库时,备库的表中没有 id=200 的数据,更新操作无目标行,执行失败;
  • 场景 2:主库删除 id=300 的数据,同步到备库时,备库的表中已经没有这条数据,删除操作无目标行,执行失败。
 

✔ 冲突 3:约束冲突(外键 / 非空 / 检查约束,发生概率 50%)

 
  • 场景 1:主库插入一条数据,某个字段为空,但备库的表中该字段设置了「非空约束」,插入失败;
  • 场景 2:主库插入一条带外键的数据,备库的关联表中没有对应的外键值,违反外键约束,插入失败。
 

✔ 冲突 4:数据类型 / 格式冲突(发生概率 30%)

 
主库和备库的同一张表,字段类型 / 长度不一致(比如主库是 varchar (50),备库是 varchar (20)),主库同步的超长数据写入备库时,触发格式错误,执行失败。
 

3. 逻辑复制冲突的「最优解决方案」(核心 + 实操,PG10 + 全支持)

 
逻辑复制的冲突,没有「一键根治」的方案,但 PostgreSQL 从 PG15 版本开始,原生内置了「冲突解决策略」,这是解决逻辑复制冲突的最优核心方案,也是生产环境的标配;PG10-PG14 版本可以通过触发器实现,原理一致。
 

核心方案:创建订阅时,指定「冲突解决策略」(重中之重,必用)

 
逻辑复制的架构是「发布(主库)+ 订阅(备库)」,所有冲突解决策略,都在备库创建订阅时指定,核心思路是:当冲突发生时,指定备库「该怎么处理」这个冲突的同步操作,不需要人工介入,自动解决。
 
PostgreSQL 内置了 6 种最常用的冲突解决策略,覆盖所有业务场景,推荐优先使用前 4 种,语法如下(备库执行):
 
-- 创建订阅时指定冲突解决策略(核心语法)
CREATE SUBSCRIPTION sub_user 
CONNECTION 'host=主库IP port=5432 dbname=test user=postgres password=123456'
PUBLICATION pub_user
WITH (
  copy_data = true,  -- 初始化时复制已有数据
  conflict_handler = 'use_remote'  -- 核心:冲突解决策略,替换成对应值即可
);
 
 

✔ 6 种核心冲突解决策略(按优先级排序,解释通俗易懂)

 
  1. use_remote 【最推荐,生产首选】:主库数据为准。冲突时,直接用主库同步过来的数据,覆盖备库的本地数据。解决 99% 的唯一键冲突,适合「主库是权威数据源,备库只是同步副本」的场景。
  2. use_local :备库数据为准。冲突时,忽略主库的同步操作,保留备库本地的数据。适合「备库有本地业务写入,且备库数据优先级更高」的场景。
  3. skip 【应急首选】:跳过冲突操作。冲突时,直接忽略这条同步指令,继续执行后续的同步。适合「少量冲突不影响业务,不想中断复制」的场景,应急用非常方便。
  4. update :主库更新备库。仅针对更新冲突,冲突时用主库的数据更新备库的对应行。适合「只同步更新操作,插入操作少」的场景。
  5. delete :删除备库冲突数据。冲突时,先删除备库的冲突行,再执行主库的同步操作。适合「数据一致性要求极高,不允许备库有脏数据」的场景。
  6. error 【默认策略】:报错中断复制。冲突时,直接抛出错误,停止复制进程,这是 PG 默认的策略,也是大家最头疼的 —— 只要冲突就中断,需要人工重启。
 

辅助方案:业务侧规范(治本,减少冲突源头)

 
  1. 尽量做到「备库少写 / 不写」:如果业务不需要备库写入,直接把备库的逻辑复制表设为只读,这是彻底避免冲突的终极方案;
  2. 主备库表结构严格一致:字段类型、长度、约束、主键 / 唯一索引,必须完全相同,避免格式 / 约束冲突;
  3. 备库本地写入的主键 / 唯一键,和主库的自增主键「分段使用」:比如主库用 1-10000,备库用 10001-20000,从根源避免唯一键冲突。
 

 

五、复制冲突的「通用排查方法」(所有复制类型通用,必学)

 
不管是物理复制还是逻辑复制,遇到冲突后,最快定位问题的方式,永远是看日志 + 查系统视图,这两个方法组合,能解决所有复制冲突的排查问题:
 

方法 1:查看备库的 PostgreSQL 日志(优先级最高,必看)

 
日志文件路径在备库的 postgresql.conflog_directory 配置,日志中会明确打印冲突的类型、冲突的 SQL 语句、冲突的表名、冲突的行数据,比如物理复制的 canceling statement due to conflict,逻辑复制的 unique constraint violation,都是一眼就能识别的关键词。
 

方法 2:查询 PG 系统视图,监控复制状态(备库执行)

 
这 3 个视图是 PostgreSQL 复制的「核心监控视图」,复制是否正常、是否有延迟、是否有冲突,都能查到:
 
-- 1. 查看复制连接状态:是否同步、延迟多少、备库IP等
SELECT * FROM pg_stat_replication;

-- 2. 查看备库重放WAL的状态:复制延迟、是否有冲突
SELECT * FROM pg_stat_wal_receiver;

-- 3. 查看逻辑复制的订阅状态:是否同步、冲突数量、最后冲突时间
SELECT * FROM pg_stat_subscription;

posted on 2026-01-13 11:15  阿陶学长  阅读(25)  评论(0)    收藏  举报