惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
爱范儿
爱范儿
H
Help Net Security
Last Week in AI
Last Week in AI
The Cloudflare Blog
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
IT之家
IT之家
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
Google DeepMind News
Google DeepMind News
B
Blog
C
Check Point Blog
T
Tailwind CSS Blog
云风的 BLOG
云风的 BLOG
D
Docker
Recent Announcements
Recent Announcements
Vercel News
Vercel News
博客园 - 聂微东
阮一峰的网络日志
阮一峰的网络日志
MyScale Blog
MyScale Blog
The GitHub Blog
The GitHub Blog
Stack Overflow Blog
Stack Overflow Blog
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
月光博客
月光博客
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
B
Blog RSS Feed
The Register - Security
The Register - Security
V
Visual Studio Blog
F
Full Disclosure
Hugging Face - Blog
Hugging Face - Blog
T
Threat Research - Cisco Blogs
Latest news
Latest news
PCI Perspectives
PCI Perspectives
Cisco Talos Blog
Cisco Talos Blog
博客园 - Franky
D
DataBreaches.Net
A
Arctic Wolf
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
Google Developers Blog
P
Palo Alto Networks Blog
Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
T
Tenable Blog
L
LINUX DO - 热门话题
Spread Privacy
Spread Privacy

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
读:双写问题——@Transactional 给不了的跨系统一致性 - 暗无天日
2026-05-05 · via 暗无天日

开篇:一段看着安全的代码

有个下单服务:保存订单到数据库,然后向消息队列发一个事件。下游的库存服务、通知服务都等着这个消息。

function placeOrder(order):
    db.save(order)
    queue.send("order.placed", order)

如果 db.save 成功但 queue.send 失败,或者反过来,会怎样?这就是双写问题(Dual Write Problem),分布式系统中最常见的陷阱之一。作者 Vineet Bhatkoti 在 DZone 上的文章讲得很清楚,这篇记下核心思路。

问题本质:两个系统,一个事务?

很多人的第一反应是"加个事务不就行了"。在 Java/Spring 里会写成 @Transactional ,在 Python 里可能用 with db.transaction() 。但这种事务只能保证数据库操作在一个事务中,消息队列不在其中。

这不是某个框架的缺陷,而是物理约束:数据库和消息队列是两台独立的机器,没有共享的事务协调器。代码在一个事务里既做数据库操作又发送消息,但 @Transactional 只能知道数据库做了什么,对消息队列一无所知。

考虑一下下面两个场景:

场景一:消息发出去了,数据库回滚了

function placeOrder(order):
    db.startTransaction()
    db.save(order)             # 数据库写入成功
    queue.send("placed", order) # 消息队列确认收到
    # 假设 JVM/进程在这里崩溃
    # 数据库事务尚未提交,恢复后回滚
    # 但消息队列已经发出去了
    db.commit()                # 永远不会执行到

下游消费者收到了订单事件,去查数据库发现订单不存在。两边都不知道对方的情况。

场景二:数据库提交了,事件丢了

function placeOrder(order):
    db.startTransaction()
    db.save(order)             # 数据库写入成功
    db.commit()                # 事务提交成功
    queue.send("placed", order) # 消息队列节点崩溃,发送失败

订单存在数据库里,但库存没扣、通知没发、仓库没拣货。

那么 XA 分布式事务可以解决问题吗?

XA(两阶段提交)的思路是引入一个协调者,分两个阶段来协调多个资源(数据库、消息队列等):

  • Prepare 阶段 :协调者问每个参与者"能不能提交?"。参与者做完整套操作、锁住资源,然后回复"准备好了"(或者"失败了")
  • Commit 阶段 :如果所有人都说"准备好了",协调者宣布"提交";只要有一人说"失败",就宣布"回滚"

听起来可行,但三个问题让它不适合现代架构:

  • :prepare + commit 两个阶段每个都要来回确认,延迟显著增加
  • 脆弱 :参与者"准备好"之后锁着资源等协调者的最终决定。如果协调者在 prepare 和 commit 之间崩溃,所有参与者都卡住了——它们不知道应该提交还是回滚,也不敢释放锁,只能等着人工来一个个检查状态并清理
  • 不兼容 Kafka :Kafka 的事务模型是内部使用的,不参与 XA 协调

