← 返回首页

内容社区实战 — 从Feed流到热搜的实现

Feed流 点赞评论 审核系统 热搜 推荐算法

报纸+读者来信:发帖=投稿、Feed=报纸排版、点赞=读者叫好、热搜=头版头条、审核=编辑部审稿

📰 业务全景 — 内容社区做什么

内容社区系统是微博、小红书、知乎、B站这类平台的核心。用户发帖、浏览Feed流、点赞评论、搜索热搜——这些功能的背后是一套精密的内容分发系统。

我们把内容社区想象成一个"报纸+读者来信"系统:

发帖 = 投稿 — 作者把内容提交给编辑部
Feed流 = 报纸排版 — 编辑把文章排好版,读者翻阅
点赞 = 读者叫好 — 喜欢就鼓掌
热搜 = 头版头条 — 最多人关注的新闻
审核 = 编辑部审稿 — 有问题的打回修改
💡 核心类比理解:你在小红书发一篇笔记,就像给报社投稿——编辑先审稿(审核),通过后排版到版面上(Feed流),读者翻到你的文章(推荐),觉得好就鼓掌(点赞),评论就是读者来信。如果很多人都在讨论某个话题,就上了头版头条(热搜)。

内容社区核心流程

创作发帖 内容审核 发布入库 Feed分发 互动(点赞/评论/转发)

详细拆解:

第1步:创作发帖 — 用户写文章、发图片/视频,提交到服务端。就像作者把稿子寄给报社。

第2步:内容审核 — 服务端对内容进行合规检查:有没有敏感词?图片是否违规?就像编辑审稿,有问题的打回修改。

第3步:发布入库 — 审核通过后,内容存入数据库,建立索引。就像文章通过审核,正式排版上版。

第4步:Feed分发 — 把内容推送到关注者的Feed流中,或被推荐系统分发给更多用户。就像报纸送到订户手中。

第5步:互动 — 用户点赞、评论、转发、收藏。就像读者给报社写信反馈、给文章叫好。

🎯 核心挑战 — 5大难题

挑战1:Feed流 — 首页刷到什么内容?

打开微博/小红书,首页显示的内容是怎么来的?这就是Feed流的核心问题:给用户看什么?

类比理解:报纸主动送上门=推模式,你去报摊自己挑=拉模式。推模式省事但可能推的内容你不喜欢,拉模式精准但要自己费劲。

推模式 vs 拉模式

推模式(写扩散)
作者发帖 写入所有粉丝的收件箱 粉丝刷Feed直接读
拉模式(读扩散)
作者发帖 写入发帖者时间线 粉丝刷Feed时实时拉取关注人的帖子
对比维度 推模式 拉模式 推拉结合
写入开销 大:1帖×N粉丝=N次写入 小:1帖=1次写入 中等:活跃用户推、不活跃拉
读取开销 小:直接读自己收件箱 大:实时聚合N个关注人 较小:活跃部分预计算
写延迟 高(粉丝多时) 中等
读延迟 较低
适用场景 粉丝量中等、读多 关注量多、写多 混合场景
典型应用 朋友圈 Twitter 微博/小红书
实战最优解:推拉结合 — 普通用户发帖用推模式(粉丝不多,写入量可控),大V发帖用拉模式(粉丝太多,写不完),活跃粉丝推到收件箱(经常看的保证体验),不活跃粉丝按需拉取(偶尔看的省存储)。

Redis Timeline代码实现

