514 lines
13 KiB
Markdown
514 lines
13 KiB
Markdown
# 资产点赞功能高性能设计文档
|
||
|
||
## 一、设计目标
|
||
|
||
### 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. **第三阶段**:根据数据量考虑分表、读写分离等扩展方案
|
||
|