pgsql内置publication/subscription功能,实现数据同步

publication / subscription 笔记

PostgreSQL 内置的逻辑复制,不需要装任何扩展。


搭建方式

数据库的配置

① 打开逻辑复制 (postgresql.conf,改完必须重启)

conf 复制代码
wal_level = logical
max_replication_slots = 10     # 至少 ≥ 表数 + 几个余量
max_wal_senders = 10

② 不需要装扩展 ------ 内置的。

检查:

sql 复制代码
SHOW wal_level;                 -- 必须 logical,replica 不行
SHOW max_replication_slots;
SHOW max_wal_senders;

角色权限配置

三种身份:

身份 要求 为什么
建 publication 的人 必须是这些表的 owner + 库级 CREATE PG 硬性要求,给权限也不行
建 subscription 的人 pg_create_subscription(PG16+)+ 库级 CREATE
连接账号(CONNECTION 里的 user) REPLICATION 连发布端拉 WAL

建 publication 的人:

sql 复制代码
-- 库级 CREATE(publication 是库级对象)
GRANT CREATE ON DATABASE db1 TO sale_admin;

-- ★ 13 张表必须都是它 owner,否则报 must be owner of table xxx

建 subscription 的人:

sql 复制代码
-- 全局
ALTER ROLE sale_admin WITH REPLICATION;
GRANT pg_create_subscription TO sale_admin;    -- PG 16+ 才有
权限 作用
表 owner 把表加进 publication,必须是 owner
库级 CREATE 建 publication / subscription
REPLICATION 连发布端拉 WAL
pg_create_subscription 建订阅的独立资格(PG16+)

⚠️ 内置复制最大的坑:非超级用户建订阅会被硬拦

复制代码
ERROR: Non-superuser cannot connect if the server does not request a password

原因 :发布端的 pg_hba 如果是 trust(不问密码),PostgreSQL 不允许非超级用户走这条路 ------ 这个检查写在源码里,给什么权限都绕不过。

规避办法(按推荐顺序):

  1. 给连接账号设密码 + pg_hba 用 scram-sha-256(最干净)
  2. 建订阅时用超级用户
  3. 换 pglogical(它没这条检查)

源端需要执行的操作

sql 复制代码
-- ① 建 publication(★ 显式列表,别用 FOR TABLES IN SCHEMA)
CREATE PUBLICATION sale_pub FOR TABLE
    sale.merchants, sale.merchant_creators, sale.shops, sale.brands, sale.members,
    sale.products, sale.shop_brands, sale.opportunities, sale.cooperations,
    sale.samples, sale.opportunity_logs,
    sale.fulfillment_lives, sale.fulfillment_videos;

-- ② 确认 13 张
SELECT count(*) FROM pg_publication_tables WHERE pubname = 'sale_pub';

FOR TABLES IN SCHEMA 需要超级用户,会报 must be superuser to create FOR TABLES IN SCHEMA publication。

同实例跨库要手工建槽(否则 CREATE SUBSCRIPTION 会自锁):

sql 复制代码
SELECT pg_create_logical_replication_slot('sale_sub_slot', 'pgoutput');

同一个实例里自己订阅自己 ------ 标准写法会死锁:

CREATE SUBSCRIPTION 要建槽,而建槽要等订阅自己的事务结束 ------ 互相等。所以必须手工先把槽建好,建订阅时传 create_slot = false。


订阅端需要执行的操作

① 建表 ------ 同名同结构。只留 列名 + 列类型 + NOT NULL + 主键,去掉外键/触发器/默认值/CHECK。

模式名必须和源端一样 ------ 内置复制不能改表名、模式名。

② 建订阅

sql 复制代码
CREATE SUBSCRIPTION sale_sub
  CONNECTION 'host=127.0.0.1 port=5432 dbname=lite_sale_test
              user=datahouse password=xxx sslmode=disable'
  PUBLICATION sale_pub
  WITH (create_slot = false,          -- 槽已手工建好
        slot_name = 'sale_sub_slot',
        copy_data = true);            -- 做一次初始全量

③ ★ 建完立刻确认它落在正确的库

sql 复制代码
SELECT s.subname, d.datname AS 所在库, s.subenabled, s.subslotname
FROM pg_subscription s JOIN pg_database d ON d.oid = s.subdbid;
-- 期望:sale_sub | person_db_test | t | sale_sub_slot

pg_subscription 是共享目录 ------ 你在 db1 查也能看到 db2 的订阅。

判断订阅属于哪个库,必须 join subdbid。

④ 验证 ------ 两库逐张对行数,必须完全一致。


怎么取消同步

顺序不能反:订阅 → 发布 → 槽。

