惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
人人都是产品经理
人人都是产品经理
腾讯CDC
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
小众软件
小众软件
B
Blog
博客园 - 叶小钗
Microsoft Azure Blog
Microsoft Azure Blog
Apple Machine Learning Research
Apple Machine Learning Research
A
About on SuperTechFans
J
Java Code Geeks
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Recent Announcements
Recent Announcements
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
Hugging Face - Blog
Hugging Face - Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
V
V2EX

Aimee's Blog

秒杀怎么防机器人:验证码之外的几道防线 防薅羊毛怎么做:号码风控、设备指纹与分层策略 接口限流怎么选:固定窗口、滑动窗口、令牌桶 批量打包下载怎么设计:流式 ZIP 与异步任务 图片处理怎么做:异步处理与 CDN 实时参数 大文件上传怎么设计:分片、断点续传、秒传 报表可重跑的设计:快照、水位线与幂等 UV 统计为什么有误差:精确计数与近似计数怎么选 大数据量导出怎么做:流式写入与异步任务 排行榜的两类需求,难度差了一个数量级 内容审核怎么选:先审后发,还是先发后审 实时热榜怎么设计:Redis ZSet 与热度分值 互关状态怎么保持同步:双向关系的维护与缓存失效 删了父评论,子评论怎么处理——先想清楚用户看到的是什么 定时消息任务触发了两次 消息量大,写扩散还是读扩散——接到这个需求先问清楚规模 同一条消息推送了三次 活动结束了,用户还在收短信 接到注销需求,先问两个问题 微服务与服务拆分:何时拆、怎么拆 异步与事件驱动架构:把协作从「打电话」改成「发消息」 高可用设计:怎么让系统尽量不宕机 可扩展性设计:怎么让系统加机器就能扛更多 缓存架构:多级缓存怎么搭 高并发三板斧:限流、熔断、降级 架构设计到底在设计什么 —— 从单体到微服务的演进 服务成本账:一个服务一个月烧多少钱 可观测性:线上出问题怎么查 API 设计:好接口长什么样 消息队列:为什么要 MQ,以及丢失、重复、顺序怎么破
并发点赞的计数设计:原子操作与最终对账
Aimee · 2026-08-06 · via Aimee's Blog

并发点赞的计数设计:原子操作与最终对账

并发点赞有一类经典现象:压测时点了 50 次赞,最终计数却少了几个,而且稳定复现。

少掉的计数去哪了?


错误方案:读-改-写

// 危险的写法
int count = articleDao.getLikeCount(articleId);
articleDao.updateLikeCount(articleId, count + 1);  // 点赞

并发时:线程 A 读到 50,线程 B 也读到 50,都写回 51。实际应该是 52,结果丢了一次点赞。

这是典型的 race condition,读-改-写在并发场景必须用原子操作替代。


原子计数:数据库层

UPDATE article SET like_count = like_count + 1 WHERE id = ?  -- 点赞
UPDATE article SET like_count = like_count - 1 WHERE id = ?  -- 取消点赞

like_count = like_count + 1 是数据库的原子操作,不存在并发丢失的问题。

但有两个问题没解决:防重复点赞精确防负数


防重复点赞

同一用户对同一内容只能点赞一次,需要单独记录"谁点了哪篇":

CREATE TABLE user_like (
    user_id    BIGINT NOT NULL,
    article_id BIGINT NOT NULL,
    created_at DATETIME,
    PRIMARY KEY (user_id, article_id)  -- 唯一约束保证不重复
);

点赞流程:

  1. user_like 插入记录(主键冲突说明已点,返回"已点赞")
  2. 插入成功再 like_count + 1

取消点赞流程:

  1. 删除 user_like 记录(不存在说明未点过,返回"未点赞")
  2. 删除成功再 like_count - 1

两步操作不是原子的,要在一个事务里:

@Transactional
public void like(long userId, long articleId) {
    int affected = userLikeDao.insert(userId, articleId);  // 0 表示已点过
    if (affected > 0) {
        articleDao.incrementLike(articleId);
    }
}

高并发场景:Redis 计数

数据库原子操作在高并发下也会有锁竞争,热门内容每秒几千次点赞,UPDATE 会产生行锁等待。

更好的方案:Redis 原子计数 + 异步落库:

// 点赞
public void like(long userId, long articleId) {
    // 用户级去重(Set 结构,sadd 返回 1 表示首次添加)
    long added = redis.sAdd("likes:" + articleId, String.valueOf(userId));
    if (added > 0) {
        redis.incr("like_count:" + articleId);
        // 发 MQ 事件,异步写 user_like 表
        mqProducer.send(new LikeEvent(userId, articleId, "LIKE"));
    }
}

Redis SADDINCR 都是原子操作,耐并发。计数读取直接从 Redis 拿,实时性好。

异步写库时,MQ 消费幂等(用主键唯一约束兜底),延迟落库可接受。


计数最终对账

Redis 和数据库之间可能有差异(Redis 重启数据丢失、MQ 消费延迟)。定期对账:

定时任务按文章批量比对 like_count 字段和 user_like 表的真实 count,发现差异写告警并修正。对账不是替代方案,是最后一道兜底。