跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

golang如何实现数据双写一致性_golang数据双写一致性实现步骤

Go原生database/sql不支持跨库事务回滚,双写失败时需设计幂等、重试、对账等补偿机制,而非依赖内存标记或单点超时控制。 双写失败时,
database/sql
事务无法跨库回滚 Go 原生
database/sql
不支持分布式事务,哪怕两个 MySQL 实例用同一套账号密码,调用
Begin()
也只对单个
*sql.DB
生效。常见错误是:先往 A 库写,再往 B 库写,B 库失败后手动调 A 库的
Rollback()
—— 这看似“回滚”,但网络中断、进程崩溃等场景下,A 库那条记录早已落盘,根本不可逆。 所以不能依赖“事务兜底”,必须从设计上接受“最多一次”或“至少一次”的语义,并配套补偿机制。 优先选“先写主库 + 发消息”模式,而非直连双库写入 若必须同步双写(如迁移期),需在应用层实现幂等 + 重试 + 对账 避免在 HTTP handler 中裸写两个
Exec()
,应封装为原子操作单元并记录操作日志 用
sync.Once
或
atomic.Value
管理双写状态不解决一致性问题 有人试图用
sync.Once
保证“只写一次”,或用
atomic.Value
标记写入完成状态——这只能防并发重复触发,完全不防写入中途失败。比如:A 库成功、B 库超时,此时
atomic.Value
已设为
true
,后续请求直接跳过,导致数据永久不一致。 真正需要的是可核查、可重放的操作记录,而不是内存标记。 立即学习 “ go语言免费学习笔记(深入) ”; 每次双写前,先插入一条
write_log
表,含
id
、
status
(pending)、
payload
(JSON)、
created_at
A 和 B 写入成功后,再更新该 log 的
status = 'done'
独立对账服务定时扫
status = 'pending'
超过 5 分钟的日志,重新投递或告警 用
github.com/go-sql-driver/mysql
配置
timeout
和
readTimeout
防止卡死 双写场景下,一个库响应慢会拖垮整个流程。MySQL 驱动默认无超时,一旦 B 库 hang 住,goroutine 就永远阻塞,连接池耗尽,服务雪崩。 必须显式配置超时参数,且两个库的超时值要错开(避免同时失败):
// A 库:写主,容忍稍高 dbA, _ := sql.Open("mysql", "user:pass@tcp(10.0.1.10:3306)/a?timeout=3s&readTimeout=2s") // B 库:写备,快速失败 dbB, _ := sql.Open("mysql", "user:pass@tcp(10.0.2.20:3306)/b?timeout=1.5s&readTimeout=1s")
超时单位必须是
s
或
ms
,写
3
不生效
timeout
控制连接建立和命令执行总耗时;
readTimeout
单独控制结果读取阶段 不要依赖
context.WithTimeout
包裹
Exec()
—— 驱动内部可能忽略 context(尤其老版本) 用
github.com/Shopify/sarama
发送成功后,仍需本地记录 offset 如果走“先写 DB,再发 Kafka”路径,Kafka 写入成功 ≠ 消费端一定能收到。网络抖动、broker 重启、ISR 缩减都可能导致消息丢失或重复。单纯靠
sarama.SyncProducer
的
SendMessage()
返回成功,只是说明 broker 接收了,不代表已持久化。 更稳妥的做法是:写 DB 成功后,把待发送的 payload 和预期 offset 记到本地表,再发 Kafka;消费端处理完,回调更新该 offset 为
committed
。未 commit 的记录可被定时任务重新推送。 不要把 Kafka 当作唯一事实源,它只是传输通道 offset 记录需带
topic
、
partition
、
msg_id
,否则无法精准重放 若用
confluent-kafka-go
,注意其
ProduceChannel
是异步的,错误需从 channel 里读,不是函数返回值 实际最难的部分不是代码怎么写,而是决定哪边是权威源、冲突时以谁为准、对账周期设多长、失败后人工介入成本是否可控——这些没法靠 Go 语法解决,得和业务方一起画清楚状态流转图。

相关文章