select_for_update() 是悲观锁而非乐观锁,因其依赖数据库行锁阻塞写操作;乐观锁需用 version 字段或高精度时间戳配合 update() 的原子 where 条件实现。

为什么直接用 select_for_update() 不算乐观锁
很多人误把 Django 的 select_for_update() 当作乐观锁,其实它是悲观锁:数据库加行锁、阻塞其他写操作。乐观锁不依赖数据库锁,而是靠版本号或时间戳比对——提交时检查数据是否被改过,没被改才更新,否则报错或重试。
Django 本身不内置乐观锁支持,但可以用 update() 的 where 条件 + 模型字段(如 version 或 updated_at)手动实现。
- 典型场景:多人同时编辑同一条订单、库存扣减、计数器递增
- 关键点:必须在
update()中用filter()精确限定“期望的旧状态”,比如version=old_version - 如果
update()返回0,说明条件不匹配——数据已被他人修改,当前操作失效
用 version 字段实现原子更新
在模型中加一个 version IntegerField,默认 0,每次成功更新后自增。注意不要用 AutoField 或主键,它只是逻辑版本号。
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
version = models.IntegerField(default=0)
更新时这样写:
仅使用 Python 将 Claw Code 虚拟化框架与 OpenClaw 集成,提供 184 种工具和 200 多条命令,用于安全分析和代码质量...
def update_article_safe(article_id, new_title, expected_version):
rows = Article.objects.filter(
id=article_id,
version=expected_version
).update(
title=new_title,
version=expected_version + 1
)
if rows == 0:
raise ValueError("Article已被他人修改,请刷新后重试")
return True
- 这个
update()是原子的:数据库层面一次性完成“检查+更新”,无竞态窗口 - 不能用
save()+refresh_from_db()模拟,因为读-改-写三步非原子,必然有并发风险 -
version字段要加数据库索引(db_index=True),否则filter(version=...)可能全表扫描
用 updated_at 时间戳替代版本号可行吗?
可以,但不推荐。时间戳精度(尤其是 MySQL 的 DATETIME 默认只到秒)容易导致“伪冲突”或“漏检测”。如果非要使用,务必用带微秒的 DateTimeField 并确保写入时用 timezone.now() 而非本地时间。
更糟的情况是:两个请求几乎同时读到相同 updated_at,都以为自己是最新,然后先后调用 update()——后者会覆盖前者,且不会报错(因为时间戳被更新了,但业务逻辑可能已错乱)。
- 时间戳适合“最终一致性要求低、仅需粗略防覆盖”的场景
- 若业务要求强一致性(如金融类变更),必须用单调递增的
version字段 - 避免在
update()中用F('updated_at')直接赋值,Django 不支持在update()里用F()修改同一字段再用于条件判断
并发重试逻辑怎么加才不卡死
乐观锁失败后不能直接抛错给用户,通常需要自动重试。但必须设上限,否则网络延迟或长事务可能造成无限循环。
def try_update_with_retry(article_id, new_title, max_retries=3):
for i in range(max_retries):
try:
article = Article.objects.get(id=article_id)
return update_article_safe(article_id, new_title, article.version)
except ValueError:
if i == max_retries - 1:
raise
time.sleep(0.01 * (2 ** i)) # 指数退避
return False
- 重试前必须重新
get()获取最新数据,包括当前version - 指数退避(
0.01,0.02,0.04)比固定延时更能缓解雪崩 - 别在事务外做重试——如果外层已开启
transaction.atomic,重试时要确保不嵌套事务,否则可能触发TransactionManagementError
真正难的不是写对这几行代码,而是想清楚:哪条数据必须强一致?哪些字段变更允许丢失?重试时要不要合并业务逻辑(比如库存扣减要累加而非覆盖)?这些决定了乐观锁到底该锁什么、怎么锁。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










