bigywater.tech
Django · 119

数据库事务与异步任务的边界

把任务发给 Celery 之类的 worker 时,worker 可能比你的事务提交得更快。这篇在真实的 PostgreSQL 上用另一个线程当 worker,诚实地看这个竞态在什么条件下出现、什么条件下没有复现。

40 / 40 查不到
任务在 atomic 里入队、提交前还有 50 ms 工作时,worker 立刻查订单
200 / 200 查到
入队后立刻提交、on_commit 入队、ATOMIC_REQUESTS 下——窗口很短,没有复现失败
重试
worker 每 20 ms 重试,第 2–3 次查到

worker 会不会先于提交

实测 · 本机执行输出
  atomic 里入队,入队后立刻退出 atomic(提交)      共 200 次: {'found': 200}
  atomic 里入队,入队后再做 50 ms 别的事才提交     共 40 次: {'missing': 40}
  on_commit 里入队                                共 200 次: {'found': 200}

这个结果需要小心解读:竞态并没有在最短窗口里复现。我用的临时 PostgreSQL 关掉了 fsync 与同步提交,提交本身很快,而 worker 线程被唤醒并发起查询的时间反而更长。一旦提交前多了 50 ms 的其它工作,失败就是必然的。所以「没复现」不代表安全,只代表窗口够短;生产环境里提交更慢(fsync)、事务更长,更容易出现。

ATOMIC_REQUESTS

实测 · 本机执行输出
  视图里直接入队: {'found': 200}
  视图里 on_commit 入队: {'found': 200}

ATOMIC_REQUESTS = True 时整个视图在一个事务里,视图返回后才提交,视图里的入队天然早于提交。本次 200 次请求都没有失败,同样是因为窗口太短,没有更多结论。

消费端重试

实测 · 本机执行输出
  atomic 里入队 + 50 ms 后才提交,worker 第几次尝试查到(0 = 5 次都没查到): {2: 10, 3: 30}

重试能补救,但它是让消费端去容忍生产端的错误。生产端用 transaction.on_commit 入队(第 120 篇)才是不产生这个窗口的做法。

分步演示

事务与任务边界
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。自己起的临时 PostgreSQL 16(fsync=off、synchronous_commit=off);worker 是同一进程里的线程 + 独立连接,不是真实的 Celery / RQ
    • 没有验证:真实的消息队列延迟、fsync 打开时的提交耗时、任务里读到旧状态(未提交的更新)的情形、Celery 的 ETA / countdown 是否足以规避、多数据库路由
    京ICP备2026067502号-1京公网安备11010502063487号