当前位置: 首页 > news >正文

多区域主动-主动(PostgreSQL 逻辑复制 + 冲突解决)在 ABP 的落地

多区域主动-主动(PostgreSQL 逻辑复制 + 冲突解决)在 ABP 的落地 🚀


📚 目录

  • 多区域主动-主动(PostgreSQL 逻辑复制 + 冲突解决)在 ABP 的落地 🚀
    • 0) 摘要(TL;DR)🧭
    • 1) 选型与边界 🎯
    • 2) 架构全景图 🗺️
      • 2.1 分区式主动-主动(推荐多数 SaaS)
      • 2.2 真多主(双活/多活)
    • 3) PostgreSQL 配置要点 ⚙️
    • 4) Schema/副本标识与过滤(强约束!)🔒
    • 5) 路线 A:分区式主动-主动(工程模板)🧰
      • 5.1 发布端(区域 A:租户 `A%`)
      • 5.2 订阅端(区域 B:只读副本)
      • 5.3 ABP 多租户路由(连接串解析,异步·保留回退链路)🧩
      • 5.4 网关(YARP)+ 主动健康检查 🛡️
    • 6) 路线 B:真多主(双向订阅 + 冲突治理)🔁
      • 6.1 互订防回环(上线互订“黄金组合”)
      • 6.2 冲突行为(PG 原生)与观测
    • 7) DDL/Schema 变更 🧱
    • 8) 观测与 SLO(最小查询)📊
    • 9) 故障切换/演练 SOP(分区式)🧪
    • 10) 可复现环境(Docker Compose + SQL)🧪
    • 11) ABP 侧最佳实践(小结)🧩
    • 12) 性能与稳定性清单 🏎️


0) 摘要(TL;DR)🧭

  • 分区式主动-主动(主推 ✅)
    TenantId 将租户路由到各自“主区域-主库”;跨区仅订阅只读副本(逻辑复制)。注意:

    1. TRUNCATE 不受行/列过滤影响
    2. 发布 UPDATE/DELETE 时,过滤列必须在副本标识(Replica Identity)中;
    3. PG16 起可 streaming=parallel;PG16 起有 origin 防回环;PG17 起可 failover=true(与物理 HA 的 failover slot 协同)。
  • 真多主(双活/多活 🔁)
    双向订阅 + 表级冲突策略。PG 原生不自动合并冲突(缺失行的 UPDATE/DELETE跳过,PG18 起可被统计/写日志);如需自动合并,选用 pglogicalEDB PGD(BDR)LWW(update_if_newer) 等策略,并开启 track_commit_timestamp 作为参考。


1) 选型与边界 🎯

  • PG 原生逻辑复制:发布/订阅;PG15+ 支持行过滤/列清单不复制 DDL/大对象/序列值;多主场景无内置冲突合并(需要流程/应用兜底)。
  • pglogical 扩展:多源合并 + 可配置冲突(last_update_wins/keep_local/...),LWW 需 track_commit_timestamp=on
  • EDB PGD(BDR):企业级多主,默认 update_if_newer(LWW),具备冲突统计/工具。

实战建议:SaaS 多租户优先“分区式主动-主动”;真多主仅在确需“任意区可写”时小范围试点,并严格控表(只选天然幂等/CRDT 友好表)。


2) 架构全景图 🗺️

2.1 分区式主动-主动(推荐多数 SaaS)

tenant A*
tenant B*
publication: A* rows
publication: B* rows
Users / Clients 🌏
YARP 🧭
ABP App 🧩 (ICurrentTenant + IConnectionStringResolver)
PG Region A 🟦(A* 主库)
PG Region B 🟩(B* 主库)

要点:每个租户仅在自己的主区写入;跨区只读订阅,无跨主冲突TRUNCATE 跨区直达⚠️。


2.2 真多主(双活/多活)

在这里插入图片描述

要点:双向订阅时在双方显式设置 origin='none'(防回环),并在“已对齐数据”场景下 copy_data=false 上线互订(见 §6.1)。