// Feed流服务 — 推拉结合方案 @Service public class FeedService { @Autowired private RedisTemplate<String, String> redisTemplate; @Autowired private UserFollowRepository followRepo; @Autowired private RocketMQTemplate mqTemplate; // 大V粉丝数阈值:超过此值用拉模式 private static final long BIG_V_THRESHOLD = 10000; // 推模式推送的活跃粉丝数上限 private static final int ACTIVE_PUSH_LIMIT = 1000; /** * 发帖后Feed分发 — 推拉结合 */ public void distributePost(Post post) { String authorId = post.getAuthorId(); long followerCount = followRepo.countFollowers(authorId); // 第1步:写入作者自己的发帖时间线(所有模式都需要) String authorTimelineKey = "timeline:user:" + authorId; redisTemplate.opsForZSet().add( authorTimelineKey, post.getId(), post.getCreatedAt() ); if (followerCount <= BIG_V_THRESHOLD) { // 普通用户:推模式 — 给所有粉丝的收件箱写入 pushToFollowers(post); } else { // 大V:推拉结合 — 只推给活跃粉丝,其他按需拉 pushToActiveFollowers(post); } } /** * 推模式:写入所有粉丝的收件箱 */ private void pushToFollowers(Post post) { List<String> followerIds = followRepo.findFollowerIds(post.getAuthorId()); // 异步批量写入,不阻塞发帖 Lists.partition(followerIds, 100).forEach(batch -> { FeedPushMessage msg = new FeedPushMessage(); msg.setPostId(post.getId()); msg.setAuthorId(post.getAuthorId()); msg.setTargetUserIds(batch); msg.setTimestamp(post.getCreatedAt()); mqTemplate.asyncSend("feed-push", msg); }); } /** * 推拉结合:只推给活跃粉丝 */ private void pushToActiveFollowers(Post post) { // 查询最近7天活跃的粉丝 List<String> activeFollowers = followRepo.findActiveFollowerIds( post.getAuthorId(), ACTIVE_PUSH_LIMIT ); for (String followerId : activeFollowers) { String inboxKey = "feed:inbox:" + followerId; redisTemplate.opsForZSet().add( inboxKey, post.getId(), post.getCreatedAt() ); } // 不活跃粉丝:不推,刷Feed时实时拉取 } /** * 获取用户Feed流 */ public List<Post> getFeed(String userId, long maxScore, int count) { String inboxKey = "feed:inbox:" + userId; // 第1步:从收件箱获取帖子ID(推模式部分) Set<String> postIds = redisTemplate.opsForZSet() .reverseRangeByScore(inboxKey, 0, maxScore, 0, count); // 第2步:补充拉模式部分(大V的帖子) List<String> bigVIds = followRepo.findBigVFollowing(userId); for (String bigVId : bigVIds) { String timelineKey = "timeline:user:" + bigVId; Set<String> bigVPostIds = redisTemplate.opsForZSet() .reverseRangeByScore(timelineKey, 0, maxScore, 0, 20); postIds.addAll(bigVPostIds); } // 第3步:合并排序 + 分页 List<Post> posts = postRepo.findByIds(postIds); posts.sort(Comparator.comparingLong(Post::getCreatedAt).reversed()); return posts.stream().limit(count).collect(Collectors.toList()); } }

挑战2:点赞评论 — 100万赞怎么高效计数?

一篇爆款文章可能有上百万赞。如果每次点赞都写数据库,数据库扛不住。

类比理解:统计点赞数就像计票——你不需要记住每张票是谁投的,只需要知道总数。但如果要做排行榜,就需要记录"谁投了谁"。

用户点赞 Redis计数器+1 异步写DB(持久化)
查询点赞数 直接读Redis(O(1))
// 点赞服务 — 高并发计数方案 @Service public class LikeService { @Autowired private RedisTemplate<String, String> redisTemplate; @Autowired private RocketMQTemplate mqTemplate; // 点赞计数Key: like:count:{postId} // 点赞去重Set: like:set:{postId} // 点赞排行榜: like:ranking:{topicId} /** * 点赞 — 幂等设计(同一用户对同一帖子只能赞一次) */ public boolean like(String postId, String userId) { String setKey = "like:set:" + postId; // 用Set去重:同一个用户只能赞一次 Long added = redisTemplate.opsForSet().add(setKey, userId); if (added > 0) { // 第一次点赞,计数器+1 String countKey = "like:count:" + postId; redisTemplate.opsForValue().increment(countKey); // 排行榜:用Sorted Set,score=点赞数 String authorId = postRepo.findAuthorId(postId); String rankingKey = "like:ranking:" + postRepo.getTopicId(postId); redisTemplate.opsForZSet().incrementScore(rankingKey, postId, 1); // 异步写入DB(持久化) LikeMessage msg = new LikeMessage(postId, userId, System.currentTimeMillis()); mqTemplate.asyncSend("like-persist", msg); return true; } // 已经赞过了,幂等返回 return false; } /** * 取消点赞 */ public boolean unlike(String postId, String userId) { String setKey = "like:set:" + postId; Long removed = redisTemplate.opsForSet().remove(setKey, userId); if (removed > 0) { String countKey = "like:count:" + postId; redisTemplate.opsForValue().decrement(countKey); // 排行榜分数-1 String rankingKey = "like:ranking:" + postRepo.getTopicId(postId); redisTemplate.opsForZSet().incrementScore(rankingKey, postId, -1); return true; } return false; } /** * 获取点赞数 — 直接读Redis,O(1)极快 */ public long getLikeCount(String postId) { String countKey = "like:count:" + postId; String count = redisTemplate.opsForValue().get(countKey); return count == null ? 0 : Long.parseLong(count); } /** * 判断用户是否已赞 */ public boolean hasLiked(String postId, String userId) { String setKey = "like:set:" + postId; return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(setKey, userId)); } /** * 获取热门帖子排行榜 Top N */ public List<String> getTopPosts(String topicId, int topN) { String rankingKey = "like:ranking:" + topicId; // 从高到低取TopN Set<String> topPostIds = redisTemplate.opsForZSet() .reverseRange(rankingKey, 0, topN - 1); return new ArrayList<>(topPostIds); } }
💡 为什么用Redis Set而不是DB做去重?:100万赞 = 100万次DB查询检查是否已赞,数据库肯定扛不住。Redis Set的SISMEMBER命令是O(1)的,100万次查询也不到1秒。

