直接obj.likes += 1在并发下丢数据,是因为该操作分“读-改-写”三步:先查出旧值(如99),Python内存中加1得100,再保存;若两个请求同时执行,均基于99计算并写入100,最终结果为100而非102,造成更新丢失。
为什么直接 obj.likes += 1 在并发下会丢数据
多个用户几乎同时点赞时,Django 默认先查出当前 likes 值(比如 99),各自加 1 得到 100,再保存——最终结果是 100 而不是正确的 102。这不是 bug,而是读-改-写操作天然的竞态窗口。
根本问题在于:这个操作没在数据库层面原子化。Python 层面的加法和 save() 是两步,中间可能被其他请求插入。
- 现象:后台日志显示多次点赞,但数据库计数增长慢、不一致
- 场景:高并发文章/评论/视频点赞页、收藏夹批量操作
- 关键点:
F表达式把计算逻辑下推到数据库执行,整个更新语句是一条 SQL,由 DB 锁保证原子性
用 F 实现点赞/收藏的原子增减
假设模型是 Article,字段为 likes 和 favorites:
from django.db.models import F点赞 +1(安全)
Article.objects.filter(id=article_id).update(likes=F('likes') + 1)
取消点赞 -1(也安全)
Article.objects.filter(id=article_id).update(likes=F('likes') - 1)
收藏 +1,同时记录用户关系(需额外逻辑)
Article.objects.filter(id=article_id).update(favorites=F('favorites') + 1)
注意:update() 不会触发模型的 save() 方法或信号,所以如果有依赖 post_save 的逻辑(如缓存更新),得手动补上。
- 必须用
filter().update(),不能用instance.save()—— 后者仍走 Python 层计算 -
F表达式只在 QuerySet 更新时生效,get()+F没用 - 如果要校验状态(如“未点赞才可点”),需用
select_for_update()或数据库唯一约束配合事务
处理「用户已点过」这类业务逻辑
F 解决了计数本身,但“是否允许点赞”是另一层判断。常见做法是建关联表(如 ArticleLike),再用 exists() + update() 组合:
go语言参考手册 中文CHM版
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。本文给大家带来Go参考手册,需要的可以来下载! Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。 Go 语言特色 简洁、快速、安全 并行、有趣、开源 内存管理、v数组安全、编译
# 先查是否已点 liked = ArticleLike.objects.filter(user=user, article_id=article_id).exists()if not liked:
原子增计数 + 记录关系
with transaction.atomic(): Article.objects.filter(id=article_id).update(likes=F('likes') + 1) ArticleLike.objects.create(user=user, article_id=article_id)这里
transaction.atomic()保证两个操作要么全成功,要么全失败;而ArticleLike表的(user_id, article_id)应设为唯一索引,防止重复插入。
- 错误做法:先
get_or_create再update—— 中间有竞态,可能创建两次 - 更健壮方案:用
INSERT ... ON CONFLICT DO NOTHING(PostgreSQL)或INSERT IGNORE(MySQL),避免事务阻塞 -
F无法做条件判断(如“仅当 likes Case/When 或原生 SQL
性能与缓存怎么配合
高频读场景下,每次查 likes 字段仍可能压数据库。建议加一层缓存,但更新时必须同步:
- 用
cache.incr('article:123:likes')配合 Redis 原子计数器,比 DB 更快 - 但 DB 是唯一真相源,缓存更新失败不能影响 DB 计数 —— 所以 DB 更新用
F保证正确,缓存更新失败可异步重试 - 不要用
cache.set('article:123', obj)缓存整个对象,因为F更新不触发save(),缓存就 stale 了 - 如果用 Redis,注意
INCR返回新值,可直接用于前端响应,避免再查 DB
真正容易被忽略的是:F 表达式不会调用字段的 default 或 validators,也不会触发 pre_save 信号。如果你的计数字段有业务约束(比如不能为负),得在数据库层加 CHECK 约束,而不是靠 Django 模型验证。