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. 日誌分析
✅ 順序摘要 (以示例時間為準)
13:13:56.728
— 創建 Post13:14:00.922
— 最後TaggedItem
(ManyToMany) 查詢13:14:01.688
— 執行on_commit()
→ 調用 Celery13:14:01.772
—translate_post()
內查詢結果:- categories:
[]
- tags:
[]
- categories:
換句話說,順序正確,但內容未反映
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 沒有錯。最終,理解並協調系統架構是開發者的責任。"
Add a New Comment