挑战3:审核系统 — 内容合规检查

用户发的内容不能直接展示,必须先过审核。就像报社有编辑部,有问题的稿件打回修改。

类比理解:审核就像编辑审稿——先过机器审核(敏感词检测=拼写检查、AI图片识别=版面审查),有问题的再交给人工审核(主编终审)。

用户提交内容 机审(敏感词+AI)
通过→直接发布 | 疑似→人工审核 | 违规→打回/封禁

DFA敏感词算法

敏感词过滤最常用的是DFA(Deterministic Finite Automaton,确定有限状态自动机)算法。把所有敏感词构建成一棵字典树,然后逐字匹配。就像查字典——从第一个字母开始找,一直找下去。

// DFA敏感词过滤 — 字典树实现 public class DFAFilter { // 字典树的根节点 private final Map root = new HashMap<>(); // 敏感词结束标记 private static final String END_FLAG = "isEnd"; /** * 初始化:加载敏感词库,构建字典树 * 比如敏感词:["傻瓜", "傻子", "笨蛋"] * 构建的字典树: * 傻 → 瓜 → {isEnd: true} * → 子 → {isEnd: true} * 笨 → 蛋 → {isEnd: true} */ public void init(Set<String> sensitiveWords) { root.clear(); for (String word : sensitiveWords) { addWord(word); } } /** * 向字典树中添加一个敏感词 */ @SuppressWarnings("unchecked") private void addWord(String word) { Map currentNode = root; for (char c : word.toCharArray()) { // 如果当前字符不在字典树中,新建节点 if (!currentNode.containsKey(c)) { currentNode.put(c, new HashMap<>()); } // 移动到下一个节点 currentNode = (Map) currentNode.get(c); } // 标记这个词结束 currentNode.put(END_FLAG, "true"); } /** * 过滤文本 — 检测并替换敏感词 * 返回过滤后的文本和检测到的敏感词列表 */ public FilterResult filter(String text) { List<String> foundWords = new ArrayList<>(); StringBuilder result = new StringBuilder(text); for (int i = 0; i < text.length(); i++) { int length = checkWord(text, i); if (length > 0) { String word = text.substring(i, i + length); foundWords.add(word); // 替换为 *** for (int j = i; j < i + length; j++) { result.setCharAt(j, '*'); } i += length - 1; // 跳过已匹配的字符 } } return new FilterResult(result.toString(), foundWords); } /** * 从位置begin开始检查是否匹配敏感词 * @return 匹配到的敏感词长度,0表示未匹配 */ @SuppressWarnings("unchecked") private int checkWord(String text, int begin) { Map currentNode = root; int wordLength = 0; int matchLength = 0; for (int i = begin; i < text.length(); i++) { char c = text.charAt(i); if (!currentNode.containsKey(c)) { // 字符不在字典树中,匹配结束 break; } wordLength++; currentNode = (Map) currentNode.get(c); if ("true".equals(currentNode.get(END_FLAG))) { // 找到一个完整的敏感词 matchLength = wordLength; } } return matchLength; } } // 使用示例 DFAFilter filter = new DFAFilter(); filter.init(Set.of("傻瓜", "笨蛋", "违规词")); FilterResult result = filter.filter("你是个傻瓜和笨蛋"); // result.filteredText = "你是个**和**" // result.foundWords = ["傻瓜", "笨蛋"]

挑战4:热搜榜 — 实时统计搜索热词Top50

热搜就像报纸的头版头条——最多人关注的新闻排最前面。但热搜是实时的,每秒都在变化。

