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: 树莓派的 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) 的
- 主数据库的更改反映到副本的延迟为 数毫秒到数百毫秒
- 通过
add()
连接的 ManyToMany 的中间表记录 尚未反映到副本
✅ Django 的 on_commit() 工作方式
on_commit()
在 Django 事务提交后立即执行- 但 Celery 是独立进程,读取的是副本数据库
- 因此,在 on_commit 时副本仍处于未更新状态
6. 验证要点
- Post 创建日志存在
- add() 查询日志也正常执行
- 实际查询顺序与任务调用顺序是正确的
- 问题在于 查询时的数据库是副本这一点
7. 解决方案
1. 在 Celery 任务中 强制从主数据库读取
# ORM 方式
post = Post.objects.using('default').get(id=post_id)
tags = post.tags.using('default').all()
categories = post.categories.using('default').all()
2. 配置 Celery 专用数据库路由
在 settings.py
中配置 Celery 任务强制使用主数据库
3. 如果副本必须保持最新状态:
- 需要将 PostgreSQL 的
synchronous_commit = on
配置为开启 (可能降低性能)
8. 结论
这个问题不是 Django 的问题,而是 在异步复制环境中延迟与 ORM 的读取优先策略冲突的结果。
解决的关键在于确保 Celery 任务强制使用主数据库。
9. Jesse 的评论
"on_commit() 无疑是在准确的时机执行的,但 读取数据库是副本 这一点是问题的本质。
Django 和 Celery 并没有错。最终,理解和协调系统架构的是开发者的责任。"
댓글이 없습니다.