# 资产点赞功能高性能设计文档 ## 一、设计目标 ### 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` 表(已有)** ```sql -- 点赞数计数器(冗余字段,用于快速查询) like_count INT NOT NULL DEFAULT 0 ``` **2. `asset_likes` 表(点赞记录表)** ```sql 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 原子操作实现 ```go // 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 数据库原子更新 ```go // 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 缓存预热和同步 ```go // 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. 索引优化** ```sql -- 唯一索引:防止重复点赞 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. 分表策略(可选,数据量特别大时)** ```sql -- 按 asset_id 取模分表 asset_likes_0, asset_likes_1, ..., asset_likes_9 -- 路由规则 table_index = asset_id % 10 ``` **3. 批量写入** ```go // 批量写入点赞记录(定时任务) func (s *LikeService) BatchWriteLikes(records []LikeRecord) error { return s.repo.BatchInsertLikes(records) } ``` ### 5.3 异步处理 **1. 消息队列(可选)** ``` 点赞操作 → Redis → 消息队列 → 数据库写入 ``` **优势:** - 削峰填谷:高并发时缓冲写入压力 - 批量处理:可以批量写入数据库 - 解耦:点赞服务和数据库写入解耦 **2. 定时任务补偿** ```go // 定时任务:同步 Redis 和数据库的点赞数 func (s *LikeService) SyncLikeCountJob() { // 1. 查询 Redis 中所有点赞数缓存 // 2. 对比数据库中的 like_count // 3. 如果差异超过阈值,进行修正 } ``` --- ## 六、数据一致性保证 ### 6.1 最终一致性 **策略:** 1. **Redis 作为主数据源**:实时查询使用 Redis 2. **数据库作为持久化存储**:定时同步 Redis 到数据库 3. **补偿机制**:定时任务对比 Redis 和数据库,修正差异 ### 6.2 一致性检查 ```go // 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 故障恢复 **场景1:Redis 故障** - 降级到数据库查询 - Redis 恢复后,从数据库同步数据 **场景2:数据库写入失败** - 记录失败日志 - 定时任务重试 - 如果重试失败,人工介入 --- ## 七、监控和告警 ### 7.1 关键指标 1. **性能指标** - 点赞操作 QPS - 点赞操作延迟(P50, P99) - Redis 命中率 - 数据库写入延迟 2. **数据指标** - Redis 和数据库的点赞数差异 - 异步写入失败率 - 缓存预热成功率 3. **业务指标** - 每日点赞数 - 热门资产点赞数 - 用户点赞活跃度 ### 7.2 告警规则 - Redis 命中率 < 80% - 点赞操作延迟 P99 > 100ms - Redis 和数据库点赞数差异 > 100 - 异步写入失败率 > 1% --- ## 八、扩展方案 ### 8.1 分库分表(数据量特别大时) **分表策略:** ```sql -- 按 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; ``` **路由逻辑:** ```go 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 性能预期 - **QPS**:10,000+ 点赞操作/秒 - **延迟**:< 50ms(P99) - **缓存命中率**:> 95% - **数据一致性**:最终一致,差异 < 1% ### 9.3 实施建议 1. **第一阶段**:实现 Redis 缓存 + 数据库存储(满足基本需求) 2. **第二阶段**:引入消息队列,优化异步写入 3. **第三阶段**:根据数据量考虑分表、读写分离等扩展方案