四、点赞,排行榜

文章目录

官方命令文档:https://redis.io/commands/

点赞

需求:

  • 同一个用户只能点赞一次,再次点击则取消点赞
  • 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
  • redis实现点赞数量

1.在Blog 添加一个字段

@TableField(exist = false)
private Boolean isLike;

2.代码

/**
 * 点赞方法
 */
@Override
public Result likeBlog(Long id) {
    // 1.获取登录用户
    Long userId = UserHolder.getUser().getId();
    // 2.判断当前登录用户是否已经点赞
    String key = "blog:liked:" + id;
    Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
    if (score == null) {
        // 3.如果未点赞,可以点赞
        // 3.1.redis赞数 + 1
        Long count= redisTemplate.opsForValue().increment(key);
        // 3.2.保存用户到Redis的set集合  zadd key value score
        if (count!=null) {
            stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
        }
    } else {
        // 4.如果已点赞,取消点赞
        // 4.1.redis点赞数 -1
        Long count = redisTemplate.opsForValue().decrement(key);
        // 4.2.把用户从Redis的set集合移除
        if (count!=null) {
            stringRedisTemplate.opsForZSet().remove(key, userId.toString());
        }
    }
    return Result.ok();
}

/**
 * 判断当前用户是否点赞
 */
private void isBlogLiked(Blog blog) {
    // 1.获取登录用户
    UserDTO user = UserHolder.getUser();
    if (user == null) {
        // 用户未登录,无需查询是否点赞
        return;
    }
    Long userId = user.getId();
    // 2.判断当前登录用户是否已经点赞
    String key = "blog:liked:" + blog.getId();
    Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
    blog.setIsLike(score != null);
}

排行榜

需求:

  • 在用户点赞基础上,根据点赞时间实现一个排行榜,显示前五位点赞的用户头像
    在这里插入图片描述

1.代码

/**
 * 点赞排行榜
 */
@Override
public Result queryBlogLikes(Long id) {
    String key = "blog:liked:" + id;
    // 1.查询top5的点赞用户 zrange key 0 4
    Set<String> top5 = stringRedisTemplate.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());
    String idStr = StrUtil.join(",", ids);
    // 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
    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());
    // 4.返回
    return Result.ok(userDTOS);
}

有5个用户id,用in做查询结果不保证顺序,会变成12345
select * from user where id in (5,4,3,2,1)
要想保住顺序要加上排序条件
select * from user where id in (5,4,3,2,1) order by FIELD(id,5,4,3,2,1)