3) PostgreSQL 配置要点 ⚙️

  • 基础wal_level=logical;合理设置 max_replication_slots / max_wal_senders

  • 提交时间戳track_commit_timestamp=on(供 LWW/取证用;有一定开销,且非严格全序)。

  • 订阅关键选项(PG16–18)

    • streaming=parallel(PG16 引入;PG18 默认 parallel,仍建议显式设置以消除版本歧义);
    • synchronous_commit=off(订阅端默认,有利于降低发布端提交等待);
    • origin=none(互订防回环;默认 any);
    • failover=truePG17+,与物理 HA 的 failover slot 协同)。
CREATE SUBSCRIPTION 关键选项(PG16–18)⚙️
streaming = parallel(PG16+,PG18 默认)
origin = 'none'(双向防回环)
synchronous_commit = off(订阅默认)
failover = true(PG17+)

4) Schema/副本标识与过滤(强约束!)🔒

  • 行过滤 + UPDATE/DELETEWHERE 表达式涉及的列必须属于副本标识(Replica Identity);否则复制会失败/不一致。
  • 列清单/行过滤对 TRUNCATE 无效:一旦发布,订阅端整表生效⚠️。

推荐建模(Tenant 入主键/唯一索引)

CREATE TABLE orders (tenant_id  text    NOT NULL,order_id   uuid    NOT NULL,amount     numeric(18,2) NOT NULL,status     text    NOT NULL,updated_at timestamptz NOT NULL DEFAULT now(),PRIMARY KEY (tenant_id, order_id)
);-- 如果不是复合主键,也可用唯一索引(列需 NOT NULL)并指定为副本标识:
-- CREATE UNIQUE INDEX uq_orders_tid_oid ON orders(tenant_id, order_id);
-- ALTER TABLE orders REPLICA IDENTITY USING INDEX uq_orders_tid_oid;
ORDERSstringtenant_idPKstringorder_idPKfloatamountstringstatusdatetimeupdated_at

主键/ID:跨区建议 UUIDv7/ULID 或“区域前缀+序列”;避免序列碰撞与切主 setval() 负担(PG18 可 uuidv7();.NET 9 可 Guid.CreateVersion7())。


5) 路线 A:分区式主动-主动(工程模板)🧰

5.1 发布端(区域 A:租户 A%

CREATE PUBLICATION pub_orders_a
FOR TABLE orders
WHERE (tenant_id LIKE 'A%')
WITH (publish = 'insert, update, delete, truncate');  -- TRUNCATE 仍会整表复制

5.2 订阅端(区域 B:只读副本)

CREATE SUBSCRIPTION sub_a_to_bCONNECTION 'host=a.db port=5432 dbname=app user=rep password=***'PUBLICATION pub_orders_aWITH (copy_data = true, streaming = parallel, synchronous_commit = off);

5.3 ABP 多租户路由(连接串解析,异步·保留回退链路)🧩

继承 DefaultConnectionStringResolver,先尝试“{Name}__{Region}”命名的连接串,找不到再回退{Name}(保留 ABP 的模块级命名/默认连接串语义)。

using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Data;
using Microsoft.Extensions.Options;[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IConnectionStringResolver))]
public class TenantDbConnectionStringResolver : DefaultConnectionStringResolver
{private readonly ICurrentTenant _currentTenant;public TenantDbConnectionStringResolver(IOptionsMonitor<AbpDbConnectionOptions> abpDbConnOptions,IOptionsSnapshot<ConnectionStrings> appConnStrings,ICurrentTenant currentTenant): base(abpDbConnOptions, appConnStrings){_currentTenant = currentTenant;}public override async Task<string> ResolveAsync(string? connectionStringName = null){var name = string.IsNullOrWhiteSpace(connectionStringName) ? "Default" : connectionStringName;var tenantId = _currentTenant.Id?.ToString() ?? "host";var region = ResolveRegionFromTenant(tenantId); // 你的逻辑:缓存/配置中心/DB 映射var key = $"{name}__{region}";                  // 例如:Default__A / Default__Bvar cs  = await base.ResolveAsync(key);return string.IsNullOrWhiteSpace(cs) ? await base.ResolveAsync(name) : cs;}private static string ResolveRegionFromTenant(string tenantId)=> tenantId.StartsWith("A", StringComparison.OrdinalIgnoreCase) ? "A" : "B";
}

5.4 网关(YARP)+ 主动健康检查 🛡️

"Clusters": {"abp-api": {"Destinations": { "a": { "Address": "https://api-a/" }, "b": { "Address": "https://api-b/" } },"HealthCheck": { "Active": { "Enabled": true, "Interval": "00:00:10", "Timeout": "00:00:02", "Path": "/health" } }}
}

6) 路线 B:真多主(双向订阅 + 冲突治理)🔁