这三个限制让 XA 在事件驱动架构里基本被排除。

四种解决模式

有四种模式可以选。核心思路都一样:把"双写"变成"单写 + 异步分发"。

模式一:Transactional Outbox(事务性发件箱)

最常见的模式。不直接写消息队列,而是在同一数据库事务里写一个 outbox 表。后台进程定期扫描 outbox,把未发送的事件发出去。

function placeOrder(order):
    db.startTransaction()
    db.save(order)
    db.save("outbox", { topic: "order.placed", payload: order })
    db.commit()
    # 只写了一处——数据库
    # 消息队列的事交给后台

# 后台进程
function publishOutbox():
    while true:
        events = db.findUnpublished()
        for event in events:
            queue.send(event.topic, event.payload)
            db.markPublished(event)
        sleep(100ms)

可能存在多次投递的情况,下游消费者需要做幂等处理来应对可能的重复消息。

模式二:CDC(变更数据捕获)

Transaction Outbox 需要手动维护 outbox 表,CDC 更彻底:直接用数据库的事务日志。应用只管写业务表,一个 CDC 管道(如 Debezium)监控事务日志,把变更自动推给消息队列。

function placeOrder(order):
    # 必须在一个数据库事务内执行,CDC 依赖事务日志
    db.save(order)
    # 应用完全不知道消息队列的存在
    # CDC 管道从事务日志中读到变更,推给消息队列

应用代码完全不知道消息队列的存在,没有耦合。代价是多了一个 CDC 管道需要维护。

模式三:Event Sourcing(事件溯源)

这一种思路更彻底:取消业务数据库,不保存"当前状态",只追加事件。当前状态由回放事件历史得出。

function placeOrder(order):
    event = { type: "OrderPlaced", data: order }
    eventStore.append(event)
    # 只有一次写入,写入目标只有一个
    # 消息队列的事由后台进程处理

注意一个常见的误区:不要在 append 之后直接 publish,那样又回到了双写问题。

# 错误做法——双写问题重现
function placeOrder(order):
    eventStore.append(event)     # 成功
    eventBus.publish(event)      # 失败——事件丢了

# 正确做法——只写一处
function placeOrder(order):
    eventStore.append(event)

# 后台进程负责分发
function publishEvents():
    events = eventStore.findUnpublished()
    for event in events:
        eventBus.publish(event)
        eventStore.markPublished(event)

Event Sourcing 的代价是架构复杂得多,不熟悉这种模式的团队上手需要时间。

Event Sourcing 和 Transactional Outbox 很类似,都是"写一次 + 后台进程分发"的模式。区别在于数据的组织方式不同

模式四:Listen to Yourself(自己监听自己)

这一种反过来:应用程序只写消息队列,然后监听自己的消息来更新数据库。

function placeOrder(order):
    queue.send("order.placed", order)
    # 注意:这里没有数据库操作
    # 数据库的更新由下面的消费者异步完成

@onMessage("order.placed")
function handleOrderPlaced(order):
    db.save(order)

调用 placeOrder 时只写了一个地方(消息队列),不存在双写问题。消息被 Kafka 确认持久化后,就算服务立即崩溃也不会丢。重启后会从 Kafka 消费到这条消息,正常更新数据库。

代价是写入后立即查询可能读不到最新状态——数据库还没更新。另外消费者必须做幂等。Kafka 会给每条消息一个编号(偏移量),消费者每处理完一条消息需要告诉 Kafka"这条我处理完了"。如果处理完之后、汇报之前崩溃了,Kafka 就会以为这条还没处理过,重启后又会再发一次。不做幂等就会产生重复订单。

选型思路

原文作者给出了一个对比表:

模式 复杂度 最适合的场景
Transactional Outbox 通用微服务
CDC 高吞吐系统
Event Sourcing 合规审计驱动
Listen to Yourself 简单事件流

大部分场景从 Transactional Outbox 开始就够了。如果 downstream 已经有 CDC 基础设施,CDC 是更干净的选择。

总结

双写问题的根因是一个逻辑操作跨了两个物理系统。数据库和消息队列是两台独立的机器,数据库事务只管得到数据库那一端,管不了消息队列。每种解决模式本质都是在做同一件事:把"同时写两个系统"变成"只写一个系统,另一个由后台异步处理"。