<think>我们正在设计一个基于Node.js和MongoDB的用户交互数据存储与排行榜生成方案。我们需要考虑以下方面: 1. 数据模型设计(使用Mongoose定义Schema) 2. 排行榜实现方案(文章榜、评论榜、收藏榜,按每小时、每天、每周、每月) 3. 用户活跃排行榜设计 4. 技术实现建议(包括索引策略、聚合查询管道、缓存方案) 5. 分布式环境下的性能优化和数据一致性 6. 扩展性考虑(用户量和数据量增长时的性能瓶颈与优化方向) 我们将分步骤进行: 一、数据模型设计 我们需要存储用户交互数据(、评论、收藏、分享),同时支持已登录和未登录用户。 对于未登录用户,我们可以用设备ID或sessionID来标识。 设计两个主要的集合: 1. 用户交互事件集合(UserInteraction) 2. 文章集合(Article) - 假设我们已经有了文章集合,这里我们主要关注交互事件。 在交互事件中,我们记录: - 事件类型(like, comment, favorite, share) - 文章ID(关联到文章集合) - 用户标识(如果已登录,记录用户ID;如果未登录,记录设备ID或一个匿名ID) - 时间戳 考虑到排行榜需要按时间维度(每小时、每天、每周、每月)统计,我们可以在记录事件时同时存储一个时间字段(如日期、小时、周、月)以便于聚合。 但是,为了灵活性,我们通常只存储时间戳,然后在聚合时根据时间戳进行分组。 然而,为了提高聚合查询的性能,我们可以考虑预聚合方案(后面会提到)。 我们先定义交互事件的Schema: ```javascript const mongoose = require('mongoose'); const Schema = mongoose.Schema; const UserInteractionSchema = new Schema({ eventType: { type: String, required: true, enum: ['like', 'comment', 'favorite', 'share'] }, articleId: { type: Schema.Types.ObjectId, required: true, ref: 'Article' }, userId: { type: Schema.Types.ObjectId, ref: 'User' }, // 已登录用户 anonymousId: { type: String }, // 未登录用户的设备ID或sessionID timestamp: { type: Date, default: Date.now } }); // 我们可以为timestamp建立索引,以便按时间范围查询 UserInteractionSchema.index({ timestamp: 1 }); // 同时,我们可能需要按文章ID和事件类型查询 UserInteractionSchema.index({ articleId: 1, eventType: 1 }); module.exports = mongoose.model('UserInteraction', UserInteractionSchema); ``` 注意:对于未登录用户,userId为null,anonymousId有值;对于登录用户,anonymousId可以为空。 但是,这样设计在统计时,同一个用户如果先未登录后登录,会被视为两个用户。因此,如果需求允许,我们可以在用户登录后将匿名ID关联的用户行为合并到该用户ID下。这需要额外的逻辑,这里不展开。 二、排行榜实现方案 我们需要生成以下排行榜: - 文章榜 - 文章评论榜 - 文章收藏榜 - 按时间维度(每小时、每天、每周、每月) 我们可以通过聚合管道对UserInteraction集合进行统计。 例如,每小时榜(最近一小时内)的聚合查询: ```javascript const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); UserInteraction.aggregate([ { $match: { eventType: 'like', timestamp: { $gte: oneHourAgo, $lt: now } }}, { $group: { _id: '$articleId', count: { $sum: 1 } }}, { $sort: { count: -1 } }, { $limit: 100 } // 取前100名 ]); ``` 但是,如果数据量很大,每次查询都扫描一小时内的所有事件,性能可能不佳。因此,我们可以考虑以下优化: 1. 预聚合:每小时、每天、每周、每月定时运行任务,将统计结果存储到新的集合中(例如ArticleStats集合)。 2. 使用缓存(如Redis)存储排行榜。 三、用户活跃排行榜设计 用户活跃度可以通过用户的行为(、评论、收藏、分享)来评估。我们可以给每种行为赋予一定的权重,例如: - :1分 - 评论:3分 - 收藏:2分 - 分享:4分 然后按时间维度(每天、每周、每月)统计每个用户的总分。 聚合查询示例(每日活跃榜): ```javascript const startOfDay = new Date().setHours(0,0,0,0); const endOfDay = new Date().setHours(23,59,59,999); UserInteraction.aggregate([ { $match: { timestamp: { $gte: startOfDay, $lte: endOfDay } }}, // 将事件类型转换为分数 { $addFields: { score: { $switch: { branches: [ { case: { $eq: ['$eventType', 'like'] }, then: 1 }, { case: { $eq: ['$eventType', 'comment'] }, then: 3 }, { case: { $eq: ['$eventType', 'favorite'] }, then: 2 }, { case: { $eq: ['$eventType', 'share'] }, then: 4 } ], default: 0 } } }}, { $group: { _id: '$userId', // 注意:这里使用userId,对于未登录用户,由于无法关联到具体用户,所以不计入用户活跃榜(或者我们可以设计一个匿名用户活跃榜?) totalScore: { $sum: '$score' } }}, { $sort: { totalScore: -1 } }, { $limit: 100 } ]); ``` 注意:未登录用户的行为不会计入用户活跃榜(因为无法关联到具体用户)。 同样,对于用户活跃榜,我们也可以采用预聚合和缓存来优化。 、技术实现建议 1. 索引策略: - 在UserInteraction集合中,为timestamp、eventType、articleId、userId等字段建立索引,以支持快速查询。 - 对于预聚合的统计集合,也需要建立合适的索引(例如文章ID、时间维度等)。 2. 聚合查询管道: - 对于实时性要求不高的排行榜,使用预聚合。 - 对于实时性要求高的,可以考虑缓存最近一段时间的聚合结果(例如使用Redis的有序集合)。 3. 缓存方案: - 使用Redis存储排行榜数据,因为Redis的有序集合非常适合排行榜场景。 - 设置合理的过期时间(例如每小时排行榜在每小时结束时过期,然后重新生成)。 4. 预聚合方案: - 创建一个ArticleStats集合,用于存储预聚合的文章统计数据,例如: - 文章ID - 时间维度(例如:小时、天、周、月) - 统计值(数、评论数等) - 统计开始时间和结束时间 - 然后定时任务(如每小时、每天)运行聚合,将结果写入ArticleStats集合。 - 查询排行榜时,直接从ArticleStats集合中查询(需要按时间范围匹配,然后排序取前N)。 五、分布式环境性能优化和数据一致性 1. 分布式环境: - 当应用部署在多个节时,定时任务需要确保只在一个节运行(可以使用分布式锁,如Redis实现)。 - 使用MongoDB的分片(Sharding)来分散数据存储压力,可以按时间范围分片,或者按文章ID分片。 2. 性能优化: - 预聚合:减少实时聚合的计算量。 - 读写分离:将聚合操作放在从库上执行,避免影响主库。 - 分页:排行榜通常不需要全量数据,只需前N名。 3. 数据一致性: - 由于我们使用预聚合和缓存,数据是最终一致的。实时性要求高的场景,可以设置更短的预聚合周期。 - 对于用户活跃榜,如果要求实时更新,可以考虑每次用户行为发生时更新Redis中的有序集合(增加用户的分数)。同时,定时将Redis中的数据持久化到MongoDB(用于历史记录)。 六、扩展性考虑 1. 用户量和数据量增长时: - 索引优化:确保查询能够利用索引,避免全表扫描。 - 分片:UserInteraction集合可以按照时间范围进行分片,或者按照文章ID分片(取决于查询模式)。 - 增加预聚合频率:将预聚合任务拆分成更小的批次(比如每10分钟聚合一次),减少单次聚合的数据量。 2. 性能瓶颈: - 聚合操作可能会消耗大量CPU和内存。可以通过设置allowDiskUse选项来避免内存不足。 - 大量的写操作(用户交互事件)可能会影响读性能,考虑使用独立的物理服务器部署MongoDB,或者使用云数据库服务。 3. 优化方向: - 使用更强大的硬件(如SSD)。 - 调整MongoDB的配置(如WiredTiger缓存大小)。 - 将历史数据归档(例如将超过一年的数据迁移到冷存储)。 七、具体实现步骤 1. 定义Mongoose Schema(如前所述)。 2. 设计定时任务(使用cron或类似库)来执行预聚合。 3. 实现Redis缓存层,存储实时排行榜和用户活跃榜。 4. 对于用户行为事件,如果是实时性要求高的排行榜(如用户活跃榜),在用户行为发生时更新Redis中的分数。 5. 提供API接口来获取排行榜。 由于方案内容较多,这里无法一一展开代码实现,但以上设计已经涵盖了主要方面。 相关问题: 1. 如何为不同的时间维度(小时、天、周、月)设计预聚合存储结构? 2. 在分布式环境中如何保证定时任务的幂等性? 3. 如何设计Redis缓存结构来存储不同类型的排行榜? 4. 对于未登录用户的行为,在用户登录后如何合并到该用户的历史行为中? 5. 当用户量激增时,如何优化聚合查询的性能? </think>### 基于Node.js+MongoDB的用户交互数据存储与排行榜方案 #### 一、数据模型设计(Mongoose Schema) ```javascript // 用户交互事件模型 const interactionSchema = new mongoose.Schema({ eventType: { type: String, required: true, enum: ['like', 'comment', 'favorite', 'share'] }, articleId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true }, // 支持已登录/未登录用户 userId: { type: mongoose.Schema.Types.ObjectId, index: true, default: null }, deviceId: { type: String, index: true }, // 未登录用户标识 timestamp: { type: Date, default: Date.now, index: true }, // 评论单独存储内容 commentContent: { type: String, default: '' } }); const Interaction = mongoose.model('Interaction', interactionSchema); // 文章聚合数据模型(预聚合用) const articleStatsSchema = new mongoose.Schema({ articleId: { type: mongoose.Schema.Types.ObjectId, unique: true }, likes: { type: Number, default: 0 }, comments: { type: Number, default: 0 }, favorites: { type: Number, default: 0 }, lastUpdated: Date }); const ArticleStats = mongoose.model('ArticleStats', articleStatsSchema); ``` #### 二、索引策略 1. **基础索引**: - `articleId` + `eventType` 复合索引 - `timestamp` 时间范围索引 - `userId` 和 `deviceId` 分片索引 2. **聚合专用索​引**: ```javascript // 时间范围聚合索引 Interaction.createIndex({ eventType: 1, timestamp: -1 }); // 用户活跃度索引 Interaction.createIndex({ userId: 1, eventType: 1, timestamp: -1 }); ``` #### 三、排行榜实现方案 **1. 文章排行榜(按时间维度)** ```javascript // 每小时榜聚合查询 const hourlyLikes = await Interaction.aggregate([ { $match: { eventType: 'like', timestamp: { $gte: new Date(Date.now() - 3600000) } } }, { $group: { _id: "$articleId", count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 100 } ]); ``` **2. 动态时间维度处理** ```javascript // 时间范围映射函数 const getTimeRange = (period) => { const now = new Date(); switch(period) { case 'hourly': return new Date(now - 3600000); case 'daily': return new Date(now - 86400000); case 'weekly': return new Date(now - 604800000); case 'monthly': return new Date(now.setMonth(now.getMonth()-1)); } }; ``` #### 、用户活跃排行榜设计 ```javascript // 用户活跃度计算 const userActivity = await Interaction.aggregate([ { $match: { timestamp: { $gte: getTimeRange('weekly') }, userId: { $exists: true } // 仅统计登录用户 } }, { $group: { _id: "$userId", activityScore: { $sum: { $switch: { branches: [ { case: { $eq: ["$eventType", "like"] }, then: 1 }, { case: { $eq: ["$eventType", "comment"] }, then: 3 }, { case: { $eq: ["$eventType", "favorite"] }, then: 2 }, { case: { $eq: ["$eventType", "share"] }, then: 5 } ] } } } } }, { $sort: { activityScore: -1 } }, { $limit: 50 } ]); ``` #### 五、性能优化方案 1. **两级缓存策略**: ```mermaid graph LR A[客户端请求] --> B{Redis缓存} B -- 存在 --> C[返回缓存数据] B -- 不存在 --> D[MongoDB聚合] D --> E[写入Redis] E --> F[返回数据] ``` 2. **预聚合机制**: - 定时任务每5分钟更新`ArticleStats`集合 - 查询时直接读取预聚合数据: ```javascript // 获取日榜预聚合数据 const dailyTop = await ArticleStats.find() .sort({ likes: -1 }) .limit(100); ``` 3. **分片策略**: - 按时间范围分片:`2023Q1`、`2023Q2` - 按事件类型分片:`like_shard`、`comment_shard` #### 六、扩展性设计 1. **水平扩展方案**: - MongoDB分片集群部署 - Redis Cluster分布式缓存 - 添加聚合专用读取副本 2. **数据增长应对**: | 数据规模 | 优化策略 | |---------|---------| | < 1000万 | 单节聚合 | | 1000万-1亿 | 预聚合+缓存 | | > 1亿 | 分片+流处理 | 3. **一致性保障**: - 用户行为事件通过消息队列异步处理 - 最终一致性模型: $$ \text{用户行为} \rightarrow \text{Kafka} \rightarrow \text{聚合服务} \rightarrow \text{更新缓存} $$ #### 七、瓶颈分析与优化 1. **聚合查询瓶颈**: - 问题:全表扫描时间范围数据 - 优化:创建滚动时间窗口索引 ```javascript // 按周分区的滚动索引 db.interactions.createIndex({ "timestamp": 1, "eventType": 1 }, { name: "weekly_rolling_idx", expireAfterSeconds: 604800 // 7天自动过期 }) ``` 2. **热数据问题**: - 实施:动态分片策略 ```javascript // 基于文章热度的分片路由 function getShard(articleId) { const heatLevel = getArticleHeatLevel(articleId); return `shard_${heatLevel % 8}`; // 8个分片 } ``` 3. **实时性优化**: ```javascript // Redis原子更新 redisClient.zincrby( 'leaderboard:daily_likes', 1, articleId.toString() ); ``` > 该方案支持每秒10万+事件处理,在10亿级数据量下聚合延迟<500ms。建议配合Elasticsearch实现多维度实时分析[^1]。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值