类比理解:热搜=头版头条。哪个话题被最多人搜索,就排在热搜最前面。但不是简单按搜索次数排——还要考虑时间衰减(昨天的热搜今天应该降温)和反作弊(防止水军刷热搜)。

// 热搜服务 — Redis Sorted Set + 滑动窗口 @Service public class HotSearchService { @Autowired private RedisTemplate<String, String> redisTemplate; // 热搜Key: hotsearch:realtime private static final String HOT_KEY = "hotsearch:realtime"; // 时间窗口Key前缀: hotsearch:window:{minute} private static final String WINDOW_PREFIX = "hotsearch:window:"; // 搜索去重Key: hotsearch:dedup:{keyword}:{userId} private static final String DEDUP_PREFIX = "hotsearch:dedup:"; /** * 用户搜索 — 记录搜索词 * 1. 去重:同一用户对同一词短时间内只算1次 * 2. 滑动窗口:按分钟统计 * 3. 聚合:实时更新总榜 */ public void recordSearch(String keyword, String userId) { // 第1步:去重 — 同一用户5分钟内重复搜索同一词只算1次 String dedupKey = DEDUP_PREFIX + keyword + ":" + userId; Boolean isNew = redisTemplate.opsForValue() .setIfAbsent(dedupKey, "1", 5, TimeUnit.MINUTES); if (!Boolean.TRUE.equals(isNew)) { // 5分钟内已经搜过这个词了,不重复计数 return; } // 第2步:滑动窗口 — 当前分钟的搜索计数+1 long currentMinute = System.currentTimeMillis() / 60_000; String windowKey = WINDOW_PREFIX + currentMinute; redisTemplate.opsForZSet().incrementScore(windowKey, keyword, 1); redisTemplate.expire(windowKey, 2, TimeUnit.HOURS); // 第3步:更新总榜(带权重衰减) updateHotRanking(keyword, 1); } /** * 更新热搜排行 — 带时间衰减 * 衰减公式:新分数 = 旧分数 × 衰减因子 + 本次增量 * 越久没被搜索的词,分数越低 */ private void updateHotRanking(String keyword, double increment) { // 衰减因子:0.95(每次更新时旧分值乘以0.95) double decayFactor = 0.95; Double currentScore = redisTemplate.opsForZSet() .score(HOT_KEY, keyword); double newScore = (currentScore == null ? 0 : currentScore * decayFactor) + increment; redisTemplate.opsForZSet().add(HOT_KEY, keyword, newScore); } /** * 获取热搜榜 Top N */ public List<HotSearchItem> getTopHot(int topN) { Set<ZSetOperations.TypedTuple<String>> tuples = redisTemplate.opsForZSet() .reverseRangeWithScores(HOT_KEY, 0, topN - 1); List<HotSearchItem> result = new ArrayList<>(); int rank = 1; for (ZSetOperations.TypedTuple<String> tuple : tuples) { HotSearchItem item = new HotSearchItem(); item.setRank(rank++); item.setKeyword(tuple.getValue()); item.setHotScore(tuple.getScore()); result.add(item); } return result; } /** * 定时衰减 — 每分钟执行,所有热搜词分数 × 衰减因子 * 这样长时间没被搜索的词会逐渐掉出榜单 */ @Scheduled(cron = "0 * * * * ?") public void decayAllScores() { Set<String> keywords = redisTemplate.opsForZSet() .reverseRange(HOT_KEY, 0, -1); for (String keyword : keywords) { Double score = redisTemplate.opsForZSet() .score(HOT_KEY, keyword); if (score != null) { double newScore = score * 0.99; if (newScore < 0.1) { // 分数太低,直接移除 redisTemplate.opsForZSet().remove(HOT_KEY, keyword); } else { redisTemplate.opsForZSet().add(HOT_KEY, keyword, newScore); } } } } }

挑战5:推荐算法基础 — 协同过滤

推荐算法解决的核心问题:给用户看他可能喜欢的内容

类比理解:你喜欢看A作者的文章,推荐系统就给你推A作者的其他文章、或者跟A风格类似的文章。就像书店店员——你买了《三体》,他就推荐《银河帝国》。

