直接用database/sql多写会丢数据,因*sql.DB间无事务协调机制,主库写成功而从库失败即导致不一致;必须用逻辑复制或两阶段提交+补偿任务保障最终一致性。

为什么直接用 database/sql 的多写会丢数据?
Go 原生 database/sql 不提供跨连接的事务一致性保障,多个 *sql.DB 实例各自 commit 时无法保证原子性。常见错误是:先写主库成功、再写从库失败,结果主库已落盘,从库没跟上,造成数据不一致。
真正需要的不是“同时写”,而是“要么全成功、要么全回滚”的语义,这必须靠外部协调或架构层兜底。
- 别指望
tx.Commit()能跨 DB 实例生效 —— 它只对当前*sql.Tx有效 - 用两个
*sql.DB分别开Begin(),然后各自Commit(),本质是两个独立事务,没有分布式事务语义 - 哪怕加了重试逻辑,若重试前进程崩溃,仍可能只写一半
用 pglogrepl + 逻辑复制替代双写
PostgreSQL 用户最稳妥的多写同步路径,其实是放弃应用层双写,改用 WAL 日志解析 + 逻辑复制。主库写一次,变更由 pglogrepl 拉取并投递到目标库,避免应用层事务耦合。
关键点在于:它不依赖 Go 应用发起写请求,而是监听数据库自身产生的变更流,天然具备顺序性和幂等性。
立即学习“go语言免费学习笔记(深入)”;
- 主库开启
wal_level = logical,创建复制槽(replication slot) - 用
pglogrepl.StartReplication()连接并消费 WAL,解析出 INSERT/UPDATE/DELETE 操作 - 把解析后的 SQL 或结构化事件,按需写入目标库(可批量、可过滤表、可转换 schema)
- 消费位点(LSN)必须持久化,重启后从上次位置继续,否则丢变更
示例片段:
conn, _ := pglogrepl.Connect(ctx, "host=localhost port=5432 dbname=postgres user=replicator password=...")
pglogrepl.StartReplication(ctx, conn, "my_slot", pglogrepl.StartReplicationOptions{...})
如果必须双写,用 TwoPhaseCommit 模式 + 补偿任务
MySQL 或不支持逻辑复制的场景下,只能靠应用层模拟两阶段提交:先预写(prepare),再统一确认(commit)或回滚(rollback)。但 Go 标准库无内置支持,需手动维护状态。
核心难点不在代码,而在状态存储和失败恢复 —— prepare 状态必须落盘(如写入一张 tx_log 表),且读写该表本身要可靠。
- 第一阶段:往主库插入业务数据,同时往
tx_log插入一条status='prepared'记录 - 第二阶段:尝试往从库写相同数据;成功则更新
tx_log.status为'committed',失败则设为'rolled_back' - 独立后台 goroutine 定期扫描
tx_log中超时未完成的'prepared'记录,触发补偿(查主库确认、重试从库或告警) - 所有操作都需幂等:从库写入前先
SELECT FOR UPDATE或用INSERT ... ON CONFLICT DO NOTHING
别忽略网络分区下的行为边界
无论选哪种方案,都要明确回答:当主库写成功、但同步链路中断时,系统是否允许读从库旧数据?是否接受短暂不一致?这些不是技术问题,而是业务 SLA 决定的。
比如金融类场景要求强一致,那逻辑复制延迟必须监控并告警;而内容类场景容忍秒级延迟,就可以用 pglogrepl 默认配置,不额外做位点校验。
最容易被忽略的是:日志解析服务挂掉期间,WAL 文件堆积导致磁盘满,进而使主库拒绝写入 —— 所以必须配 max_replication_slots 和 archive_timeout,并监控 pg_replication_slots.active。


















