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

推荐订阅源

G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
WordPress大学
WordPress大学
阮一峰的网络日志
阮一峰的网络日志
V
Visual Studio Blog
雷峰网
雷峰网
博客园_首页
The Cloudflare Blog
Hugging Face - Blog
Hugging Face - Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
爱范儿
爱范儿
小众软件
小众软件
D
Docker
P
Proofpoint News Feed
B
Blog
Vercel News
Vercel News
B
Blog RSS Feed
U
Unit 42
月光博客
月光博客
The GitHub Blog
The GitHub Blog
Apple Machine Learning Research
Apple Machine Learning Research
Y
Y Combinator Blog
I
InfoQ
Recent Announcements
Recent Announcements

Aimee's Blog

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

互关状态怎么保持同步:双向关系的维护与缓存失效

A 和 B 互相关注,UI 上都显示"互相关注"。

A 取消了对 B 的关注,A 那边显示变成了"已关注"(单向),但 B 打开 A 的主页,居然还显示"互相关注"。

数据没同步,B 看到的是错的。


关注关系是两条单向记录

很多人第一反应:是不是"互关"字段没更新?

实际上,设计合理的关注系统里没有"互关字段"——互关是两条单向关系叠加的结果,不是独立存储的状态:

CREATE TABLE follow (
    follower_id   BIGINT NOT NULL,   -- 谁关注了
    followed_id   BIGINT NOT NULL,   -- 被关注的人
    created_at    DATETIME,
    PRIMARY KEY (follower_id, followed_id)
);

A 关注 B:(follower_id=A, followed_id=B) 一条记录 B 关注 A:(follower_id=B, followed_id=A) 一条记录

"A 和 B 是否互关"= 这两条记录是否都存在。没有单独的"互关"字段。


根因:UI 层判断互关时用了缓存

A 取消关注 B,删除了 (A, B) 这条记录。

B 的页面"查看 A 的主页"时展示的互关状态,是从某处缓存里读的,缓存没有失效。

修复思路:A 取消关注时,主动失效 B 视角里与 A 相关的缓存

@Transactional
public void unfollow(long followerId, long followedId) {
    followDao.delete(followerId, followedId);

    // 失效双方的关系缓存
    cacheManager.evict("follow_status:" + followerId + ":" + followedId);
    cacheManager.evict("follow_status:" + followedId + ":" + followerId);
}

关键点:follow_status:B:A 这个 key 描述的是"B 关注 A、且 A 关注 B(互关)"的状态,A 取消关注 B 时,这个 key 也要失效,因为互关状态已经改变。


查关注状态的正确姿势

展示"A 和 B 的关系状态"时,应该同时查两个方向:

public FollowStatus getFollowStatus(long viewerId, long targetId) {
    boolean iFollow = followDao.exists(viewerId, targetId);
    boolean theyFollow = followDao.exists(targetId, viewerId);

    if (iFollow && theyFollow) return FollowStatus.MUTUAL;
    if (iFollow) return FollowStatus.FOLLOWING;
    if (theyFollow) return FollowStatus.FOLLOWER;
    return FollowStatus.NONE;
}

缓存这个结果时,key 要同时涉及两个方向:follow_status:{min(A,B)}:{max(A,B)},用较小 ID 在前、较大 ID 在后排序,保证 A 看 B 和 B 看 A 命中同一个缓存 key,任何一方的关注变化都只需要失效一个 key。


关注数计数

粉丝数(被关注数)是高频读取的字段,通常单独维护计数:

CREATE TABLE user_stat (
    user_id          BIGINT PRIMARY KEY,
    following_count  INT DEFAULT 0,   -- 我关注了多少人
    follower_count   INT DEFAULT 0    -- 有多少人关注我
);

A 关注 B 时:A 的 following_count + 1,B 的 follower_count + 1。 A 取消关注 B 时:A 的 following_count - 1,B 的 follower_count - 1

这两步要和 follow 表的写入放在同一个事务里,否则计数和关系记录不一致。数量级大时也可以用 Redis 原子计数 + 异步落库,原理同点赞计数。