模块四接口限流固定窗口 Redis计数器1. 设计目标Feed 流接口是高频读接口用户刷首页、下拉加载更多都会触发。如果没有限流可能出现恶意脚本高频刷接口打爆 Redis客户端 bug 导致无限重试单一用户耗尽连接池影响其他人限流目标保护系统不被单一用户拖垮在正常体验和系统稳定之间做平衡返回友好提示而不是直接崩溃2. 方案选型方案原理优点缺点固定窗口时间窗口内计数简单、内存小窗口边界可能突刺滑动窗口更精细的时间分片平滑实现复杂令牌桶按速率生成令牌允许突发需维护状态我的选择固定窗口 Redis 计数器理由实现简单5 行核心代码依赖 Redis 原子操作无并发问题3. 核心流程4. 关键代码限流工具类 RateLimiterUtil.javapackage com.feed.util; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; Component public class RateLimiterUtil { Autowired private RedisTemplateString, String redisTemplate; /** * 固定窗口限流 * param key 限流key * param maxRequests 时间窗口内最大请求次数 * param timeWindow 时间窗口 * return true 放行false 被限流 */ public boolean allowRequest(String key, int maxRequests, int timeWindow) { String redisKey rate_limit: key; Long count redisTemplate.opsForValue().increment(redisKey); // 第一次请求设置过期时间 if (count 1) { redisTemplate.expire(redisKey, timeWindow, TimeUnit.SECONDS); } return count maxRequests; } }在 FeedController 中使用GetMapping public ResultListArticle getfeed(RequestParam Long userId, RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size) { // 用户级限流10秒内最多5次 if (!rateLimiterUtil.allowRequest(userId.toString(), 5, 10)) { return Result.error(429, 请求太频繁请稍后再试); } // 正常业务逻辑... return Result.success(feedService.selectMode(userId, page, size)); }5. 限流效果验证用 JMeter 压测/feed?userId1请求次数预期结果前 5 次正常返回数据第 6 次返回 429提示请求太频繁控制台日志用户 1 走推模式 用户 1 走推模式 用户 1 走推模式 用户 1 走推模式 用户 1 走推模式 请求被限流: 用户 1 请求太频繁6. 为什么不直接用网关限流对比项网关限流应用层限流粒度通常是 IP 级别可以是用户 ID 级别灵活性较低高可结合业务逻辑依赖需要网关组件只需 Redis当前项目是单体架构应用层限流更简单、可控。7. 后续优化方向优化点说明优先级全局限流防止多用户同时洪峰中令牌桶平滑限流避免窗口边界突刺低限流熔断降级返回降级数据而非报错低模块五推拉结合Feed流核心架构1. 设计目标Feed 流系统有两种经典模式模式原理优点缺点推模式发文时写入粉丝收件箱读快5ms写扩散严重拉模式读时实时查询关注作者写成本低读慢83ms核心矛盾全推大V发文要推给千万粉丝 → 写爆炸全拉用户刷首页要实时聚合 → 读爆炸目标小V全推体验好大V推拉结合节约资源2. 方案设计3. 核心实现3.1 判断是否大V// 查询作者粉丝数 User author userMapper.selectById(article.getAuthorId()); boolean isBigV author.getFanCount() 10000;3.2 大V只推活跃粉丝// 活跃阈值7天内登录 long activeThreshold System.currentTimeMillis() - 7 * 24 * 3600 * 1000; for (Follow fan : fans) { User fanUser userMapper.selectById(fan.getUserId()); boolean isActive fanUser.getLastLoginTime() ! null fanUser.getLastLoginTime().getTime() activeThreshold; if (isActive) { // 推送 String key inbox: fan.getUserId(); redisTemplate.opsForZSet().add(key, articleId, article.getCreateTime().getTime()); } else { // 跳过不推送 skippedCount; } }3.3 小V全推if (!isBigV) { for (Follow fan : fans) { String key inbox: fan.getUserId(); redisTemplate.opsForZSet().add(key, articleId, article.getCreateTime().getTime()); } }3.4 粉丝拉取时的自动切换public ListArticle selectMode(Long userId, int page, int size) { User user userMapper.selectById(userId); boolean isActive isUserActive(user); if (isActive) { // 活跃用户走推模式读 Redis 收件箱 return getFeedFromInbox(userId, page, size); } else { // 不活跃用户走拉模式实时查 MySQL return pullFeedFromMySQL(userId, page, size); } }4. 效果验证模式QPS平均响应存储成本全推18575ms高全拉88683ms低推拉结合活跃用户 1857 / 不活跃 8865ms / 83ms降低 70%压测数据大V发文推送2个活跃粉丝跳过2个不活跃粉丝活跃粉丝刷首页走推模式响应5ms不活跃粉丝刷首页走拉模式响应83ms5. 成本收益分析方案存储成本写扩散压力读延迟活跃用户读延迟不活跃用户全推极高极大5ms5ms浪费全拉极低极小83ms83ms推拉结合低小5ms83ms可接受核心 trade-off用不活跃用户的少量读延迟换取海量存储成本和写扩散压力的数量级下降。6. 为什么不直接用推模式设置过期时间问题说明过期时间如何设置太短会丢失历史文章太长内存仍高冷热分离不彻底不活跃用户仍然占用内存写扩散压力仍在大V发文时还是要推给所有人推拉结合从源头上解决了问题不活跃用户根本不写入收件箱。7. 后续优化方向优化点说明优先级基于用户行为动态调整阈值粉丝数阈值可配置低分级更精细如粉丝活跃度打分不只是“登录/未登录”低不活跃用户收件箱清理定期清理超期未访问的收件箱中总结两个模块的价值模块核心价值面试亮点接口限流保护系统不被刷爆用户级限流 固定窗口 429友好提示推拉结合平衡性能和成本大V只推活跃粉丝 压测数据 1857 vs 886