13 KiB
13 KiB
资产点赞功能高性能设计文档
一、设计目标
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
关键优化点:
- Redis 原子操作:使用
SETNX+INCR确保原子性 - 先写缓存,后写数据库:保证响应速度
- 异步写入:避免数据库写入阻塞请求
- 唯一索引保护:即使异步写入失败,数据库层面也能防止重复
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 最终一致性
策略:
- Redis 作为主数据源:实时查询使用 Redis
- 数据库作为持久化存储:定时同步 Redis 到数据库
- 补偿机制:定时任务对比 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 故障恢复
场景1:Redis 故障
- 降级到数据库查询
- Redis 恢复后,从数据库同步数据
场景2:数据库写入失败
- 记录失败日志
- 定时任务重试
- 如果重试失败,人工介入
七、监控和告警
7.1 关键指标
-
性能指标
- 点赞操作 QPS
- 点赞操作延迟(P50, P99)
- Redis 命中率
- 数据库写入延迟
-
数据指标
- Redis 和数据库的点赞数差异
- 异步写入失败率
- 缓存预热成功率
-
业务指标
- 每日点赞数
- 热门资产点赞数
- 用户点赞活跃度
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 核心设计原则
- 缓存优先:Redis 作为主数据源,保证响应速度
- 异步写入:数据库写入异步化,不阻塞请求
- 原子操作:使用 Redis 原子操作和数据库唯一索引保证一致性
- 最终一致性:允许短暂的数据不一致,通过定时任务保证最终一致
9.2 性能预期
- QPS:10,000+ 点赞操作/秒
- 延迟:< 50ms(P99)
- 缓存命中率:> 95%
- 数据一致性:最终一致,差异 < 1%
9.3 实施建议
- 第一阶段:实现 Redis 缓存 + 数据库存储(满足基本需求)
- 第二阶段:引入消息队列,优化异步写入
- 第三阶段:根据数据量考虑分表、读写分离等扩展方案