You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于NestJS+Postgres+Flutter的社交应用Redis缓存与Feed实现问询

问题1:Redis缓存搜索结果并实现数据变更即时更新

1. 缓存键设计

针对不同搜索场景设计明确的缓存键,避免冲突:

  • 用户搜索:search:users:{query}:{page}:{size}(query为搜索关键词,page/size为分页参数)
  • 频道搜索:search:channels:{query}:{page}:{size}
  • 关注者列表:followers:{followableType}:{followableId}:{page}:{size}(followableType为user/channel,followableId为目标ID)

缓存值为序列化后的搜索结果JSON字符串,同时设置合理过期时间(比如5分钟),防止缓存长期失效。

2. 查询流程(先缓存后DB)

在NestJS服务中实现缓存优先的查询逻辑:

import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';

// 示例:用户搜索服务
async searchUsers(query: string, page: number, size: number) {
  const cacheKey = `search:users:${query}:${page}:${size}`;
  const cachedResult = await this.redisClient.get(cacheKey);
  
  if (cachedResult) {
    return JSON.parse(cachedResult);
  }
  
  // 缓存未命中,查询数据库
  const dbResult = await this.userRepository.search(query, page, size);
  // 写入缓存,设置5分钟过期
  await this.redisClient.setEx(cacheKey, 300, JSON.stringify(dbResult));
  
  return dbResult;
}

3. 数据变更时的缓存失效策略

当用户/频道数据(头像、名称、粉丝数等)变更,或关注关系变动时,主动删除关联缓存(而非更新缓存,避免并发一致性问题):

  • 用户/频道资料更新:删除对应搜索缓存前缀下的所有键
  • 关注/取消关注:删除关注者列表缓存及相关搜索缓存

实现批量删除前缀匹配的缓存键(基于Redis SCAN命令):

// 批量删除匹配指定前缀的缓存键
async deleteCacheByPrefix(prefix: string) {
  let cursor = '0';
  do {
    const [newCursor, keys] = await this.redisClient.scan(cursor, { match: `${prefix}*` });
    cursor = newCursor;
    if (keys.length > 0) {
      await this.redisClient.del(keys);
    }
  } while (cursor !== '0');
}

// 示例:用户资料更新后触发缓存失效
async updateUser(userId: string, updateDto: UpdateUserDto) {
  const updatedUser = await this.userRepository.update(userId, updateDto);
  // 删除所有用户搜索缓存
  await this.deleteCacheByPrefix('search:users:');
  // 删除该用户的详情缓存(若有)
  await this.redisClient.del(`user:${userId}:profile`);
  
  return updatedUser;
}

问题2:构建用户动态Feed系统

推荐使用推模式+Redis有序集合实现,兼顾实时性和查询效率,同时满足取消关注后移除对应内容的需求。

1. Feed存储设计

用Redis有序集合(Sorted Set)存储每个用户的Feed,核心设计:

  • 键名:feed:{userId}
  • 成员格式:{sourceType}:{sourceId}:{postId}(sourceType为user/channel,sourceId为发帖用户/频道ID,postId为帖子ID)
  • 分数:帖子创建时间戳(保证Feed按时间倒序排序)

2. 帖子推送逻辑

发布帖子时,异步将帖子推送给所有关注者的Feed:

// 示例:发布帖子服务
async createPost(postDto: CreatePostDto) {
  const post = await this.postRepository.save(postDto);
  const sourceType = post.channelId ? 'channel' : 'user';
  const sourceId = post.channelId || post.userId;
  const timestamp = post.createdAt.getTime();
  
  // 查询所有关注者
  const followers = await this.followerRepository.find({
    where: { followableType: sourceType, followableId: sourceId }
  });
  const followerIds = followers.map(f => f.followerId);
  
  // 用消息队列异步推送(避免阻塞主线程,推荐用@nestjs/bull)
  await this.feedQueue.add('push-post', {
    postId: post.id,
    sourceType,
    sourceId,
    timestamp,
    followerIds
  });
  
  return post;
}

// 队列处理器:执行Feed推送
@Processor('feed')
export class FeedProcessor {
  constructor(@InjectRedis() private readonly redisClient: Redis) {}

  @Process('push-post')
  async handlePushPost(job: Job) {
    const { postId, sourceType, sourceId, timestamp, followerIds } = job.data;
    const member = `${sourceType}:${sourceId}:${postId}`;
    
    // 批量添加到关注者的Feed
    const pipeline = this.redisClient.multi();
    followerIds.forEach(id => {
      pipeline.zAdd(`feed:${id}`, { score: timestamp, value: member });
    });
    await pipeline.exec();
    
    // 记录反向索引:存储哪些用户的Feed包含该帖子(用于帖子删除时清理)
    await this.redisClient.sAdd(`post:${postId}:feed-users`, followerIds);
  }
}

3. 取消关注时的Feed清理

利用Redis有序集合的字典序移除功能,批量清理来自被取消关注对象的所有帖子:

async unfollow(followDto: UnfollowDto) {
  const { followerId, followableType, followableId } = followDto;
  
  // 删除数据库中的关注关系
  await this.followerRepository.delete({ followerId, followableType, followableId });
  
  // 批量移除Feed中来自该对象的所有帖子
  const prefix = `${followableType}:${followableId}:`;
  await this.redisClient.zRemRangeByLex(`feed:${followerId}`, `[${prefix}`, `(${prefix}\xff`);
  
  // 更新反向索引:移除该用户在对应帖子的Feed记录
  const posts = await this.postRepository.find({
    where: followableType === 'channel' 
      ? { channelId: followableId } 
      : { userId: followableId }
  });
  for (const post of posts) {
    await this.redisClient.sRem(`post:${post.id}:feed-users`, followerId);
  }
}

4. Feed查询逻辑

按分页拉取Feed,并批量查询帖子详情:

async getFeed(userId: string, page: number, size: number) {
  const start = (page - 1) * size;
  const end = start + size - 1;
  
  // 从Redis拉取Feed成员(倒序,最新在前)
  const members = await this.redisClient.zRevRange(`feed:${userId}`, start, end);
  if (members.length === 0) return [];
  
  // 解析所有帖子ID
  const postIds = members.map(m => m.split(':')[2]);
  // 批量查询帖子(可缓存帖子详情优化性能)
  const posts = await this.postRepository.findByIds(postIds);
  
  // 保持Feed的顺序
  const postMap = new Map(posts.map(p => [p.id, p]));
  return members.map(m => postMap.get(m.split(':')[2])).filter(Boolean);
}

5. 优化建议

  • 对于百万级关注量的大V/频道,推送时可采用分批次异步处理,避免Redis连接过载
  • 为Feed设置过期时间,只保留最近30天的内容,更早的内容采用拉模式补充
  • 缓存帖子详情,减少DB查询次数:用post:{postId}缓存帖子数据,查询时优先读缓存

内容的提问来源于stack exchange,提问作者Paul Diamant

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 02:35:55