非常游戏网
Python Django中如何实现点赞收藏的计数功能_使用F表达式避免竞争条件

Python Django中如何实现点赞收藏的计数功能_使用F表达式避免竞争条件

2026-05-28日常编程178847

直接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,字段为 likesfavorites

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_createupdate —— 中间有竞态,可能创建两次
  • 更健壮方案:用 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 表达式不会调用字段的 defaultvalidators,也不会触发 pre_save 信号。如果你的计数字段有业务约束(比如不能为负),得在数据库层加 CHECK 约束,而不是靠 Django 模型验证。