Interaction服务
问题1:在这个项目中,点赞功能使用了Redis Lua脚本来实现,请解释:
1. 为什么需要使用Lua脚本而不是普通的Redis命令?
2. 这段Lua脚本是如何保证原子性和幂等性的?
3. 如果Lua脚本执行失败,应该如何处理?
参考答案 :
1. 为什么使用Lua脚本 :
- 原子性 :Lua脚本在Redis中是原子执行的,不会被其他命令打断,可以保证多个操作的一致性
- 减少网络开销 :将多次Redis操作合并到一个脚本中执行,减少网络往返时间
- 复杂逻辑 :可以在脚本中实现更复杂的业务逻辑判断
2. 原子性和幂等性保证 :
-- 检查用户是否已经存在于点赞集合中(幂等性关键)
local is_member = redis.call('SISMEMBER', KEYS[1], ARGV[1])
-- 如果用户已经点赞,直接返回当前计数(不执行写操作)
if is_member == 1 then
local current_count = redis.call('GET', KEYS[2])
return {1, tonumber(current_count or 0)}
end
-- 执行点赞操作(原子性)
redis.call('SADD', KEYS[1], ARGV[1]) -- 加入集合
local new_count = redis.call('INCR', KEYS[2]) -- 计数器+1
return {0, new_count}
原子性 :整个Lua脚本在Redis中作为一个整体执行,中间不会插入其他命令 幂等性 :通过 SISMEMBER 检查用户是否已点赞,避免重复计数
3. 失败处理 :
- Redis客户端会返回错误,应用层需要捕获并处理
- 可以选择:重试、降级到数据库操作、记录日志告警
- 确保最终一致性:通过消息队列异步补偿或定期对账
问题2:缓存版本控制策略
项目中使用了缓存版本号机制(如 PostCacheVersionKey ),请解释:
1. 这种缓存失效策略的原理是什么?
2. 相比直接删除缓存key,这种方式有什么优缺点?
3. 在什么场景下适合使用这种策略?
参考答案 :
1. 原理 :
// common/redis/keys.go:16
PostCacheVersionKey = KeyPrefix + "post:cache_version:"
// service/content/rpc/internal/logic/cache_helper.go:34-40
func buildPostDetailCacheKey(ctx context.Context, svcCtx *svc.ServiceContext, postID uint64) string {
return fmt.Sprintf("%s%s:v%s", redis.PostInfoKey, strconv.FormatUint(postID, 10),
getPostCacheVersion(ctx, svcCtx, postID))
}
// 失效缓存时,只需要增加版本号
func invalidatePostCaches(ctx context.Context, svcCtx *svc.ServiceContext, postID uint64) {
_, _ = svcCtx.Redis.Incr(ctx, fmt.Sprintf("%s%d", redis.PostCacheVersionKey, postID))
}
工作流程 :
- 缓存key中包含版本号: schill:post:info:{id}:v{version}
- 需要失效缓存时,不删除key,而是 INCR 版本号
- 下次读取时使用新版本号,自然读取不到旧缓存
2. 优缺点 :
优点 :
- 避免缓存击穿:旧缓存key仍然存在,直到过期,大量并发请求不会同时打到数据库
- 实现简单:只需要维护一个版本号key
- 可以优雅降级:即使版本号操作失败,仍可使用旧版本缓存
- 减少网络开销:不需要遍历和删除多个相关缓存key
缺点 :
- 内存占用:旧缓存key会在Redis中保留直到过期
- 缓存不一致窗口期:在版本号更新后,旧缓存仍然有效(短暂时间)
- 需要额外存储:每个实体需要维护一个版本号key
3. 适用场景 :
- 读多写少的场景
- 对一致性要求不是特别严苛(接受短暂不一致)
- 需要防止缓存击穿的热点数据
- 缓存key较多,失效成本高的场景
问题3:消息队列的异步处理模式
项目中使用Kafka处理各种事件(如 PostStarMessage ),请分析:
1. 为什么点赞成功后要发送消息而不是直接更新数据库?
2. 这种异步模式可能会带来什么问题?如何解决?
3. 消息发送失败怎么办?
参考答案 :
1. 为什么使用异步 :
// service/interaction/rpc/internal/logic/starpostlogic.go:59-78
if status == 0 {
go func() {
msg := mq.PostStarMessage{
PostID: in.PostId,
UserID: in.UserId,
Timestamp: timestamp,
}
if err := l.svcCtx.KafkaProducer.SendEvent(...); err != nil {
logx.Errorf("send post star event failed: %v", err)
}
}()
}
原因 :
- 性能优化 :Redis操作很快,用户可以立即得到响应,数据库更新异步进行
- 解耦 :互动服务不需要直接依赖内容服务的数据库更新逻辑
- 削峰填谷 :高并发点赞时,消息队列可以缓冲请求
- 扩展性 :可以有多个消费者处理同一事件(如更新计数、生成Feed、通知等)
2. 异步模式的问题与解决方案 :

