qinghuan
← 回到知识探索

后端现场 / 可下载小项目

订单写成了,消息却没发:动手做 outbox

用两个临时 SQLite 数据库观察提交、重试和重复投递,弄清每次失败究竟留下了什么。

展开本文目录
  1. 下载完整练习包
  2. 把订单和待发送的事一起保存
  3. 重试订单和重投消息,用两种身份
  4. 处理成功了,确认却可能没记下来
  5. 在 Windows 上运行并对照结果
  6. 本地去重有明确范围
  7. 记住这三点
  8. 别把例子用过头
  9. 自己试一小步
  10. 查原始资料
  11. 找一份资料继续练
开始前,会这些就够了
  • 能运行 Python 文件
  • 知道表和一行数据是什么意思
遇到不熟悉的词,先看这 2 个解释
幂等
在约定的范围内,把同一个请求做一次或重复做多次,对目标业务产生的预期效果相同;它用来防止重复执行把一件事变成额外多办几次。
事务
把一组数据库操作放进同一个提交范围,让它们作为一件事被确认或撤销,并按数据库规则控制其他事务看到的变化;它帮助处理做到一半时的失败。
打开完整术语小抄 ↗

假设你在文具店下单,程序先保存订单,再通知后面的处理程序。如果保存成功后突然中断,订单还在,通知却没发。把两行代码挨着写,并不能让两个动作一起成功。

这个可下载练习用假设订单演示 outbox,也就是存放待发送事件的表。我们会故意在几个位置抛出异常,再检查数据库。无需账号、网络或第三方依赖,完整源码放在练习包里。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 3 个文件
包里有什么
  • demo.py:失败与恢复演示
  • verify.py:自动验证
  • README.md:中文操作步骤

在这个文件夹打开 PowerShell,逐行运行

python --version
python demo.py
python verify.py

先观察演示输出,再运行验证。订单和消息都是示例数据;数据库由程序在临时目录中创建,正常结束后清理,每次重跑从头开始。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

把订单和待发送的事一起保存

发送者数据库有订单表和 outbox 表。同一个事务先写订单,再写“这个订单已创建”的事件,最后提交。事务把这些写入作为一组处理:提交前失败就一起回滚,发送程序随后只读取已提交的事件。

注意,保存待发送事件还没有完成通知。事件提交以后,需要另一个步骤读取并处理它。我们用直接调用消费者函数代替真实队列,让你能逐步看到哪里成功、哪里还没完成;这不是生产队列或分布式部署。

重试订单和重投消息,用两种身份

request_id 是业务请求的身份。同一次购买没有收到响应,重试时仍用原来的键;确实再买一次才换键。本实验先用一个短事务把键和规范化后的商品、数量绑定,再开始订单事务。

因此,订单提交前失败时,会出现“键已经登记,订单和事件都没有”的状态。原参数可以继续执行,换参数就拒绝,订单存在前后都一样。保留失败请求的键是本练习的规则,不表示登记成功就等于下单成功。

event_id 是已经生成的那条事件的身份,写入 outbox 时确定,以后重投也不变。消费者按它检查是否处理过,不能每次投递都重新生成身份。本例每单只有一种事件;真实业务有多种事件时还要区分类型或版本。

处理成功了,确认却可能没记下来

消费者使用另一个临时数据库,把 inbox 已处理登记和本地效果写在同一个事务里。效果只是表中的一行假设数据。消费提交前失败,两者回滚;以后再来相同事件,还能重新处理。

更容易误判的是消费已经提交,发送者却尚未把事件标记为已确认。实验在这里中断,再关闭并重开数据库。发送者仍看到待处理事件,会重新调用消费者;消费者找到原登记,跳过重复的本地效果,然后发送者补上确认。

订单提交后丢失响应也是相似的观察盲区:调用者收到异常,不能据此断言订单已经回滚。练习会用原 request_id 重试,让你对照返回的身份与表中行数。

在 Windows 上运行并对照结果

下载练习包并全部解压,进入同时有 demo.py、verify.py、README.md 的文件夹。打开 PowerShell,用 Set-Location -LiteralPath '你的完整文件夹路径' 进入该目录;单引号保留,内容换成实际路径。

