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

514 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 资产点赞功能高性能设计文档
## 一、设计目标
### 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 故障恢复
**场景1Redis 故障**
- 降级到数据库查询
- 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+ 点赞操作/秒
- **延迟**< 50msP99
- **缓存命中率**> 95%
- **数据一致性**:最终一致,差异 < 1%
### 9.3 实施建议
1. **第一阶段**实现 Redis 缓存 + 数据库存储满足基本需求
2. **第二阶段**引入消息队列优化异步写入
3. **第三阶段**根据数据量考虑分表读写分离等扩展方案