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

直接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 模型验证。

相关推荐:

如何实现Python中线程安全的单例模式_使用元类metaclass或Lock加锁

因为__new__不是原子操作:线程A判断_instance为None后,未执行super().__new__前,线程B也判为None,导致重复创建;GIL不保护跨语句逻辑,需用双重检查锁定(DCL)加threading.Lock确保线程安全。 为什么直接用 __new__ 实现单例在多线程下会失效 因为 __new__ 本身不是原子操作:线程 A 判断 _instance 为 None 后,还没...

Python如何实现图的深度优先遍历_基于栈结构或递归函数实现

递归DFS栈溢出风险高因CPython默认递归深度仅1000,长链或环易触发RecursionError;隐式栈不可控,闭包和帧对象加剧内存开销;图应优先用显式栈实现。 递归实现DFS时,为什么栈溢出风险比预期高? Python默认递归深度限制是1000层,图中存在长链或环状结构时极易触发RecursionError。这不是算法错,而是CPython解释器对递归调用的硬性约束。 用sys.setr...

高效实现 Pandas DataFrame 中基于多列的条件列创建

本文介绍多种优化方法,帮助你在处理大规模数据时高效创建条件列,包括向量化操作、numpy.select、pd.cut 和缓存函数调用等策略,显著减少计算开销并提升可读性与执行速度。 本文介绍多种优化方法,帮助你在处理大规模数据时高效创建条件列,包括向量化操作、`numpy.select`、`pd.cut` 和缓存函数调用等策略,显著减少计算开销并提升可读性与执行速度。 在 Pandas 中为大型 ...

Python怎么优雅地解构嵌套元组数据_利用星号表达式处理冗余项

星号表达式()在解构中只能出现一次且仅限当前层级,嵌套结构中使用(如(a, b, (c, *d)))会报SyntaxError;正确做法是分步解构或改用索引/切片。 用 * 解构嵌套元组时,为什么报错 SyntaxError: two starred expressions 星号表达式(*)在解构中只能出现一次,且不能和另一个 * 共存。常见错误是想对嵌套结构里多层都用 *,比如:a, *b, (...

Pandas 中按列分组统计满足条件的唯一值数量并筛选结果

本文介绍如何在 Pandas 中基于多条件过滤(如 colC > 0 且 colB 属于指定集合),按 colA 分组统计 colB 的唯一值个数,并仅保留计数大于 2 的分组,最终导出对应原始数据子集。 本文介绍如何在 pandas 中基于多条件过滤(如 `colc > 0` 且 `colb` 属于指定集合),按 `cola` 分组统计 `colb` 的唯一值个数,并仅保留计数大于 ...

Python如何实现一致性哈希算法_解决分布式缓存数据分配不均

一致性哈希通过固定哈希空间(如2^32)构建环形结构,节点和数据映射其上并顺时针就近分配;增删节点仅影响邻近区间,大幅减少数据迁移;引入虚拟节点可缓解物理节点分布不均问题,提升负载均衡性。 直接用 hash_ring 库最省事,但真要自己写、调参或排查分布不均问题,必须理解虚拟节点数量、哈希函数输出范围、环结构更新时机这三个关键点。 为什么简单取模(hash(key) % N)在节点变动时会崩 节...

高效优化 Pandas 中基于多列的条件列生成

本文介绍多种高效构建条件列的方法,重点对比 apply + 缓存、向量化操作与 numpy.select 等方案,帮助你在处理万级数据时显著提升性能并保持代码可读性。 本文介绍多种高效构建条件列的方法,重点对比 `apply` + 缓存、向量化操作与 `numpy.select` 等方案,帮助你在处理万级数据时显著提升性能并保持代码可读性。 在 Pandas 中为大型 DataFrame(如 20...

Python中Scikit-learn如何实现半监督学习_利用LabelPropagation

LabelPropagation要求数据具备局部平滑性,特征须标准化,未标记样本标签必须为-1,且需至少两个已标记类别;拟合用transduction_获取全部预测,不提供置信度,对噪声敏感。 LabelPropagation 在 scikit-learn 中的适用前提 LabelPropagation 不是万能半监督工具,它要求数据天然具备“局部平滑性”——即相似样本大概率共享标签。如果特征空间...

Spark SQL 中解析嵌套布尔表达式并生成逻辑分解步骤的完整教程

本文介绍如何在 Spark SQL 环境中准确解析含多层括号的布尔表达式(如 A1 AND (A4 OR (A2 AND A3))),并按运算优先级递归生成带编号逻辑标签(如 logic1, logic2)的中间表达式序列,解决纯 SQL 字符串分割无法处理嵌套结构的根本问题。 本文介绍如何在 spark sql 环境中准确解析含多层括号的布尔表达式(如 `a1 and (a4 or (a2 an...