先执行 python --version,确认是 Python 3.10 或更新版本,再运行下方演示。最后执行 python verify.py,检查十五项测试均为 ok、末尾为 OK。README 解释每一行输出,也提供找不到命令或文件时的检查步骤。

所有数据库都在本次运行创建的临时目录中。程序正常退出会关闭连接并清理文件,下次运行重新开始;没有选取用户数据库的入口。中断由异常模拟,并未测试断电或强杀进程。

小实验 · text
python demo.py

怎样运行:先下载解压练习包,在包含 demo.py 的文件夹打开 PowerShell;需要 Python 3.10+。

运行后对照结果
Temporary SQLite files; no network.
before commit failed: orders=0 outbox=0 reserved_keys=1
same key, different parameters before order: rejected
retry same request: order=order:req-001 events=1
same request again: orders=1 outbox=1
same key, different parameters after order: rejected
client response lost: committed_orders=2
retry after lost response: orders=2 outbox=2
delivery acknowledgement lost: local_effects=1 pending=2
after reconnect: pending=2 local_effects=1
recovery delivery: order-created:req-001 local_effect=skipped
recovery delivery: order-created:req-002 local_effect=applied
finished: orders=2 pending=0 inbox=2 local_effects=2
temporary files cleaned up: True

本地去重有明确范围

数据库唯一约束负责挡住重复身份,事务负责把相关写入一起提交。源码显式声明非空、启用外键,并用 BEGIN IMMEDIATE 开始写事务;这仍可能遇到锁争用,不代表任意数量的程序都能同时写入。

如果把本地效果换成发邮件或调用支付接口,那个外部动作不在消费者的 SQLite 事务里,不能照搬本例就声称自动去重。事件仍会重复到达、处理函数仍会再次运行;这里展示的是有条件的本地效果去重,不是通用的 exactly-once 保证。

记住这三点

  • 订单与待发送事件一起提交,能避免本例中的订单留下而事件漏记。
  • 请求重试沿用 request_id,消息重投沿用 event_id,两者解决的重复不同。
  • 先确认失败位于哪个提交点,再判断哪些数据需要恢复。

别把例子用过头

  • 请求键登记和订单写入是两次事务,只有登记记录不能算下单成功。
  • 本地效果与已处理登记同事务的保证,不自动覆盖邮件等外部副作用。
  • 临时文件重连演示不等于真实进程崩溃、断电或多机部署测试。

自己试一小步

增加一个失败恢复测试,让第一次消费在提交前失败,再重复投递同一事件,最后处理另一笔订单的事件。

  1. 沿用 verify.py 的临时目录测试类,准备两个不同请求键的假设订单。
  2. 在每次消费前后记录待确认事件、已处理登记和本地效果的数量。
  3. 分别跟踪请求键与事件身份,写出你判断可以重试的依据。

做到这些,就算完成

  • 新增测试有明确断言,并且不接入真实数据库或网络。
  • 能够说明每个失败点前已提交的内容,而不是只看是否抛出异常。
  • 改变同键参数时,原请求和原事件内容没有被悄悄覆盖。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • 事务发件箱模式 (新标签页)↗AWS 规范性指导 · 中文

    订单与待发送事件同事务、发送与确认之间的重复消息风险及消费端去重需求。

    中文正文公开可读;本文只借用模式的机制,不要求使用 AWS 产品。

  • Transaction (新标签页)↗SQLite · 英文

    显式事务、BEGIN IMMEDIATE、提交与回滚,以及存在其他写事务时的限制。

    SQLite SQL 文档;锁等待或提交失败仍须由应用处理。

  • CREATE TABLE (新标签页)↗SQLite · 英文

    主键、唯一与非空约束,尤其普通文本主键的历史空值兼容行为。

    本练习显式写出 NOT NULL,避免把 SQLite 普通文本主键误当作自动非空。

  • sqlite3 — DB-API 2.0 interface for SQLite databases (新标签页)↗Python 官方文档 · 英文

    Python 3.10 的 sqlite3 接口、显式事务控制、SQL 参数绑定和连接关闭。

    固定为 Python 3.10 文档;练习实际验证环境为 Python 3.14.0,未声称已遍历所有支持版本。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。