1. 問題概述

在 Django 環境中,利用 transaction.on_commit() 結構來調用 Celery 任務時,
發生了 Celery 任務內部 ManyToMany 欄位數據為空的現象


2. 現象

  • 在完成 post.categories.add(...)post.tags.add(...)
  • 通過 transaction.on_commit() 回調調用 translate_post.delay()
  • 在 Celery 任務內部查詢 post.categories.all()post.tags.all() 時,返回 空列表([])

3. 環境配置

  • WRITE: GCP VM 的 PostgreSQL
  • READ: Raspberry Pi 的 PostgreSQL 副本 (流複製)
  • 通過 Django DB router 設定讀取請求發送到副本
  • 使用 ManyToMany 關係欄位 的 Post 模型

4. 日誌分析

✅ 順序摘要 (以示例時間為準)

  1. 13:13:56.728 — 創建 Post
  2. 13:14:00.922 — 最後 TaggedItem (ManyToMany) 查詢
  3. 13:14:01.688 — 執行 on_commit() → 調用 Celery
  4. 13:14:01.772translate_post() 內查詢結果:
    • categories: []
    • tags: []

換句話說,順序正確,但內容未反映


5. 原因分析

✅ PostgreSQL 副本延遲

  • PostgreSQL 的默認復制是 異步(async) 操作
  • 主 DB 的變更反映到副本需要 數毫秒至數百毫秒的延遲
  • add() 連接的 ManyToMany 的中間表記錄 尚未反映到副本

✅ Django 的 on_commit() 運行方式

  • on_commit() 在 Django 交易提交後立即執行
  • 但 Celery 是獨立進程,且 讀取使用了副本 DB
  • 結果是 在 on_commit 時副本尚未更新

6. 驗證要點

  • Post 創建日誌存在
  • add() 查詢日誌正常執行
  • 實際查詢順序與任務調用順序正確
  • 問題在於 查詢時使用的 DB 是副本

7. 解決方案

1. 在 Celery 任務中 強制從主 DB 讀取

# ORM 方式
post = Post.objects.using('default').get(id=post_id)
tags = post.tags.using('default').all()
categories = post.categories.using('default').all()

2. 配置 Celery 專用 DB Router

可以在 settings.py 中設置 Celery 任務必須使用主 DB


3. 當副本必須保持最新狀態時:

  • 需要設置 PostgreSQL 的 synchronous_commit = on (可能降低性能)

8. 結論

這個問題並不是 Django 的問題,而是 異步復制環境中的延遲和 ORM 的讀取優先策略衝突的結果
解決的關鍵是 保證在 Celery 任務中強制使用主 DB


9. Jesse 的評論

"on_commit() 明確地在正確的時間執行,但 讀取的 DB 是副本 是問題的本質。
Django 和 Celery 沒有錯。最終,理解並協調系統架構是開發者的責任。"