6.1 互订防回环(上线互订“黄金组合”)

-- ✓ 先离线或一次性脚本完成两端数据对齐,再启动实时流
-- A 订 B:只接收 B 的“本地起源”事务;上线互订时建议不要 copy 首批
CREATE SUBSCRIPTION sub_b_to_aCONNECTION 'host=b.db port=5432 dbname=app user=rep password=***'PUBLICATION pub_all_bWITH (copy_data = false, origin = 'none', streaming = parallel);-- B 订 A 同理

ℹ️ 小贴士:copy_data=true + origin='none' 在互订场景可能出现“起源不明”告警;已对齐数据后互订,建议 copy_data=false

6.2 冲突行为(PG 原生)与观测

  • 缺失行UPDATE/DELETE:订阅端跳过(不报错);PG18 起会记录并在 pg_stat_subscription_stats 计数。
  • 唯一键冲突:按类型计数(insert_exists/update_exists 等)。
  • 自动合并:使用 pglogical/PGDLWW(需 track_commit_timestamp=on),仅适合幂等覆盖类表(如资料/配置)。

7) DDL/Schema 变更 🧱

PG 原生不复制 DDL:需统一迁移流程(一般“先订阅端加列→后发布端;删除相反顺序”)。涉及副本标识/过滤列务必谨慎,避免复制中断。


8) 观测与 SLO(最小查询)📊

-- 订阅端延迟/健康(逻辑复制)
SELECT subname, pid, received_lsn, latest_end_lsn,last_msg_send_time, last_msg_receipt_time,EXTRACT(EPOCH FROM (now() - latest_end_time)) AS apply_delay_s
FROM pg_stat_subscription;-- PG18+ 冲突/错误统计(可重置)
SELECT subname,apply_error_count, sync_error_count,confl_insert_exists, confl_update_exists, confl_update_missing,confl_delete_missing, confl_update_origin_differs, confl_delete_origin_differs
FROM pg_stat_subscription_stats;
-- 重置统计(需要超级用户)
-- SELECT pg_stat_reset_subscription_stats(NULL);

9) 故障切换/演练 SOP(分区式)🧪

User 👤YARP 🧭ABP 🧩PG A 🟦PG B 🟩Request (X-TenantId=A-1001)Route AWriteLogical replication → B(只读)Region A down ⚠️Freeze writes(A*)临时只读或提升为写主Reconcile (Outbox/CDC)Switch back to AUser 👤YARP 🧭ABP 🧩PG A 🟦PG B 🟩

TRUNCATE 警示框 ⚠️

  • TRUNCATE 不看过滤/列清单 → 跨区整表生效
  • SOP:变更窗口 + 双人复核 + 暂停订阅或在所有订阅方一致执行 + 恢复前二次核验。

10) 可复现环境(Docker Compose + SQL)🧪

docker-compose.yml(PG18 示例;PG16/17 可切镜像)

services:pg_a:image: postgres:18environment: { POSTGRES_PASSWORD: pgpass }command: >postgres -c wal_level=logical-c max_wal_senders=10-c max_replication_slots=10-c track_commit_timestamp=onports: ["5433:5432"]pg_b:image: postgres:18environment: { POSTGRES_PASSWORD: pgpass }command: >postgres -c wal_level=logical-c max_wal_senders=10-c max_replication_slots=10-c track_commit_timestamp=onports: ["5434:5432"]

