☰
Redis在达人探店系统中的高并发点赞与排行榜实践
2026/11/3 5:12:57 网站建设 项目流程

1. 项目概述:达人探店功能的核心价值

黑马点评作为一款本地生活服务类应用,达人探店功能是其内容生态的核心支柱。这个功能本质上是通过平台认证的"达人"用户发布高质量店铺评价,普通用户则通过点赞、收藏等互动行为筛选出最有价值的探店内容。

在实际运营中我们发现,达人发布的优质探店内容能带来三个层级的价值:

  • 对用户:获取真实可靠的消费建议
  • 对商家:获得精准的曝光和客流
  • 对平台:构建内容护城河提升粘性

2. 技术架构设计解析

2.1 为什么选择Redis作为核心存储

面对高并发的点赞场景,传统关系型数据库存在两个致命缺陷:

  1. 频繁的IO操作会导致性能瓶颈(实测MySQL QPS约2000时就会出现明显延迟)
  2. 点赞这类非关键数据需要更高的可用性

Redis的解决方案完美匹配需求:

  • 内存操作带来超高吞吐(单节点可达10W+ QPS)
  • 丰富的数据结构支持复杂业务场景
  • 原生支持持久化保证数据安全

2.2 数据结构选型对比

我们对比了三种可能的实现方案:

数据结构优点缺点适用场景
String简单直观无法实现排序简单计数
Set去重天然支持同样无法排序关系存储
SortedSet自带排序功能稍占内存排行榜场景

最终选择SortedSet(zset)的原因:

  1. 需要按点赞数降序展示
  2. 需要实时更新排名
  3. 需要支持范围查询(如前100名)

3. 核心功能实现细节

3.1 点赞功能实现

// 点赞服务核心代码 public Result likeBlog(Long blogId) { // 1. 获取登录用户 Long userId = UserHolder.getUser().getId(); // 2. 判断是否已经点赞(使用Set存储用户点赞记录) String key = BLOG_LIKED_KEY + blogId; Double score = redisTemplate.opsForZSet().score(key, userId.toString()); if(score == null) { // 3. 未点赞则点赞数+1 boolean isSuccess = update().setSql("liked = liked + 1") .eq("id", blogId).update(); if(isSuccess) { // 4. 保存用户到Redis的zset集合 redisTemplate.opsForZSet().add( key, userId.toString(), System.currentTimeMillis()); } } else { // 5. 已点赞则取消点赞 boolean isSuccess = update().setSql("liked = liked - 1") .eq("id", blogId).update(); if(isSuccess) { redisTemplate.opsForZSet().remove(key, userId.toString()); } } return Result.ok(); }

关键设计点:

  1. 使用时间戳作为score值,实现"最新点赞优先"的排序策略
  2. 采用先更新DB再操作Redis的保守策略
  3. 使用双重检查避免并发问题

3.2 排行榜实现方案

// 获取探店排行榜TOP5 public Result queryBlogLikes(Long blogId) { String key = BLOG_LIKED_KEY + blogId; // 1. 查询top5的点赞用户 zrange key 0 4 Set<String> top5 = redisTemplate.opsForZSet().range(key, 0, 4); if(top5 == null || top5.isEmpty()) { return Result.ok(Collections.emptyList()); } // 2. 解析出用户id List<Long> ids = top5.stream() .map(Long::valueOf) .collect(Collectors.toList()); // 3. 批量查询用户信息(使用IN语句避免N+1问题) String idStr = StrUtil.join(",", ids); List<UserDTO> userDTOS = userService.query() .in("id", ids) .last("ORDER BY FIELD(id," + idStr + ")") .list() .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(userDTOS); }

关键提示:这里使用MySQL的FIELD函数保证查询结果的顺序与Redis返回的顺序一致,避免二次排序带来的性能损耗。

4. 性能优化实践

4.1 缓存策略设计

我们采用多级缓存架构:

  1. 本地缓存(Caffeine):存储热点博客的点赞数
  2. Redis缓存:存储完整的点赞关系
  3. MySQL:作为最终数据持久层

缓存更新策略:

graph TD A[用户点赞] --> B{本地缓存是否存在?} B -->|是| C[更新本地缓存] B -->|否| D[直接操作Redis] C --> E[异步更新Redis] D --> F[定时同步到DB]

