topfans/backend/docs/资产点赞功能高性能设计.md
2026-04-07 22:29:48 +08:00

13 KiB
Raw Blame History

资产点赞功能高性能设计文档

一、设计目标

1.1 性能要求

  • 高并发:支持每秒 10,000+ 点赞操作
  • 低延迟:点赞操作响应时间 < 50ms
  • 数据一致性:点赞数和点赞状态最终一致
  • 可扩展性:支持百万级资产、千万级点赞记录

1.2 功能要求

  • 用户可以对资产进行点赞/取消点赞
  • 防止重复点赞(同一用户对同一资产只能点赞一次)
  • 实时显示点赞数
  • 查询用户是否已点赞(is_liked

二、架构设计

2.1 整体架构

┌─────────────┐
│   Gateway   │
└──────┬──────┘
       │ HTTP
       ▼
┌─────────────────┐
│  Social Service │  ← 点赞业务逻辑
└──────┬──────────┘
       │
       ├──────────────┬──────────────┐
       ▼              ▼              ▼
   ┌──────┐      ┌──────┐      ┌─────────┐
   │ Redis │      │  DB  │      │  MQ     │
   │ Cache │      │      │      │(可选)   │
   └──────┘      └──────┘      └─────────┘

2.2 数据存储设计

2.2.1 数据库表设计

1. assets 表(已有)

-- 点赞数计数器(冗余字段,用于快速查询)
like_count INT NOT NULL DEFAULT 0

2. asset_likes 表(点赞记录表)

CREATE TABLE asset_likes (
    id BIGSERIAL PRIMARY KEY,
    asset_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    star_id BIGINT NOT NULL,  -- 用于数据隔离和查询优化
    created_at BIGINT NOT NULL,
    
    -- 唯一索引:防止重复点赞
    UNIQUE KEY uk_asset_likes_user_asset (asset_id, user_id),
    
    -- 索引:用于查询用户点赞列表
    INDEX idx_asset_likes_user_star (user_id, star_id, created_at DESC),
    
    -- 索引:用于查询资产点赞用户列表
    INDEX idx_asset_likes_asset (asset_id, created_at DESC),
    
    -- 外键约束
    FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE
);

设计说明:

  • uk_asset_likes_user_asset 唯一索引:防止同一用户对同一资产重复点赞
  • idx_asset_likes_user_star 索引:支持查询用户在某明星下的点赞列表
  • idx_asset_likes_asset 索引:支持查询资产的点赞用户列表(可选功能)

2.2.2 Redis 缓存设计

1. 点赞数缓存

Key: asset:like_count:{asset_id}
Value: {count}
TTL: 1小时或永久定时同步

2. 点赞状态缓存

Key: asset:like:{asset_id}:{user_id}
Value: 1已点赞或 0未点赞
TTL: 7天

3. 用户点赞列表缓存(可选)

Key: user:liked_assets:{user_id}:{star_id}
Value: Set{asset_id1, asset_id2, ...}
TTL: 1小时

三、核心流程设计

3.1 点赞流程(高并发优化)

用户点赞请求
    │
    ▼
1. 检查 Redis 缓存(点赞状态)
    │
    ├─ 已点赞 → 返回错误(防重复)
    │
    └─ 未点赞 → 继续
    │
    ▼
2. Redis 原子操作SET + INCR
    - SET asset:like:{asset_id}:{user_id} 1
    - INCR asset:like_count:{asset_id}
    │
    ▼
3. 立即返回成功(响应时间 < 50ms
    │
    ▼
4. 异步写入数据库(后台任务)
    - INSERT INTO asset_likes (asset_id, user_id, star_id, created_at)
    - UPDATE assets SET like_count = like_count + 1 WHERE id = asset_id

关键优化点:

  1. Redis 原子操作:使用 SETNX + INCR 确保原子性
  2. 先写缓存,后写数据库:保证响应速度
  3. 异步写入:避免数据库写入阻塞请求
  4. 唯一索引保护:即使异步写入失败,数据库层面也能防止重复

3.2 取消点赞流程

用户取消点赞请求
    │
    ▼
1. 检查 Redis 缓存(点赞状态)
    │
    ├─ 未点赞 → 返回错误
    │
    └─ 已点赞 → 继续
    │
    ▼
2. Redis 原子操作DEL + DECR
    - DEL asset:like:{asset_id}:{user_id}
    - DECR asset:like_count:{asset_id}
    │
    ▼
3. 立即返回成功
    │
    ▼
4. 异步删除数据库记录(后台任务)
    - DELETE FROM asset_likes WHERE asset_id = ? AND user_id = ?
    - UPDATE assets SET like_count = like_count - 1 WHERE id = asset_id

3.3 查询点赞数流程

查询资产点赞数
    │
    ▼
1. 查询 Redis 缓存
    │
    ├─ 缓存命中 → 直接返回
    │
    └─ 缓存未命中 → 查询数据库
         │
         ▼
      2. SELECT like_count FROM assets WHERE id = asset_id
         │
         ▼
      3. 写入 Redis 缓存
         │
         ▼
      4. 返回结果

3.4 查询点赞状态流程

查询用户是否已点赞
    │
    ▼
1. 查询 Redis 缓存
    │
    ├─ 缓存命中 → 直接返回
    │
    └─ 缓存未命中 → 查询数据库
         │
         ▼
      2. SELECT COUNT(*) FROM asset_likes 
         WHERE asset_id = ? AND user_id = ?
         │
         ▼
      3. 写入 Redis 缓存
         │
         ▼
      4. 返回结果

四、实现细节

4.1 Redis 原子操作实现

// LikeAsset 点赞资产(原子操作)
func (s *LikeService) LikeAsset(ctx context.Context, assetID, userID, starID int64) error {
    // 1. 检查是否已点赞Redis
    likeKey := fmt.Sprintf("asset:like:%d:%d", assetID, userID)
    exists, err := s.redis.Exists(ctx, likeKey).Result()
    if err != nil {
        return err
    }
    if exists > 0 {
        return errors.New("already liked")
    }

    // 2. Redis 原子操作SETNX + INCR
    countKey := fmt.Sprintf("asset:like_count:%d", assetID)
    pipe := s.redis.Pipeline()
    pipe.SetNX(ctx, likeKey, "1", 7*24*time.Hour)  // 7天过期
    pipe.Incr(ctx, countKey)
    _, err = pipe.Exec(ctx)
    if err != nil {
        return err
    }

    // 3. 异步写入数据库
    go s.asyncWriteLike(ctx, assetID, userID, starID)

    return nil
}

// asyncWriteLike 异步写入点赞记录
func (s *LikeService) asyncWriteLike(ctx context.Context, assetID, userID, starID int64) {
    // 使用消息队列或直接写入数据库
    // 如果使用消息队列,可以批量处理,提高性能
    err := s.repo.CreateLikeRecord(assetID, userID, starID)
    if err != nil {
        // 记录日志,定时任务补偿
        logger.Logger.Error("Failed to write like record", zap.Error(err))
    }
    
    // 更新 assets 表的 like_count使用原子更新
    err = s.repo.IncrementLikeCount(assetID, 1)
    if err != nil {
        logger.Logger.Error("Failed to update like count", zap.Error(err))
    }
}

4.2 数据库原子更新

// IncrementLikeCount 原子增加点赞数
func (r *assetRepository) IncrementLikeCount(assetID int64, delta int32) error {
    return r.db.Model(&models.Asset{}).
        Where("id = ?", assetID).
        UpdateColumn("like_count", gorm.Expr("like_count + ?", delta)).Error
}

// DecrementLikeCount 原子减少点赞数
func (r *assetRepository) DecrementLikeCount(assetID int64, delta int32) error {
    return r.db.Model(&models.Asset{}).
        Where("id = ? AND like_count >= ?", assetID, delta).
        UpdateColumn("like_count", gorm.Expr("like_count - ?", delta)).Error
}

4.3 缓存预热和同步

// SyncLikeCountFromDB 从数据库同步点赞数到 Redis
func (s *LikeService) SyncLikeCountFromDB(ctx context.Context, assetID int64) error {
    var asset models.Asset
    err := s.repo.GetAsset(assetID, &asset)
    if err != nil {
        return err
    }

    countKey := fmt.Sprintf("asset:like_count:%d", assetID)
    return s.redis.Set(ctx, countKey, asset.LikeCount, 0).Err()
}

// WarmupCache 缓存预热(定时任务)
func (s *LikeService) WarmupCache(ctx context.Context) {
    // 查询热门资产(点赞数 > 100
    hotAssets, _ := s.repo.GetHotAssets(100)
    
    for _, asset := range hotAssets {
        countKey := fmt.Sprintf("asset:like_count:%d", asset.ID)
        s.redis.Set(ctx, countKey, asset.LikeCount, 0)
    }
}

五、性能优化策略

5.1 缓存策略

1. 多级缓存

  • L1: 应用内存缓存(本地缓存,如 go-cache
  • L2: Redis 缓存
  • L3: 数据库

2. 缓存更新策略

  • Write-Through:写入时同时更新缓存和数据库(适合点赞数)
  • Write-Behind:先写缓存,异步写数据库(适合点赞记录)
  • Cache-Aside:查询时先查缓存,未命中再查数据库

3. 缓存过期策略

  • 点赞数永久缓存定时同步或1小时过期
  • 点赞状态7天过期用户7天内可能再次查看
  • 点赞列表1小时过期

5.2 数据库优化

1. 索引优化

-- 唯一索引:防止重复点赞
UNIQUE KEY uk_asset_likes_user_asset (asset_id, user_id)

-- 复合索引:支持查询用户点赞列表
INDEX idx_asset_likes_user_star (user_id, star_id, created_at DESC)

-- 单列索引:支持查询资产点赞数(如果需要)
INDEX idx_asset_likes_asset (asset_id)

2. 分表策略(可选,数据量特别大时)

-- 按 asset_id 取模分表
asset_likes_0, asset_likes_1, ..., asset_likes_9

-- 路由规则
table_index = asset_id % 10

3. 批量写入

// 批量写入点赞记录(定时任务)
func (s *LikeService) BatchWriteLikes(records []LikeRecord) error {
    return s.repo.BatchInsertLikes(records)
}

5.3 异步处理

1. 消息队列(可选)

点赞操作 → Redis → 消息队列 → 数据库写入

优势:

  • 削峰填谷:高并发时缓冲写入压力
  • 批量处理:可以批量写入数据库
  • 解耦:点赞服务和数据库写入解耦

2. 定时任务补偿

// 定时任务:同步 Redis 和数据库的点赞数
func (s *LikeService) SyncLikeCountJob() {
    // 1. 查询 Redis 中所有点赞数缓存
    // 2. 对比数据库中的 like_count
    // 3. 如果差异超过阈值,进行修正
}

六、数据一致性保证

6.1 最终一致性

策略:

  1. Redis 作为主数据源:实时查询使用 Redis
  2. 数据库作为持久化存储:定时同步 Redis 到数据库
  3. 补偿机制:定时任务对比 Redis 和数据库,修正差异

6.2 一致性检查

// CheckConsistency 检查 Redis 和数据库的一致性
func (s *LikeService) CheckConsistency(ctx context.Context, assetID int64) error {
    // 1. 查询 Redis 点赞数
    redisCount, _ := s.redis.Get(ctx, fmt.Sprintf("asset:like_count:%d", assetID)).Int64()
    
    // 2. 查询数据库点赞数
    dbCount, _ := s.repo.GetLikeCount(assetID)
    
    // 3. 如果差异超过阈值,进行修正
    if abs(redisCount - dbCount) > 10 {
        // 以数据库为准,更新 Redis
        s.redis.Set(ctx, fmt.Sprintf("asset:like_count:%d", assetID), dbCount, 0)
    }
    
    return nil
}

6.3 故障恢复

场景1Redis 故障

  • 降级到数据库查询
  • Redis 恢复后,从数据库同步数据

场景2数据库写入失败

  • 记录失败日志
  • 定时任务重试
  • 如果重试失败,人工介入

七、监控和告警

7.1 关键指标

  1. 性能指标

    • 点赞操作 QPS
    • 点赞操作延迟P50, P99
    • Redis 命中率
    • 数据库写入延迟
  2. 数据指标

    • Redis 和数据库的点赞数差异
    • 异步写入失败率
    • 缓存预热成功率
  3. 业务指标

    • 每日点赞数
    • 热门资产点赞数
    • 用户点赞活跃度

7.2 告警规则

  • Redis 命中率 < 80%
  • 点赞操作延迟 P99 > 100ms
  • Redis 和数据库点赞数差异 > 100
  • 异步写入失败率 > 1%

八、扩展方案

8.1 分库分表(数据量特别大时)

分表策略:

-- 按 asset_id 取模分表
CREATE TABLE asset_likes_0 LIKE asset_likes;
CREATE TABLE asset_likes_1 LIKE asset_likes;
...
CREATE TABLE asset_likes_9 LIKE asset_likes;

路由逻辑:

func getTableName(assetID int64) string {
    return fmt.Sprintf("asset_likes_%d", assetID % 10)
}

8.2 Redis 集群

方案:

  • 使用 Redis Cluster 或 Codis
  • asset_id 进行分片
  • 支持水平扩展

8.3 读写分离

方案:

  • 写操作:主库
  • 读操作:从库(查询点赞列表等)

九、总结

9.1 核心设计原则

  1. 缓存优先Redis 作为主数据源,保证响应速度
  2. 异步写入:数据库写入异步化,不阻塞请求
  3. 原子操作:使用 Redis 原子操作和数据库唯一索引保证一致性
  4. 最终一致性:允许短暂的数据不一致,通过定时任务保证最终一致

9.2 性能预期

  • QPS10,000+ 点赞操作/秒
  • 延迟< 50msP99
  • 缓存命中率> 95%
  • 数据一致性:最终一致,差异 < 1%

9.3 实施建议

  1. 第一阶段:实现 Redis 缓存 + 数据库存储(满足基本需求)
  2. 第二阶段:引入消息队列,优化异步写入
  3. 第三阶段:根据数据量考虑分表、读写分离等扩展方案