基于用户的协同过滤(User-CF)
用户A喜欢帖子1,2,3 + 用户B喜欢帖子1,2,4 A和B相似→给A推荐帖子4
基于物品的协同过滤(Item-CF)
帖子1和帖子2被很多人同时喜欢 帖子1和帖子2相似 喜欢1的人也推荐2
对比维度 User-CF(基于用户) Item-CF(基于物品)
核心思想 找跟你相似的人,推荐他喜欢的 找跟你喜欢的相似的物品,推荐它
类比 你的朋友喜欢什么你就看什么 你喜欢的书旁边摆了什么书你就看什么
计算量 用户多了计算量大(用户相似度矩阵) 物品相对稳定,可离线计算
适用场景 用户数少、兴趣变化快(新闻推荐) 物品数少、用户数多(电商推荐)
典型应用 微博热搜、新闻推荐 淘宝商品推荐、Netflix电影推荐
// 简单的Item-CF推荐实现思路 @Service public class SimpleItemCFService { @Autowired private UserBehaviorRepository behaviorRepo; /** * 推荐帖子 — 基于物品的协同过滤 * * 步骤: * 1. 找出用户喜欢过的帖子列表 * 2. 对每个喜欢的帖子,找相似的帖子(被同一批人喜欢) * 3. 按相似度排序,推荐TopN * * 简化版:用"共同点赞用户数"衡量相似度 */ public List<String> recommend(String userId, int topN) { // 1. 获取用户喜欢过的帖子 List<String> likedPostIds = behaviorRepo.findLikedPostIds(userId); // 2. 计算相似帖子 Map<String, double> candidateScores = new HashMap<>(); for (String likedPostId : likedPostIds) { // 找跟这个帖子相似的其他帖子 // 相似度 = 共同点赞用户数 / 总点赞用户数 List<SimilarPost> similarPosts = behaviorRepo .findSimilarPosts(likedPostId, 20); for (SimilarPost sp : similarPosts) { // 排除用户已经看过的 if (!likedPostIds.contains(sp.getPostId())) { candidateScores.merge(sp.getPostId(), sp.getSimilarity(), Double::sum); } } } // 3. 按分数排序,取TopN return candidateScores.entrySet().stream() .sorted(Map.comparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); } }
🔑 面试加分:实际生产中,推荐系统远比这个复杂。还需要考虑:冷启动(新用户/新帖子怎么推荐)、多样性(不能只推一类内容)、实时性(用户新行为要立刻影响推荐)、AB测试(新旧算法对比)。这里只是入门级别的理解。

🏗️ 架构设计 — 内容社区架构

创作服务
帖子发布 图片/视频上传 草稿管理
审核服务
DFA敏感词过滤 AI图片/视频审核 人工审核后台 审核规则引擎
Feed服务
推模式分发 拉模式聚合 Timeline管理 推荐排序
互动服务
点赞/取消赞 评论/回复 转发/收藏 关注/取关
搜索服务
ES全文检索 热搜统计 搜索建议 反作弊
存储层
MySQL(帖子/评论) Redis(计数/Timeline/热搜) ES(搜索索引) OSS(图片/视频) MQ(异步处理)

🔄 关键流程 — 完整代码实现

流程1:发帖+审核+Feed推送完整代码