sql 复制代码
-- 【订阅端】★ 先 DISABLE + 断开槽关联,再 DROP
ALTER SUBSCRIPTION sale_sub DISABLE;
ALTER SUBSCRIPTION sale_sub SET (slot_name = NONE);
DROP SUBSCRIPTION sale_sub;

为什么要先 DISABLE + SET slot_name = NONE :

DROP SUBSCRIPTION 会去连发布端删槽。如果连不上(网络断、发布端挂了),

整条命令会失败,订阅也删不掉。这两步先断开关联,DROP 就不需要联网了。

核查:

sql 复制代码
SELECT count(*) FROM pg_subscription;
SELECT count(*) FROM pg_replication_slots;   -- 应该没剩下

只停、留个底:

sql 复制代码
ALTER SUBSCRIPTION sale_sub DISABLE;   -- 恢复用 ENABLE

删发布端:

sql 复制代码
DROP PUBLICATION IF EXISTS sale_pub;

遇到问题怎么排查

固定四步。

第 1 步 · 看订阅状态

sql 复制代码
-- 【订阅端】注意 join subdbid(pg_subscription 是共享目录)
SELECT s.subname, d.datname AS 所在库, s.subenabled,
       (st.pid IS NOT NULL) AS worker在跑
FROM pg_subscription s
JOIN pg_database d ON d.oid = s.subdbid
LEFT JOIN pg_stat_subscription st ON st.subid = s.oid;

第 2 步 · 看每张表的进度

sql 复制代码
SELECT c.relname, sr.srsubstate
FROM pg_subscription_rel sr JOIN pg_class c ON c.oid = sr.relid
ORDER BY sr.srsubstate, c.relname;
-- i=初始化  d=正在拷  f=拷完  s=已同步  r=就绪 ✅

第 3 步 · 看日志第一条 ERROR

bash 复制代码
docker logs 容器名 --since 10m | grep -iE "ERROR|FATAL" | head -5
# 云上:控制台 → 日志管理 → 错误日志

★ pg_subscription 里没有 last_error 这一列,报错只能翻日志。

第 4 步 · 对症处理

第一条 ERROR 原因 怎么修
Non-superuser cannot connect if the server does not request a password 发布端 pg_hba 是 trust 给账号设密码 + pg_hba 改 scram,或用超级用户建订阅
must be owner of table xxx 建 publication 的人不是表 owner 换 owner 建,或用超级用户
must be owner of publication xxx 改 publication 的不是它的 owner 同上
must be superuser to create FOR TABLES IN SCHEMA publication 用了 FOR TABLES IN SCHEMA 改成显式列表
duplicate key ... _pkey 目标表非空,首同步撞主键 TRUNCATE 目标表 → 删订阅重建
订阅 replicating、槽 active,但没数据 发布被清空了 见下
ALTER SUBSCRIPTION ... does not exist 连错库了 join subdbid 确认
CREATE SUBSCRIPTION 卡住不返回 同实例跨库自锁 手工建槽 + create_slot = false

★ 最隐蔽的坑:发布被静默清空

复制代码
DROP SCHEMA xxx CASCADE  或任何 DROP TABLE
→ PostgreSQL 会静默地把表从 publication 里移除 ------ 不报错、不提示

症状(全程不报错):订阅状态正常、槽 active、复制连接 streaming,但目标端永远是空的,pg_subscription_rel 是 0 行。

关键证据:

sql 复制代码
SELECT count(*) FROM pg_publication_tables WHERE pubname='sale_pub';  -- 返回 0

修复两步,缺一不可:

sql 复制代码
-- ① 【源】把表补回发布
ALTER PUBLICATION sale_pub ADD TABLE sale.merchants, sale.shops, ...;

-- ② 【目标】让订阅重新读发布端的表清单
ALTER SUBSCRIPTION sale_sub REFRESH PUBLICATION;

第 ② 步不能省。 apply worker 内部缓存了「空表清单」,

光补发布它不会自己重新读 ------ 必须 REFRESH 踢它一下。


其他需要注意的地方

失败会自己重试(和 pglogical 不一样)

订阅失败后每 5 秒重试一次 ,一直失败一直重试。修掉根因(删冲突行 / 补缺失列),它会自己恢复,不用手工拉。

pglogical 失败几次就放弃,必须手工 alter_subscription_enable 或重建订阅。

apply 以「订阅 owner」的身份跑

数据写进目标表时用的是订阅 owner 的身份,不是建表的人。

pglogical 不一样 ------ 它用的是「节点 DSN 里的用户」。

应用时会关掉触发器和外键