初始化(两边同构表 + 发布/订阅)

-- A / B 同步执行
-- PG18 可用 uuidv7();若为 PG16/17,可换 gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS pgcrypto;CREATE TABLE orders(tenant_id  text NOT NULL,order_id   uuid NOT NULL DEFAULT uuidv7(),amount     numeric(18,2) NOT NULL,status     text NOT NULL,updated_at timestamptz NOT NULL DEFAULT now(),PRIMARY KEY (tenant_id, order_id)
);-- A 发布:A% 租户
CREATE PUBLICATION pub_orders_a
FOR TABLE orders WHERE (tenant_id LIKE 'A%')
WITH (publish='insert,update,delete,truncate');-- B 发布:B% 租户
CREATE PUBLICATION pub_orders_b
FOR TABLE orders WHERE (tenant_id LIKE 'B%')
WITH (publish='insert,update,delete,truncate');-- B 订 A(只读副本)
CREATE SUBSCRIPTION sub_a_to_b
CONNECTION 'host=pg_a port=5432 dbname=postgres user=postgres password=pgpass'
PUBLICATION pub_orders_a
WITH (copy_data=true, streaming=parallel, synchronous_commit=off);

若验证真多主:两边发布可不加过滤(或互写租户),并以 origin='none' + copy_data=false 建立双向订阅(先离线对齐首批数据)。


11) ABP 侧最佳实践(小结)🧩

  • 连接串解析ICurrentTenant + 自定义 IConnectionStringResolver(继承默认实现,保留回退链路),按“租户→区域”映射到 {Name}__{Region}
  • API 网关:YARP 注入/透传 X-TenantId,开启主动健康检查与必要的 Header/Path 变换。
  • 领域建模:把 TenantId 放入主键/唯一索引副本标识;软删墓碑避免“删除复活”。

12) 性能与稳定性清单 🏎️

  • streaming=parallel(PG16+;PG18 默认)+ 调整 max_parallel_apply_workers_per_subscription,并保证 max_logical_replication_workers 总池足够。
  • 订阅端 synchronous_commit=off(默认),降低发布端提交等待。
  • 尽量用 PK/唯一索引做副本标识,少用 REPLICA IDENTITY FULL
  • pg_stat_subscription_stats冲突类型计数+阈值告警;配合差异报表与回放脚本闭环。
http://www.dtcms.com/a/470465.html

相关文章:

  • 东莞精密机械制造工厂如何10个SolidWorks共用一台服务器资源
  • 六盘水市住房和城乡建设局网站wordpress 淘宝客app
  • Google 智能体设计模式:推理技术
  • 恒丰建设集团有限公司 网站嘉兴提高网站排名
  • 奥远网站建设流程怎做不下网站刷枪
  • VBA即用型代码手册:保存为PDF文件SaveAs PDF
  • 【环境配置】Windows上安装(升级)Cuda11.6 + cudnn9.8 + pytorch 并测试
  • 国内网页设计网站建设建立wordpress网站吗
  • 400电话网络推广微信网站推广方法视频
  • Memcached append 命令详解
  • [Android soong构建系统]实例:定制化编译某个模块
  • 学院网站建设实例做厨具公司网站
  • ceph 数据落盘异常问题分析
  • 福州网站搭建aws 高可用 WordPress
  • 微网站开发方案wordpress英文博客模板下载
  • 为什么很多公司没自己的网站php网站在线打包源码
  • 厚街做网站的公司wordpress新闻
  • 肖特基二极管作用及应用
  • Debezium系列之:SQL Server 事务日志
  • 建设网站源码高权重网站代做排名
  • C++:set和map详解版
  • 【Docker】docker存储配置与管理
  • 网站设计结果怎么做电视台网站
  • 廉江网站制作制作静态网站制作
  • 纯知识干货vue2学习之问答六
  • dnf做心悦宠物的网站官方模板关键字生成的代码添加在网站的什么地方?
  • 主流视频各种压缩码对比
  • 如何确定网站栏目静态网站开发工具有哪些
  • commons-collections4
  • (13)100天python从入门到拿捏《目录操作》