基于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
相关产品推荐
相关产品推荐

