F()表达式通过将计算下推至数据库层实现原子更新,避免“读-改-写”竞态,必须配合update()使用;select_for_update()需在transaction.atomic()中且命中索引才生效;乐观锁用version字段在WHERE子句校验,失败时rows为0;多进程需调用close_old_connections()防连接失效。

用 F() 表达式做原子更新,绕过“读-改-写”陷阱
并发下最常见错误是先 get() 再修改字段最后 save(),比如 obj.count += 1; obj.save()。两个请求几乎同时执行,都会读到旧值,各自加 1 后写回,结果只 +1 而非 +2。
F() 把计算逻辑下推到数据库层,生成类似 UPDATE table SET count = count + 1 WHERE id = 123 的语句,整个操作由数据库保证原子性。
- 必须配合
.update()使用:MyModel.objects.filter(id=123).update(count=F('count') + 1);单独赋值给实例字段再save()不行(obj.count = F('count') + 1看似可行,但若中间有其他字段修改,仍可能触发全量更新) - 支持链式运算:
F('price') * 0.9、F('updated_at') + timedelta(hours=1) - 不返回模型实例,只返回影响行数(可用于判断是否更新成功)
用 select_for_update() 加行锁,控制“读-改-写”流程
当业务逻辑复杂、不能单靠 update() 完成(比如要校验多个字段、调外部 API、生成关联记录),就得显式加锁,确保同一行不被并发读取后各自修改。
但这个方法极易失效——它只在事务里起作用,且依赖数据库引擎和索引。
- 必须包裹在
transaction.atomic()块中,否则锁无效(MySQL 静默忽略,PostgreSQL 可能报错) - 查询必须命中索引,否则 MySQL 可能升级为表锁;
User.objects.filter(email='x').select_for_update()若email无索引,风险极高 - 避免空
__in查询:.filter(id__in=[]).select_for_update()不会加任何锁 - 加
nowait=True可让拿不到锁时立刻抛DatabaseError,适合快速失败场景
用带版本号的 filter().update() 实现乐观锁
不希望阻塞、又需要严格一致性时,乐观锁更轻量。核心是把版本检查塞进 SQL 的 WHERE 子句,靠数据库原子性保证:只有当前版本未变,才允许更新。
例如库存扣减:rows = Product.objects.filter(id=pid, version=old_ver).update(stock=F('stock') - 1, version=F('version') + 1)。
-
rows返回 0 表示更新失败(版本已变),必须重试或拒绝请求 - 绝不能先
get()再手动比对version,Python 层判断和后续save()之间存在竞态窗口 - 字段名如
version需为IntegerField或DateTimeField,且初始值要统一(比如全设为 0 或auto_now_add)
别忽略事务边界和连接复用问题
Django 默认每个请求不在事务中,ATOMIC_REQUESTS=True 是全局开关,但它只覆盖 view 层;异步任务、管理命令、多进程子进程里的 ORM 操作,都得手动加 transaction.atomic()。
更隐蔽的问题来自连接池和多进程:
- 用
multiprocessing.Pool并发跑 ORM 时,子进程会复用父进程的数据库连接,导致DatabaseError: MySQL server has gone away;解决方式是子进程内首次 ORM 操作前调用django.db.close_old_connections() - SQLite 不支持
select_for_update(),开发时用它测试锁逻辑会“假装成功”,上线前务必切到 PostgreSQL 或 MySQL InnoDB - 高并发下频繁开事务+加锁会拖慢响应,
nowait=True或队列削峰(如 Celery 单 worker 处理库存)往往是更实际的选择
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











