开始前,会这些就够了
- 能运行 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 解释每一行输出,也提供找不到命令或文件时的检查步骤。
所有数据库都在本次运行创建的临时目录中。程序正常退出会关闭连接并清理文件,下次运行重新开始;没有选取用户数据库的入口。中断由异常模拟,并未测试断电或强杀进程。
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,两者解决的重复不同。 - 先确认失败位于哪个提交点,再判断哪些数据需要恢复。
别把例子用过头
- 请求键登记和订单写入是两次事务,只有登记记录不能算下单成功。
- 本地效果与已处理登记同事务的保证,不自动覆盖邮件等外部副作用。
- 临时文件重连演示不等于真实进程崩溃、断电或多机部署测试。
自己试一小步
增加一个失败恢复测试,让第一次消费在提交前失败,再重复投递同一事件,最后处理另一笔订单的事件。
- 沿用
verify.py的临时目录测试类,准备两个不同请求键的假设订单。 - 在每次消费前后记录待确认事件、已处理登记和本地效果的数量。
- 分别跟踪请求键与事件身份,写出你判断可以重试的依据。
做到这些,就算完成
- 新增测试有明确断言,并且不接入真实数据库或网络。
- 能够说明每个失败点前已提交的内容,而不是只看是否抛出异常。
- 改变同键参数时,原请求和原事件内容没有被悄悄覆盖。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- 事务发件箱模式 (新标签页)↗
订单与待发送事件同事务、发送与确认之间的重复消息风险及消费端去重需求。
- Transaction (新标签页)↗
显式事务、BEGIN IMMEDIATE、提交与回滚,以及存在其他写事务时的限制。
- CREATE TABLE (新标签页)↗
主键、唯一与非空约束,尤其普通文本主键的历史空值兼容行为。
- sqlite3 — DB-API 2.0 interface for SQLite databases (新标签页)↗
Python 3.10 的 sqlite3 接口、显式事务控制、SQL 参数绑定和连接关闭。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
Apache RocketMQ 官方:事务消息与本地事务回查 (新标签页)↗
只读半事务消息与回查流程,和本地 outbox 对照画出数据库提交前后失败的结果,分清两种方案怎样补发通知。
查看学习准备与访问说明 →RabbitMQ 官方 Java 教程:任务队列与可靠发布 (新标签页)↗
只练处理途中停止消费者和消息重新投递,检查同一个任务编号再次出现时,业务是否会被重复执行。
查看学习准备与访问说明 →