apply 以 session_replication_role = replica 运行:

  • 外键被禁用 ------ 目标端有外键也从不生效
  • 触发器被禁用 ------ 两库 updated_at 精确到微秒一致,说明 trg_*_updated_at 根本没跑
  • 但 CHECK / NOT NULL / UNIQUE 照样生效 ------ 这些会拦住 apply

加表也必须你是 owner

ALTER PUBLICATION ... ADD TABLE 要求你是那张表的 owner。

pglogical 没这个要求 ------ 只要 SCHEMA 的 USAGE 就够。

DDL 完全不传

源端改表结构,目标端不会跟。要手工同步。

加列 会报错好发现;删列是静默的,最难发现。

不能改表名 / 模式名 / 列名

源端是什么,目标端就得是什么。想放 raw 层,在 sale 之上建视图。

目标端必须只读

本地改的数据会被同步静默覆盖,不报错。

孤儿同步槽

复制代码
pg_19126_sync_18484_7688900967435198503 | active = f

首同步时每张表起一个同步 worker,每个建一个临时槽。worker 异常退出会留下孤儿槽,槽位耗尽后全线卡死。

sql 复制代码
-- 先看
SELECT slot_name, active FROM pg_replication_slots WHERE slot_name LIKE 'pg\_%\_sync\_%';

-- 全删(只服务一次性拷贝,没有长期价值)
SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots
WHERE slot_name LIKE 'pg\_%\_sync\_%' AND NOT active;

max_sync_workers_per_subscription 不是订阅参数

sql 复制代码
-- ❌ 会报 unrecognized subscription parameter
CREATE SUBSCRIPTION ... WITH (max_sync_workers_per_subscription = 1);

它是服务器级 GUC(默认 2):

sql 复制代码
ALTER SYSTEM SET max_sync_workers_per_subscription = 1;
SELECT pg_reload_conf();   -- sighup 级别,不用重启

参数建议

参数 建议 说明
wal_level logical 启动参数,改了要重启
max_replication_slots ≥ 表数 + 余量 每张表首同步会开临时槽
max_slot_wal_keep_size 2~5 GB 默认 -1 不限制,订阅挂了 WAL 会堆到磁盘满
max_sync_workers_per_subscription 2(默认) 调成 1 更慢,但槽位不容易爆

槽跟着订阅生命周期

  • CREATE SUBSCRIPTION 成功 → 立刻创建
  • DROP SUBSCRIPTION → 立刻删除

和 pglogical 的关系

内置复制 pglogical
装扩展 不用 要装(第三方)
非超级用户建订阅 ❌ 不行(源码硬拦) ✅ 可以
加表要表 owner ❌ 要 ✅ 不要
apply 身份 订阅 owner 节点 DSN 里的用户
单表重同步 ❌ 只能删订阅重建 ✅ 有函数
序列同步 ❌ ✅
DDL 传播 ❌ ✅
失败自动重试 ✅ 每 5 秒 ❌ 放弃

一句话记忆

顺序 建表 → 建 publication(源)→ 手工建槽 → 建 subscription(目标)
三个身份 publication 建者(表 owner )· subscription 建者(pg_create_subscription)· 连接账号(REPLICATION)
最大的坑 非超级用户 + pg_hba trust = 建不了订阅,绕不过
最隐蔽的故障 发布被静默清空 ------ 订阅正常,就是没数据
失败会自愈 每 5 秒重试,修好根因自动恢复
取消同步 DISABLE → SET slot_name=NONE → DROP SUBSCRIPTION
相关推荐
有想法的py工程师2 小时前
PostgreSQL 字符集双重转义击穿 磁盘 I/O
数据库·postgresql
头发还在的女程序员2 小时前
【无标题】
网络·数据库·短剧小程序·短剧系统·海外短剧·短剧后台
千千寰宇2 小时前
[数据库系统] 数据库系统的设计原理研究
数据结构·数据库·database-opengemini/influxdb·database-dament/达梦
麦壳饼2 小时前
SonnetDB SQL 分页查询:LIMIT/OFFSET 与 FETCH 语法
数据库·sonnetdb
Gauss松鼠会2 小时前
【GaussDB】破除gaussdb ugin索引支持中文模糊查询的迷思-字符序
java·运维·服务器·网络·数据库·gaussdb·经验总结
l1t2 小时前
DeepSeek总结的PostgreSQL因选择而宽松,因偶然而永久
数据库·postgresql
Nturmoils3 小时前
数据同步中断后,KFS 怎么把链路接回来
数据库
leisoo80973 小时前
股票筹码分布怎么用获利比例成本区间与集中度实战 IG50免费开源股票数据API接口
开发语言·jvm·数据库·python·开源
字节跳动的猫3 小时前
LikeShop 商品评价体系二开:追评、晒图审核与评价标签筛选功能开发
运维·数据结构·数据库