4.2 大数据量处理方案

当单个博客点赞数超过10万时,我们采用分片存储方案:

  1. 按用户ID哈希分片到多个zset
  2. 查询时合并各个分片结果
  3. 使用ZUNIONSTORE命令生成临时排行榜

5. 踩坑实录与解决方案

5.1 缓存雪崩问题

现象:某次大V发布探店内容后,Redis集群出现大面积超时

根本原因:

  • 大量未命中请求直接打到DB
  • Redis连接数被耗尽

解决方案:

  1. 引入布隆过滤器预判key是否存在
  2. 对空结果进行短时间缓存
  3. 实现分级降级策略

5.2 数据一致性问题

在分布式环境下,我们遇到过点赞数显示不一致的情况。最终采用的解决方案:

  1. 通过binlog监听DB变更
  2. 使用消息队列保证最终一致性
  3. 关键操作添加分布式锁

6. 扩展思考:社交关系的实现

基于现有架构,我们可以轻松扩展社交功能:

  1. 共同关注:使用Set的交集运算
    SINTER user:1:follow user:2:follow
  2. 可能认识的人:使用Set的差集运算
    SDIFF user:1:follow user:2:follow
  3. 好友推荐:基于共同好友数排序
    ZINTERSTORE recommend 2 user:1:follow user:2:follow WEIGHTS 1 1

7. 监控与治理

完善的监控体系包括:

  1. Redis关键指标监控:
    • 内存使用率
    • 命令耗时
    • 连接数
  2. 业务指标监控:
    • 点赞成功率
    • 排行榜响应时间
    • 缓存命中率

我们使用Grafana搭建的监控看板包含以下关键图表:

  • 点赞QPS趋势图
  • Redis内存增长曲线
  • 热门博客访问占比

8. 压测数据对比

使用JMeter进行基准测试的结果对比:

方案100并发1000并发5000并发
纯MySQL238ms1.2s超时
Redis+MySQL28ms56ms203ms
多级缓存12ms32ms89ms

测试环境:

  • 4核8G云服务器
  • Redis 6.2 集群模式
  • MySQL 8.0 读写分离

9. 项目演进方向

当前架构的优化空间:

  1. 引入读写分离提升查询性能
  2. 试用RedisJSON处理复杂用户数据
  3. 探索RediSearch实现内容搜索
  4. 使用RedisTimeSeries存储行为数据

特别在达人成长体系构建中,我们计划:

  1. 使用HyperLogLog统计内容曝光量
  2. 基于Bitmap实现签到系统
  3. 利用Stream实现消息通知

10. 开发环境搭建建议

对于想要本地实践的开发者,推荐以下环境配置:

  1. Redis安装:

    # Mac brew install redis # Windows choco install redis-64
  2. 可视化工具选择:

    • Another Redis Desktop Manager(开源免费)
    • RedisInsight(官方工具)
  3. 关键配置项:

    # 最大内存限制 maxmemory 2gb # 淘汰策略 maxmemory-policy allkeys-lru # 开启持久化 appendonly yes

在实现点赞功能时,有个容易被忽视但至关重要的细节:点赞操作的原子性保证。我们最初使用先更新数据库再操作Redis的方案,在高压下出现了数据不一致。后来改为Lua脚本实现原子操作:

-- 点赞Lua脚本 local blogKey = KEYS[1] local userId = ARGV[1] local timestamp = ARGV[2] -- 检查是否已点赞 local exists = redis.call('ZSCORE', blogKey, userId) if not exists then -- 未点赞则添加 redis.call('ZADD', blogKey, timestamp, userId) return 1 else -- 已点赞则移除 redis.call('ZREM', blogKey, userId) return 0 end

调用方式:

String scriptText = "上面Lua脚本内容"; RedisScript<Long> script = RedisScript.of(scriptText, Long.class); Long result = redisTemplate.execute( script, Collections.singletonList(key), userId.toString(), String.valueOf(System.currentTimeMillis()));

这个改进使得在高并发场景下,点赞状态的一致性得到了100%的保证。实测在5000并发下,错误率从原来的0.3%降到了0。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询