comment服务
问题:评论投票 Lua 脚本原子性实现
问题 :分析 votecommentlogic.go 中的 Lua 脚本:
1. 这个脚本解决了什么问题?
2. 如何保证幂等性?
3. 如果 Redis 宕机,数据如何恢复?
const voteScript = `
local voteKey = KEYS[1]
local infoKey = KEYS[2]
local newVote = ARGV[1]
local expire = ARGV[2]
-- 获取旧的投票状态
local oldVote = redis.call('get', voteKey)
if not oldVote then
oldVote = '0'
end
-- 如果状态没有变化,直接返回当前计数
if oldVote == newVote then
local likeCount = redis.call('hget', infoKey, 'like_count') or '0'
local dislikeCount = redis.call('hget', infoKey, 'dislike_count') or '0'
return {likeCount, dislikeCount, newVote}
end
-- 计算计数变化
local likeDelta = 0
local dislikeDelta = 0
-- 减去旧状态的计数
if oldVote == '1' then
likeDelta = -1
elseif oldVote == '2' then
dislikeDelta = -1
end
-- 加上新状态的计数
if newVote == '1' then
likeDelta = likeDelta + 1
elseif newVote == '2' then
dislikeDelta = dislikeDelta + 1
end
-- 应用修改
if newVote == '0' then
redis.call('del', voteKey)
else
redis.call('set', voteKey, newVote)
redis.call('expire', voteKey, expire)
end
if likeDelta ~= 0 then
redis.call('hincrby', infoKey, 'like_count', likeDelta)
end
if dislikeDelta ~= 0 then
redis.call('hincrby', infoKey, 'dislike_count', dislikeDelta)
end
-- 返回最新的计数
local likeCount = redis.call('hget', infoKey, 'like_count') or '0'
local dislikeCount = redis.call('hget', infoKey, 'dislike_count') or '0'
return {likeCount, dislikeCount, newVote}
`
1. 解决的问题 :
- 原子性 :多个 Redis 操作原子执行,防止并发问题
- 数据一致性 :投票状态和计数保持一致
- 性能优化 :减少网络开销
2. 幂等性保证 :
```
-- 检查状态是否变化
if oldVote == newVote then
-- 直接返回,不执行修改
return {likeCount, dislikeCount, newVote}
end
```
- 相同请求多次执行结果一致
- 不会重复计数
3. Redis 宕机恢复策略 :
- 方案A:Kafka 事件回溯 (当前实现)
// 1. 投票先写 Redis,然后发送 Kafka
go func() {
if err := l.svcCtx.KafkaProducer.SendMessage(
l.svcCtx.Config.KqProducerConf.TopicCommentVote,
voteEvent); err != nil {
logx.Errorf("发送投票事件消息失败: %v", err)
}
}()
// 2. Consumer 消费消息落库
func (c *CommentConsumer) handleVoteEvent(event mq.VoteEvent) error {
// 数据库持久化投票记录
// 更新计数
}
方案B:定期对账
func reconcileVoteCounts() {
// 1. 扫描数据库最近1小时的投票记录
// 2. 与 Redis 中的计数对比
// 3. 不一致时以数据库为准
}
方案C:降级处理
// 当前代码已实现降级
if err != nil {
logx.Errorf("执行投票Lua脚本失败: %v", err)
// 降级到数据库处理
return l.voteCommentDB(in)
}
问题:游标分页 vs Offset 分页对比
问题 :看 comment 服务的分页实现:
1. 为什么使用 cursor 分页而不是 offset?
2. 这种实现有什么局限性?
3. 如何支持跳转到第 N 页?
参考答案 :
1. Cursor 分页的优势
// GetCommentList 使用游标分页
func (l *GetCommentListLogic) GetCommentList(in *pb.GetCommentListReq) (*pb.GetCommentListResp, error) {
// 使用 ZRangeByScore 游标查询
maxScore := "+inf"
minScore := "-inf"
if cursor > 0 {
maxScore = fmt.Sprintf("(%d", cursor)
}
idStrings, err := l.svcCtx.Redis.ZRevRangeByScore(
ctx, key, minScore, maxScore, 0, pageSize+1)
}
Cursor 分页优势 :
- 性能稳定 :OFFSET 会扫描前 N 行后丢弃,数据量大时很慢
- 一致性好 :不受数据插入删除影响
- 实现简单 :Redis ZSet 原生支持
Offset 分页问题 :
-- offset 分页的性能问题
SELECT * FROM comment LIMIT 1000000, 20;
-- 需要扫描并扔掉前 1000000 行
2. Cursor 分页的局限性 :
- 无法跳转到任意页 :只能翻前/翻后
- 不支持页码显示 :没有页码概念
- 删除的评论会导致结果重复或丢失
3. 支持跳转到第 N 页的方案
// 方案A:混合方案
func getCommentListHybrid(in *pb.GetCommentListReq) (*pb.GetCommentListResp, error) {
if in.Cursor > 0 {
// 使用游标分页
return getCommentListByCursor(in)
}
if in.Page > 0 && in.Page <= 10 {
// 前10页支持 offset 分页,预加载
return getCommentListByOffset(in)
}
// 超过10页引导用户使用搜索
return nil, errors.New("请使用搜索或缩小范围")
}
// 方案B:构建页码索引(适合静态内容)
func buildPageIndex(postID uint64, sortType string) {
// 预计算每页的第一个评论ID
key := buildCommentListKey(postID, sortType)
commentIDs, _ := redis.ZRange(ctx, key, 0, -1)
for i := 0; i < len(commentIDs); i += pageSize {
pageIndexKey := fmt.Sprintf("comment:page_index:%s:%s:%d",
postID, sortType, i/pageSize + 1)
redis.Set(ctx, pageIndexKey, commentIDs[i], 1*time.Hour)
}
}
问题 :
- 用户体验差 :删除成功但评论还在列表里(直到 Consumer 处理)
- 数据不一致 :数据库删除但缓存未清理干净
- 没有处理子回复 :子回复变成孤儿
2. 子回复的处理策略 :
选项A:级联软删除 (推荐)
func deleteCommentWithReplies(commentID uint64) error {
// 找到所有子回复
var allReplyIDs []uint64
var queue = []uint64{commentID}
for len(queue) > 0 {
currentID := queue[0]
queue = queue[1:]
allReplyIDs = append(allReplyIDs, currentID)
var replies []*model.Comment
db.Where("parent_id = ? AND deleted_at IS NULL", currentID).Find(&replies)
for _, r := range replies {
queue = append(queue, r.ID)
}
}
// 批量软删除
now := time.Now()
db.Model(&model.Comment{}).Where("id IN ?", allReplyIDs).
Updates(map[string]interface{}{
"deleted_at": &now,
"status": 3,
})
// 批量清理缓存
for _, id := range allReplyIDs {
redis.Del(ctx, fmt.Sprintf("%s%d", redis.CommentInfoKey, id))
redis.Del(ctx, fmt.Sprintf("%s%d", redis.CommentContentKey, id))
}
}
选项B:显示"已删除"占位符
// 不删除子回复,只标记父评论
// 前端显示:此评论已删除
问题:评论限流与防刷机制
问题 :看投票逻辑中的限流:
1. 当前的防刷机制够吗?
2. 如何防止恶意刷赞?
3. 如何实现基于 IP 和设备的复杂限流?
参考答案 :
1. 当前实现分析
// votecommentlogic.go
date := time.Now().Format("20060102")
userVoteKey := fmt.Sprintf("%s%d:%s", redis.UserVoteCountKey, in.UserId, date)
voteCount, err := l.svcCtx.Redis.Incr(l.ctx, userVoteKey)
if err != nil {
logx.Errorf("用户投票计数失败: %v", err)
// 不影响主流程
} else {
// 设置过期时间
if voteCount == 1 {
l.svcCtx.Redis.Expire(l.ctx, userVoteKey, time.Hour*24)
}
// 每日最大 200 次
if voteCount > 200 {
return nil, errutil.RpcBusinessError(errutil.ErrTooManyRequests)
}
}
当前的不足 :
- 只限制了用户级别的频率
- 没有 IP 限制
- 没有设备指纹
- 没有异常行为检测
- 没有滑动窗口限流
2. 恶意刷赞防护方案
// 多级限流
func checkRateLimit(userID uint64, commentID uint64, ip string) error {
ctx := context.Background()
// 1. 同一用户对同一评论限制
key1 := fmt.Sprintf("vote:limit:user:%d:%d", userID, commentID)
if cnt, _ := redis.Incr(ctx, key1); cnt > 1 {
return errors.New("不能重复投票")
}
redis.Expire(ctx, key1, 24*time.Hour)
// 2. 用户频率限制(滑动窗口)
key2 := fmt.Sprintf("vote:limit:user:%d", userID)
now := time.Now().Unix()
redis.ZAdd(ctx, key2, redis.Z{Score: float64(now), Member: strconv.FormatInt(now, 10)})
redis.ZRemRangeByScore(ctx, key2, "0", fmt.Sprintf("%d", now-3600)) // 只保留1小时内
if cnt, _ := redis.ZCard(ctx, key2); cnt > 100 {
return errors.New("投票太频繁")
}
redis.Expire(ctx, key2, 2*time.Hour)
// 3. IP 限制
key3 := fmt.Sprintf("vote:limit:ip:%s", ip)
if cnt, _ := redis.Incr(ctx, key3); cnt > 500 {
return errors.New("IP 受限")
}
redis.Expire(ctx, key3, time.Hour)
// 4. 同一评论短时间内大量投票
key4 := fmt.Sprintf("vote:limit:comment:%d", commentID)
windowStart := now - 60 // 1分钟窗口
redis.ZAdd(ctx, key4, redis.Z{Score: float64(now), Member: strconv.FormatUint(userID, 10)})
redis.ZRemRangeByScore(ctx, key4, "0", fmt.Sprintf("%d", windowStart))
if cnt, _ := redis.ZCard(ctx, key4); cnt > 50 {
return errors.New("评论投票异常")
}
redis.Expire(ctx, key4, 5*time.Minute)
return nil
}
3. 高级异常行为检测
func detectAbnormalBehavior(userID uint64) {
// 1. 检查用户是否在短时间内对大量不同评论投票
recentVotes := getRecentVotes(userID, time.Hour)
if len(recentVotes) > 200 {
flagUser(userID, "high_frequency_voter")
}
// 2. 检查用户投票模式是否异常(只点赞,或只点踩)
likeRatio := calculateLikeRatio(recentVotes)
if likeRatio > 0.95 || likeRatio < 0.05 {
flagUser(userID, "suspicious_voting_pattern")
}
// 3. 检查新账号异常活跃
userProfile := getUserProfile(userID)
if userProfile.CreatedAt.After(time.Now().Add(-24*time.Hour)) && len(recentVotes) > 50 {
flagUser(userID, "new_account_abnormal_activity")
}
}
问题:评论排序热更新问题
问题 :当一个评论的点赞数变化后,如何更新 Redis 中的排序?
1. 当前实现如何处理?
2. 有什么更高效的方案?
3. 如何防止排序抖动?
参考答案 :
1. 当前实现分析
当前代码中,投票后只是让缓存失效:
invalidatePostCommentListCache(l.ctx, l.svcCtx, comment.PostID)
这种做法的问题:
- 缓存雪崩风险 :频繁失效缓存会大量查询数据库
- 性能差 :每次点赞都可能触发重建
- 用户体验差 :排序更新不及时
2. 高效实时排序更新方案
func updateCommentScoreRealTime(postID uint64, commentID uint64, deltaLikes int32) {
ctx := context.Background()
key := fmt.Sprintf("%s%d:hot", redis.PostCommentsKey, postID)
// 1. 获取评论当前信息
info, err := redis.HGetAll(ctx, fmt.Sprintf("%s%d", redis.CommentInfoKey, commentID)).Result()
if err != nil || len(info) == 0 {
return // 缓存未命中,等待重建
}
// 2. 计算新分数
likeCount, _ := strconv.Atoi(info["like_count"])
replyCount, _ := strconv.Atoi(info["reply_count"])
createdAt, _ := strconv.ParseInt(info["created_at"], 10, 64)
// 新分数(包含 delta)
newScore := float64((likeCount+int(deltaLikes)) + replyCount*3)
newScore -= float64(time.Now().Unix()-createdAt) / 3600
// 3. 更新分数
redis.ZAdd(ctx, key, redis.Z{Score: newScore, Member: commentID})
// 4. 同时更新 info 缓存
redis.HIncrBy(ctx, fmt.Sprintf("%s%d", redis.CommentInfoKey, commentID),
"like_count", int64(deltaLikes))
}
3. 防止排序抖动
// 方案A:延迟更新 + 批量合并
type pendingScoreUpdate struct {
commentID uint64
delta int32
}
var pendingUpdates = make(chan pendingScoreUpdate, 1000)
func batchUpdateScores() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
buffer := make(map[uint64]int32)
for {
select {
case update := <-pendingUpdates:
buffer[update.commentID] += update.delta
// 缓冲区满或到时间,批量刷新
if len(buffer) >= 100 {
flushBuffer(buffer)
buffer = make(map[uint64]int32)
}
case <-ticker.C:
if len(buffer) > 0 {
flushBuffer(buffer)
buffer = make(map[uint64]int32)
}
}
}
}
func flushBuffer(buffer map[uint64]int32) {
// 按帖子分组,批量更新
posts := groupByPost(buffer)
for postID, comments := range posts {
updatePostScores(postID, comments)
}
}
// 方案B:阈值更新(只有变化足够大时才重新排序)
func shouldUpdateScore(oldLikeCount int32, newLikeCount int32) bool {
threshold := 0.1 // 10%的变化
if oldLikeCount == 0 {
return newLikeCount > 0
}
change := math.Abs(float64(newLikeCount-oldLikeCount)) / float64(oldLikeCount)
return change > threshold
}
relation服务
关注一个人但是同时他注销了怎么办?
以下评审基于当前仓库源码与配置,未包含线上压测、真实慢查询、监控指标和生产流量画像。因此性能/并发结论是“源码级风险评审”,需要后续用 p95/p99、QPS、Redis/MySQL/Kafka 指标验证。
总体结论
当前五个服务已经具备微服务拆分、gRPC、MySQL、Redis、Kafka、部分读写分离、批量查询、Lua 原子操作、singleflight/逻辑过期等基础能力。主要短板集中在:缓存大 Key/全量重建、无界 goroutine 异步发送、DB 与 Kafka 非事务一致、部分批量接口退化为循环 Redis 查询、深分页/全量 Count、鉴权与内容治理能力不足。
高优先级共性风险:
- P0:DB 写入成功但 Kafka 发送失败会导致统计、搜索、缓存同步不一致。建议引入 outbox/event table 或事务消息补偿。
- P0:评论列表和关系链缓存存在全量加载风险,大帖子/大 V 用户会形成大 Key 和重建尖峰。
- P1:多个写路径使用 go func 异步发消息,缺少 worker pool、队列上限、失败重试和关闭排空。
- P1:服务侧功能鉴权依赖传入 userId/currentUserId,RPC 层未看到统一 auth context 强校验。
- P1:缺少压测、慢查询、Redis 热 Key、Kafka 堆积和连接池监控数据,无法证明高并发容量。
user 服务/用户中心
接口覆盖注册、登录、刷新 token、用户信息、资料、头像、状态、统计、批量基础信息,见 user.proto (line 174)。
- 高性能分析:优点是用户信息使用 go-zero cache + singleflight,批量基础信息限制 200 并用 IN 查询。问题是登录每次同步写 last_login_time 和 last_active_time,高频登录会放大写 I/O;批量缓存逐个 Get/Set,未使用 Redis pipeline;bcrypt 密码校验成本可控但需要限流保护。建议登录活跃时间异步合并更新,批量缓存改 pipeline/mget,增加登录失败限流。
- 高并发分析:注册使用唯一索引和事务,能防用户名竞态,见 registerlogic.go (line 33)。问题是没有看到账号级/手机号/IP 级登录注册限流、验证码、锁定策略;Kafka 消费更新统计缺少完整幂等说明。建议用 Redis 滑窗限流、失败次数锁定、消费幂等键。
- 缓存分析:优点是用户 info/profile/stat 有缓存和空值过期。问题是更新后只删/版本失效,未看到缓存预热;批量基础信息空对象缓存可能让调用方无法区分不存在和字段为空。建议统一 cache-aside 语义,批量接口补充 notFound 标识或返回顺序映射。
- 数据库使用分析:user 有 username/phone/email 唯一索引,user_stat/profile 有 uk_user_id,基础合理,见 db.sql (line 17)。问题是手机号/邮箱唯一但注册接口未覆盖;软删除与唯一索引会阻止同名重新注册。建议根据业务决定“软删后是否可复用”,必要时改组合唯一键。
- 功能完整性分析:已有注册、登录、刷新 token、资料、头像、状态、统计。缺失:手机号/邮箱注册登录、密码重置、修改密码、MFA、设备/session 管理、登出/refresh token 撤销、RBAC/权限、账户注销、隐私导出/删除、风控审计。建议优先补认证安全闭环。
综合风险与优先级:P0 认证安全与限流;P1 token 撤销/session 管理;P1 用户统计异步一致性;P2 隐私合规能力。
content 服务/内容中心
覆盖发帖、改帖、删帖、详情、列表、批量、话题、置顶、加精,见 content.proto (line 155)。
- 高性能分析:优点是详情有本地缓存、Redis 逻辑过期、singleflight 和互斥锁,见 getpostdetaillogic.go (line 31)。问题是列表每次 Count + Offset,深分页和大表会慢,见 getpostlistlogic.go (line 70);发帖话题逐个 upsert + 查询,批量差。建议列表改游标/时间线,热门/最新榜单用 Redis ZSET 或 feed 服务承接,话题批量 upsert。
- 高并发分析:优点是写操作包事务,读库配置存在。问题是创建后异步 Kafka 不可靠,置顶/加精/更新会造成列表缓存版本膨胀但旧 key 等待 TTL 回收;同步 Kafka producer 配置存在阻塞风险。建议 outbox、异步 worker、热点帖子隔离缓存。
- 缓存分析:优点是详情防穿透/击穿设计较完整,列表有版本号。本地缓存会提升热点详情性能。问题是列表版本按用户维度,公共 feed 可能出现大量旧 key;关注 feed 不使用缓存。建议对公共列表使用固定窗口缓存,对关注列表交给 feed/时间线缓存。
- 数据库使用分析:post 有 idx_user_id、idx_visibility、idx_created_at,但列表排序是 is_top DESC, created_at DESC,缺少匹配联合索引;关注列表 join following 需要 (user_id, follow_id) 可用,但 post 侧也需要 (user_id, created_at)。建议增加 idx_post_top_created(is_top, created_at)、idx_post_user_created(user_id, created_at)、必要时覆盖 deleted_at。
- 功能完整性分析:已有发布、编辑、删除、详情、列表、话题、标签、置顶、加精。缺失:草稿、审核流、敏感内容过滤、版本历史、定时发布、内容搜索入口、媒体资源管理、权限可见性完整校验、举报处理、恢复/回收站。建议优先补审核/敏感词/可见性校验。
综合风险与优先级:P0 内容审核与可见性安全;P1 列表深分页/索引;P1 Kafka 事件可靠性;P2 草稿和版本。
comment 服务/评论中心
覆盖创建、删除、根评论列表、回复列表、评论投票,见 comment.proto (line 85)。
- 高性能分析:优点是评论列表用 ZSET、评论 info/content pipeline,投票用 Lua 原子更新。问题是重建评论缓存会一次性查询某帖子所有根评论,见 getcommentlistlogic.go (line 200);热帖评论量大时会造成 MySQL/Redis/内存尖峰。建议只缓存前 N 页或分段 ZSET,长尾走 DB 游标。
- 高并发分析:投票先 Redis 后 Kafka 异步落库,响应快,但 DB 最终一致依赖消息成功,见 votecommentlogic.go (line 94)。创建评论后异步发两类事件,失败只打日志。建议 outbox、消费幂等、失败重试、消息堆积监控。
- 缓存分析:有逻辑过期、空值缓存、锁、pipeline,是五个服务里较完整的缓存实现。问题是热评论/热帖会产生大 ZSET;投票更新 info hash 后列表热度排序可能滞后;删除/审核状态对缓存一致性依赖失效函数。建议热度榜异步增量更新,删除/隐藏统一事件驱动失效。
- 数据库使用分析:comment 有 (post_id,parent_id)、(post_id,created_at)、(parent_id,created_at),基础合理。但 cursor fallback 用 id < cursor + created_at DESC 排序不完全匹配;hot 排序缺少复合索引。建议新增 (post_id,parent_id,created_at,id),hot 场景考虑 Redis 主导。
- 功能完整性分析:已有创建、删除、根评论、回复、点赞/点踩。缺失:评论审核、敏感词、举报、置顶、精选、作者删除/管理员删除区分、软删展示策略、恢复、楼中楼深度限制、评论搜索、反刷屏。建议优先补审核/举报/删除权限。
综合风险与优先级:P0 评论治理与反刷;P0 大帖缓存重建;P1 投票落库可靠性;P2 排序和恢复能力。
interaction 服务/互动中心
覆盖点赞、取消点赞、收藏、取消收藏、分享、批量状态查询,见 interaction.proto (line 121)。
- 高性能分析:点赞/收藏用 Redis Lua,DB 落库异步批处理,吞吐设计方向正确,见 starpostlogic.go (line 32)。问题是每次请求动态 EVAL 脚本而不是 EVALSHA;异步发送事件仍套一层 goroutine;批量状态虽限制 256,但需要确认是否 pipeline。建议预加载 Lua SHA、去掉请求内无界 goroutine或改 bounded worker。
- 高并发分析:Redis Lua 保证单实体计数原子;消费端有批量落库和幂等键,见 kafka_consumer.go (line 99)。问题是 producer Idempotent=false,DB 与 Redis 最终一致无补偿;Redis 成功 Kafka 失败会永久丢落库。建议 outbox/重试队列,定期 Redis-DB reconciliation。
- 缓存分析:互动状态主要在 Redis set/hash/counter,适合高频读写。问题是关系集合按 post 聚合,热门帖子可能形成热 Key/大 Key;没有看到分片 key 或本地只读缓存。建议热帖按 userId hash 分片关系集合,计数用分片 counter 汇总。
- 数据库使用分析:post_star、post_collection 有 (user_id,post_id) 唯一和 idx_post_id,基础合理,见 db.sql (line 123)。缺失 post_share DDL 在当前 db.sql 未看到,需要确认迁移来源。建议补完整 DDL、为用户收藏列表增加 (user_id,collected_at)。
- 功能完整性分析:已有点赞、收藏、分享、批量状态。缺失:点踩帖子、转发、浏览/曝光埋点、用户互动列表、收藏夹、取消分享语义、幂等请求 ID、反作弊风控。建议优先补幂等/风控/用户侧列表。
综合风险与优先级:P0 Redis 成功但消息失败;P1 热 Key;P1 Kafka producer 幂等;P2 互动类型扩展。
relation 服务/关系链
覆盖关注、取关、关注状态、关注列表、粉丝列表、批量状态,见 relation.proto (line 80)。
- 高性能分析:关注/粉丝列表用 Redis ZSET,适合分页读。问题是缓存缺失时会加载用户完整关系链进一个 ZSET,见 relation_cache.go (line 112);大 V 粉丝列表会大 Key。批量关注状态是循环检查,见 batchcheckfollowstatuslogic.go (line 27)。建议分片 ZSET、只缓存窗口页、批量状态用 pipeline 或 DB/Redis 批量命令。
- 高并发分析:DB 唯一键防重复关注,Lua 同步更新双向缓存。问题是先 DB 后 Redis/Kafka,任一失败会导致缓存或统计不一致;同步 Kafka 可能拉长关注接口 p99。建议关注事件 outbox,缓存失败时删除版本并后台修复。
- 缓存分析:有空列表短 TTL 和 singleflight,能防击穿。问题是 7 天 TTL 的完整关系链缓存会占用大量 Redis;取消关注只更新已存在 key,对未热缓存无影响但统计依赖事件。建议对粉丝大户使用分页缓存、bitmap/bloom 只服务状态检查。
- 数据库使用分析:following 有 (user_id,follow_id) 唯一和 idx_follow(follow_id),基础可用,见 db.sql (line 216)。但列表排序按 created_at DESC,缺少 (user_id,created_at) 和 (follow_id,created_at)。建议补这两个联合索引。
- 功能完整性分析:已有关注/取关、互关状态、关注/粉丝列表、批量状态。缺失:好友/联系人、黑名单、静音/屏蔽、共同关注、推荐关系、移除粉丝、分组、隐私关系、双向一致性审计。建议优先补黑名单和共同关注查询,黑名单应参与内容/评论/私信可见性判断。
综合风险与优先级:P0 大 V 关系链大 Key;P1 关系统计一致性;P1 批量状态循环查询;P2 黑名单/共同关注。
需补充的信息
要形成可落地容量评估,还需要:线上 QPS/p95/p99、MySQL EXPLAIN 和慢查询、Redis key size/hotkey 统计、Kafka lag/失败率、服务实例数和 CPU/内存、网关鉴权链路、备份恢复方案、压测脚本与目标 SLA。
建议下一步路线:先做 P0 一致性和治理能力,然后补关键索引与缓存大 Key 改造,最后做压测闭环和容量水位表。

被折叠的 条评论
为什么被折叠?