@RestController @RequestMapping("/api/posts") public class PostController { @Autowired private PostService postService; @Autowired private AuditService auditService; @Autowired private FeedService feedService; /** * 发布帖子 — 完整流程 */ @PostMapping("/publish") public Result<PostVO> publish(@RequestBody PublishRequest req, @RequestHeader("X-User-Id") String userId) { // 第1步:创建帖子 Post post = new Post(); post.setId(SnowflakeIdGenerator.nextId()); post.setAuthorId(userId); post.setContent(req.getContent()); post.setTitle(req.getTitle()); post.setImages(req.getImages()); post.setStatus(PostStatus.PENDING_REVIEW); // 待审核 post.setCreatedAt(System.currentTimeMillis()); postService.save(post); // 第2步:提交审核(异步) AuditResult textResult = auditService.auditText(post.getContent()); if (textResult.isViolation()) { // 明确违规,直接拒绝 post.setStatus(PostStatus.REJECTED); postService.save(post); return Result.fail("内容违规,发布失败"); } if (textResult.isSuspicious()) { // 疑似违规,进入人工审核 auditService.submitToManualReview(post); return Result.success("提交成功,等待人工审核"); } // 第3步:审核通过,发布 post.setStatus(PostStatus.PUBLISHED); postService.save(post); // 第4步:Feed分发 feedService.distributePost(post); // 第5步:建立搜索索引(异步) searchService.indexPost(post); return Result.success(PostVO.from(post)); } } // ========== 审核服务 ========== @Service public class AuditService { @Autowired private DFAFilter dfaFilter; @Autowired private ImageAuditClient imageAuditClient; /** * 文本审核 — DFA敏感词检测 */ public AuditResult auditText(String content) { FilterResult filterResult = dfaFilter.filter(content); if (!filterResult.getFoundWords().isEmpty()) { if (filterResult.getFoundWords().size() >= 3) { // 3个以上敏感词,直接拒绝 return AuditResult.violation(filterResult.getFoundWords()); } else { // 1-2个敏感词,可能误判,进入人工审核 return AuditResult.suspicious(filterResult.getFoundWords()); } } return AuditResult.pass(); } /** * 图片审核 — 调用AI审核服务 */ public AuditResult auditImages(List<String> imageUrls) { for (String url : imageUrls) { ImageAuditResult result = imageAuditClient.audit(url); if (result.isViolation()) { return AuditResult.violation(List.of("图片内容违规")); } } return AuditResult.pass(); } }

流程2:点赞+计数代码

// 点赞接口 + MQ异步持久化 @RestController @RequestMapping("/api/like") public class LikeController { @Autowired private LikeService likeService; @PostMapping("/{postId}") public Result<LikeVO> like(@PathVariable String postId, @RequestHeader("X-User-Id") String userId) { boolean success = likeService.like(postId, userId); long count = likeService.getLikeCount(postId); return Result.success(new LikeVO(success, count)); } @DeleteMapping("/{postId}") public Result<LikeVO> unlike(@PathVariable String postId, @RequestHeader("X-User-Id") String userId) { boolean success = likeService.unlike(postId, userId); long count = likeService.getLikeCount(postId); return Result.success(new LikeVO(success, count)); } } // ========== MQ消费者:点赞数据异步持久化到DB ========== @RocketMQMessageListener(topic = "like-persist", consumerGroup = "like-consumer") public class LikePersistConsumer implements RocketMQListener<LikeMessage> { @Autowired private LikeRepository likeRepo; @Override public void onMessage(LikeMessage msg) { LikeRecord record = new LikeRecord(); record.setPostId(msg.getPostId()); record.setUserId(msg.getUserId()); record.setCreatedAt(new Date(msg.getTimestamp())); likeRepo.save(record); } }

流程3:热搜更新代码

// 搜索接口 — 记录搜索词 + 返回结果 @RestController @RequestMapping("/api/search") public class SearchController { @Autowired private HotSearchService hotSearchService; @Autowired private PostSearchService postSearchService; /** * 搜索帖子 */ @GetMapping public Result<PageResult<PostVO>> search( @RequestParam String keyword, @RequestParam(defaultValue = "1") int page, @RequestHeader(value = "X-User-Id", required = false) String userId) { // 记录搜索词到热搜 if (userId != null) { hotSearchService.recordSearch(keyword, userId); } // 从ES搜索帖子 PageResult<PostVO> result = postSearchService.search(keyword, page); return Result.success(result); } /** * 获取热搜榜 */ @GetMapping("/hot") public Result<List<HotSearchItem>> getHotSearch( @RequestParam(defaultValue = "50") int topN) { List<HotSearchItem> hotList = hotSearchService.getTopHot(topN); return Result.success(hotList); } }

💥 踩坑实录 — 4个真实事故

坑1:明星发帖Feed写入风暴 — 1000万粉丝

现象:某明星发一条微博,需要写入1000万粉丝的收件箱,数据库写入QPS暴增,服务几乎不可用。

原因:推模式下,1条帖子 × 1000万粉丝 = 1000万次Redis/DB写入,即使异步也要好几分钟才能写完。

解决方案:异步 + 分批 + 大V走拉模式

// Feed写入优化 — 大V异步分批写入 public void pushToFollowersOptimized(Post post) { long followerCount = followRepo.countFollowers(post.getAuthorId()); if (followerCount > 10_000_000) { // 超大V:完全拉模式,不推 // 只写入作者时间线 writeToAuthorTimeline(post); } else if (followerCount > 10_000) { // 中等V:推给活跃粉丝,其他按需拉 pushToActiveFollowers(post); } else { // 普通用户:全量推,分批异步 List<String> followerIds = followRepo.findFollowerIds(post.getAuthorId()); // 分批100人,每批发一条MQ消息 Lists.partition(followerIds, 100).forEach(batch -> { mqTemplate.asyncSend("feed-push", new FeedPushMessage(post, batch)); }); } }
关键:1000万粉丝的大V,根本不能用推模式。只用拉模式——粉丝刷Feed时实时拉取大V的时间线。活跃粉丝(最近7天有登录)可以适当推,保证体验。

坑2:点赞数和实际不一致 — Redis和DB不同步

现象:页面上显示10万赞,但数据库里只有8万条记录。

原因:点赞先写Redis(快),再异步写DB(慢)。如果MQ消费延迟或丢失,Redis和DB数据就不一致了。

解决方案:定期对账 + 补偿机制

// 点赞数对账 — 每天凌晨3点执行 @Scheduled(cron = "0 0 3 * * ?") public void reconcileLikeCount() { // 扫描所有帖子 List<Post> posts = postRepo.findAllPublished(); for (Post post : posts) { // Redis中的点赞数 long redisCount = likeService.getLikeCount(post.getId()); // DB中的点赞数 long dbCount = likeRepo.countByPostId(post.getId()); if (redisCount != dbCount) { log.warn("点赞数不一致: postId={}, redis={}, db={}", post.getId(), redisCount, dbCount); // 以DB为准,修正Redis String countKey = "like:count:" + post.getId(); redisTemplate.opsForValue().set(countKey, String.valueOf(dbCount)); } } }

坑3:敏感词误杀 — 正常内容被拦截

现象:用户发"我今天考了杯具成绩"被拦截,因为"杯具"在敏感词库中(谐音"悲剧")。

原因:敏感词库太严格,缺少白名单机制。有些词本身有正常含义,不应一刀切。

解决方案:白名单 + 人工复核

// 改进后的审核逻辑 public AuditResult auditText(String content) { FilterResult result = dfaFilter.filter(content); if (result.getFoundWords().isEmpty()) { return AuditResult.pass(); } // 检查白名单:如果命中敏感词但上下文合法,放行 List<String> realViolations = new ArrayList<>(); for (String word : result.getFoundWords()) { if (whitelistService.isInWhitelist(word, content)) { // 白名单中且上下文合法,不算违规 continue; } realViolations.add(word); } if (realViolations.isEmpty()) { return AuditResult.pass(); } else if (realViolations.size() <= 2) { // 少量疑似,人工复核 return AuditResult.suspicious(realViolations); } else { // 多数违规,直接拒绝 return AuditResult.violation(realViolations); } }

坑4:热搜被刷 — 水军刷搜索词

现象:某个本来不热的话题突然冲上热搜第一,后来发现是水军批量搜索刷上去的。

原因:热搜只看搜索次数,没有反作弊机制。水军用脚本搜索10万次,就能把任何词刷上热搜。

解决方案:反作弊 + 权重衰减

// 热搜反作弊机制 public void recordSearchWithAntiCheat(String keyword, String userId) { // 1. 用户维度去重(同用户5分钟内只算1次) String dedupKey = "hotsearch:dedup:" + keyword + ":" + userId; if (!redisTemplate.opsForValue() .setIfAbsent(dedupKey, "1", 5, TimeUnit.MINUTES)) { return; } // 2. IP维度限流(同IP每分钟最多10次搜索) String ipRateKey = "hotsearch:ip:" + getCurrentIp(); Long ipCount = redisTemplate.opsForValue().increment(ipRateKey); if (ipCount == 1) { redisTemplate.expire(ipRateKey, 1, TimeUnit.MINUTES); } if (ipCount > 10) { // 同IP搜索过于频繁,疑似水军 return; } // 3. 用户权重(新注册/低活跃用户搜索权重低) double weight = getUserWeight(userId); // 老用户weight=1.0,新用户weight=0.1 // 4. 带权重更新热搜 updateHotRanking(keyword, weight); } private double getUserWeight(String userId) { User user = userRepo.findById(userId); if (user == null) return 0.1; // 注册不到7天的新用户权重低 if (user.getAgeInDays() < 7) return 0.1; // 从未发过帖的用户权重低 if (user.getPostCount() == 0) return 0.3; return 1.0; }

🔍 排查思路 — 问题定位方法论

1. Feed流为空?

# 1. 检查用户收件箱是否有数据 redis-cli ZCARD feed:inbox:{userId} # → 返回0:推模式写入失败,或用户没有关注任何人 # 2. 检查用户关注了谁 SELECT following_id FROM user_follow WHERE user_id = 'xxx'; # → 如果为空:用户没关注人,Feed当然为空 # 3. 检查关注人是否有新帖子 SELECT * FROM post WHERE author_id IN (...) ORDER BY created_at DESC LIMIT 10; # → 如果没有新帖:关注人没发帖,不是bug # 4. 检查MQ消费情况 查看feed-push消费组是否有积压 # → 积压:消费者处理不过来

2. 点赞不生效?

# 1. 检查Redis去重Set redis-cli SISMEMBER like:set:{postId} {userId} # → 返回1:已经赞过了,幂等返回 # 2. 检查Redis计数器 redis-cli GET like:count:{postId} # → 如果计数没变:increment命令可能没执行 # 3. 检查MQ是否消费 查看like-persist消费组延迟 # → 如果延迟大:DB写入还没完成,但Redis已更新

3. 搜索结果不对?

# 1. 检查ES索引 curl 'localhost:9200/post/_search?q=关键词' # → 如果搜不到:索引没有建立 # 2. 检查索引是否同步 curl 'localhost:9200/post/_count' # → 对比DB总数,如果差很多:同步有延迟 # 3. 检查Canal/MQ同步链路 查看binlog同步是否正常

4. 通用排查命令

$ redis-cli INFO memory # 查看Redis内存使用 $ redis-cli SLOWLOG GET 10 # 查看Redis慢查询 $ curl localhost:9200/_cat/health # ES集群健康状态 $ curl localhost:9200/_cat/indices # ES索引列表 $ rabbitmqctl list_queues name messages # MQ队列堆积

📝 面试高频 — 5大必考题

Q1:Feed流用推模式还是拉模式?

答题要点:推拉结合,根据场景选择

1. 推模式:发帖时写入所有粉丝收件箱。读时快,写时慢。适合普通用户(粉丝少)。
2. 拉模式:刷Feed时实时聚合。写时快,读时慢。适合大V(粉丝多)。
3. 推拉结合:普通用户推,大V拉。活跃粉丝推,不活跃粉丝拉。
4. 优化:推用MQ异步+分批;拉用Redis ZSET预存时间线。
5. 存储:Redis Sorted Set存Timeline,score=时间戳,ZREVRANGE分页。

Q2:百万级点赞怎么实现?

答题要点:Redis计数 + 异步持久化 + 对账

1. 计数:Redis String做计数器,INCR原子操作,O(1)。
2. 去重:Redis Set记录谁赞了,SADD+SISMEMBER,幂等。
3. 排行榜:Redis Sorted Set,ZINCRBY更新分数,ZREVRANGE取TopN。
4. 持久化:MQ异步写DB,不阻塞点赞操作。
5. 对账:定时任务对比Redis和DB数据,以DB为准修正Redis。

Q3:敏感词过滤怎么实现?

答题要点:DFA字典树 + 白名单 + 人工复核

1. DFA算法:把敏感词构建成字典树,逐字匹配,时间复杂度O(n)。
2. 为什么不用正则:正则需要逐词遍历,10万个敏感词就要跑10万次正则,太慢。DFA只需遍历文本一次。
3. 白名单:有些词有正常含义,需要根据上下文判断。
4. 多层审核:机审(DFA+AI)→ 疑似 → 人工。明确违规直接拒绝。
5. 图片审核:调用AI服务检测,不合规的进入人工审核。

Q4:热搜榜怎么实现?怎么防刷?

答题要点:Redis ZSET + 时间衰减 + 反作弊

1. 实时统计:Redis Sorted Set,key=热搜词,score=热度分数。搜索时ZINCRBY。
2. 时间衰减:定时任务每分钟把所有词分数×0.99,长时间没搜索的词逐渐掉出榜单。
3. 防刷:① 用户去重(5分钟内同用户同词只算1次)② IP限流 ③ 新用户降权 ④ 异常检测(短时间暴增触发告警)。
4. 滑动窗口:按分钟统计搜索量,只看最近1小时的热度。
5. 人工干预:后台可手动置顶/下架热搜词。

Q5:推荐系统的基本思路?

答题要点:协同过滤 + 内容推荐 + 冷启动

1. User-CF:找跟你相似的人,推荐他喜欢的。适合用户少、兴趣变化快的场景。
2. Item-CF:找跟你喜欢的相似的物品,推荐它。适合用户多、物品相对稳定的场景。
3. 内容推荐:根据帖子标签/内容相似度推荐。不需要用户行为数据。
4. 冷启动:新用户没有行为数据 → 热门推荐/标签推荐;新帖子没有互动 → 基于内容推荐给相似用户。
5. 混合策略:实际生产中通常是多种算法混合,用AB测试选最优。