Skip to content

如何设计点赞系统

一、问题描述

1.1 业务背景

点赞是互联网产品最基础、最常见的互动功能,广泛应用于:

  • 社交平台:微信朋友圈、微博、抖音、小红书
  • 内容平台:知乎、B站、掘金、CSDN
  • 电商平台:淘宝评论点赞、京东问答点赞
  • 视频平台:YouTube、TikTok、爱奇艺

1.2 核心功能

基础功能

  1. 点赞/取消点赞:用户可以点赞或取消点赞
  2. 点赞数展示:显示内容的总点赞数
  3. 点赞状态:显示当前用户是否已点赞
  4. 点赞列表:查看谁点赞了这条内容

进阶功能

  1. 点赞通知:通知内容创作者
  2. 点赞排行:热门内容排序
  3. 防刷:防止恶意刷赞
  4. 点赞动画:前端交互效果

1.3 技术挑战

高并发读写

  • 热门内容可能被百万人点赞
  • 读QPS远大于写QPS(读:写 = 100:1)
  • 需要快速响应(<50ms)

数据一致性

  • 点赞数统计必须准确
  • 防止重复点赞
  • 并发点赞时的竞态条件

性能优化

  • 点赞数频繁变化,如何避免频繁写库
  • 如何快速判断用户是否已点赞
  • 如何高效获取点赞列表

存储压力

  • 每天可能产生亿级点赞数据
  • 点赞记录需要持久化
  • 历史数据如何归档

1.4 面试考察点

  • 缓存设计:如何用Redis加速查询
  • 数据一致性:缓存和数据库如何保持一致
  • 并发控制:如何防止重复点赞
  • 性能优化:如何优化高并发场景
  • 扩展性:如何支持更多点赞类型

二、需求分析

2.1 功能性需求

需求描述优先级
FR1用户可以点赞内容(文章、视频、评论等)P0
FR2用户可以取消点赞P0
FR3显示内容的总点赞数P0
FR4显示当前用户是否已点赞P0
FR5查看点赞列表(谁点赞了)P1
FR6点赞通知创作者P1
FR7按点赞数排序(热门内容)P1
FR8点赞数统计和分析P2

2.2 非功能性需求

性能需求

  • 点赞/取消响应时间:<50ms(P95)
  • 点赞数查询:<20ms(P95)
  • 支持QPS:读10万+,写1万+
  • 批量查询点赞状态:<100ms(100个内容)

一致性需求

  • 点赞数最终一致(允许短暂延迟)
  • 点赞/取消操作强一致(不能重复点赞)
  • 点赞记录可追溯(审计、风控)

可用性需求

  • 系统可用性:99.9%
  • 点赞失败有友好提示
  • 缓存故障可降级到数据库

扩展性需求

  • 支持多种点赞类型(文章、视频、评论)
  • 支持点赞数阈值提醒(突破1000赞)
  • 支持取消点赞后的"反悔"功能

2.3 数据规模

假设

  • 日活用户:1000万
  • 每用户每天点赞:10次
  • 日点赞量:1亿次
  • 存储周期:永久(支持取消)

计算

写QPS = 1亿 / 86400 ≈ 1157 QPS(峰值3000+)
读QPS = 写QPS × 100 ≈ 115,700 QPS
存储 = 1亿条/天 × 365天 × 20字节 ≈ 730GB/年

2.4 业务约束

  1. 用户约束:每个用户对同一内容只能点赞一次
  2. 时效约束:点赞数延迟<1秒可接受
  3. 安全约束:防止刷赞、批量点赞
  4. 合规约束:用户可以删除自己的点赞记录

三、技术选型

3.1 存储方案对比

方案优点缺点适用场景
MySQL事务支持、可靠性能一般、并发低持久化存储
Redis极高性能易丢失、成本高缓存热数据
MongoDB高性能、灵活无事务点赞记录
Cassandra高写入性能查询受限海量数据

推荐方案

  • MySQL:存储点赞记录、点赞数(持久化)
  • Redis:缓存点赞数、点赞状态(加速查询)
  • 消息队列:异步写库、削峰填谷

3.2 缓存策略对比

策略优点缺点适用场景
Cache Aside简单、可控一致性差通用场景
Read Through自动加载复杂读多写少
Write Through强一致性能差强一致性
Write Behind高性能可能丢数据高并发写

推荐:Write Behind(异步写库)

3.3 技术栈

组件技术选型作用
数据库MySQL持久化存储
缓存Redis热数据缓存
消息队列Kafka异步写库
应用框架Spring Boot / Go业务逻辑
通知WebSocket / 推送实时通知

四、架构设计

4.1 系统架构图

mermaid
graph TB
    subgraph 客户端
        A[移动端/Web]
    end
    
    subgraph 应用层
        B[点赞API]
        B --> C[点赞服务]
        B --> D[统计服务]
        B --> E[通知服务]
    end
    
    subgraph 缓存层
        F[Redis集群]
        F --> F1[点赞数<br/>Key: like:count:post:id]
        F --> F2[点赞状态<br/>Key: like:status:user:id:post:id]
        F --> F3[点赞列表<br/>Key: like:list:post:id]
    end
    
    subgraph 消息队列
        G[Kafka]
        C --> G
        G --> H[消费者<br/>写库]
    end
    
    subgraph 存储层
        I[MySQL主库]
        J[MySQL从库]
        H --> I
        I --> J
    end
    
    subgraph 通知层
        E --> K[WebSocket]
        E --> L[推送服务]
    end
    
    A --> B
    C --> F
    D --> F
    D --> J

4.2 核心流程

点赞流程

mermaid
sequenceDiagram
    participant U as 用户
    participant API as 点赞API
    participant Redis as Redis
    participant MQ as Kafka
    participant DB as MySQL
    participant Notify as 通知服务
    
    U->>API: 点赞请求
    API->>Redis: 检查是否已点赞
    
    alt 已点赞
        Redis-->>API: 已点赞
        API-->>U: 提示已点赞
    else 未点赞
        Redis-->>API: 未点赞
        
        par 并行处理
            API->>Redis: 1. 点赞数+1
            API->>Redis: 2. 记录点赞状态
            API->>Redis: 3. 添加到点赞列表
        and
            API->>MQ: 发送点赞消息
        end
        
        API-->>U: 点赞成功
        
        MQ->>DB: 异步写库
        MQ->>Notify: 触发通知
        Notify->>创作者: 推送点赞通知
    end

取消点赞流程

mermaid
sequenceDiagram
    participant U as 用户
    participant API as 点赞API
    participant Redis as Redis
    participant MQ as Kafka
    participant DB as MySQL
    
    U->>API: 取消点赞
    API->>Redis: 检查是否已点赞
    
    alt 未点赞
        Redis-->>API: 未点赞
        API-->>U: 提示未点赞
    else 已点赞
        par 并行处理
            API->>Redis: 1. 点赞数-1
            API->>Redis: 2. 删除点赞状态
            API->>Redis: 3. 从列表移除
        and
            API->>MQ: 发送取消消息
        end
        
        API-->>U: 取消成功
        
        MQ->>DB: 异步更新数据库
    end

4.3 数据库设计

点赞记录表(MySQL)

sql
CREATE TABLE like_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL COMMENT '用户ID',
    target_type TINYINT NOT NULL COMMENT '目标类型 1文章 2视频 3评论',
    target_id BIGINT NOT NULL COMMENT '目标ID',
    status TINYINT DEFAULT 1 COMMENT '状态 1点赞 0取消',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    UNIQUE KEY uk_user_target (user_id, target_type, target_id),
    KEY idx_target (target_type, target_id, create_time),
    KEY idx_user (user_id, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 
COMMENT='点赞记录表';

-- 分表策略:按target_id取模分16张表
-- like_record_0, like_record_1, ..., like_record_15

点赞统计表(MySQL)

sql
CREATE TABLE like_count (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    target_type TINYINT NOT NULL COMMENT '目标类型',
    target_id BIGINT NOT NULL COMMENT '目标ID',
    like_count INT DEFAULT 0 COMMENT '点赞数',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    UNIQUE KEY uk_target (target_type, target_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='点赞统计表';

Redis数据结构

redis
# 1. 点赞数(String)
Key: like:count:{target_type}:{target_id}
Value: 12345
TTL: 永久(热数据)

# 2. 用户点赞状态(String)
Key: like:status:{user_id}:{target_type}:{target_id}
Value: 1(已点赞)或 0(未点赞)
TTL: 7天

# 3. 点赞用户列表(ZSet,按时间排序)
Key: like:list:{target_type}:{target_id}
Score: 时间戳
Member: user_id
TTL: 1天(只缓存最新的)

# 4. 用户点赞列表(ZSet)
Key: user:like:{user_id}:{target_type}
Score: 时间戳
Member: target_id
TTL: 7天

五、核心实现

5.1 点赞服务(Go实现)

点击查看完整实现
go
package like

import (
    "context"
    "encoding/json"
    "fmt"
    "time"
    
    "github.com/go-redis/redis/v8"
    "gorm.io/gorm"
)

// LikeService 点赞服务
type LikeService struct {
    db    *gorm.DB
    redis *redis.Client
    mq    MessageQueue
}

// LikeRecord 点赞记录
type LikeRecord struct {
    ID         int64     `gorm:"primarykey"`
    UserID     int64     `gorm:"index:uk_user_target,priority:1"`
    TargetType int8      `gorm:"index:uk_user_target,priority:2"`
    TargetID   int64     `gorm:"index:uk_user_target,priority:3;index:idx_target,priority:1"`
    Status     int8      `gorm:"default:1"`
    CreateTime time.Time `gorm:"autoCreateTime"`
    UpdateTime time.Time `gorm:"autoUpdateTime"`
}

// Like 点赞
func (s *LikeService) Like(ctx context.Context, userID, targetID int64, targetType int8) error {
    // 1. 检查是否已点赞(Redis)
    statusKey := s.getLikeStatusKey(userID, targetID, targetType)
    status, err := s.redis.Get(ctx, statusKey).Int()
    
    if err == nil && status == 1 {
        return fmt.Errorf("already liked")
    }
    
    // 2. Redis操作(原子性)
    pipe := s.redis.Pipeline()
    
    // 2.1 点赞数+1
    countKey := s.getLikeCountKey(targetID, targetType)
    pipe.Incr(ctx, countKey)
    
    // 2.2 记录点赞状态
    pipe.Set(ctx, statusKey, 1, 7*24*time.Hour)
    
    // 2.3 添加到点赞列表(ZSet,score为时间戳)
    listKey := s.getLikeListKey(targetID, targetType)
    pipe.ZAdd(ctx, listKey, &redis.Z{
        Score:  float64(time.Now().Unix()),
        Member: userID,
    })
    pipe.Expire(ctx, listKey, 24*time.Hour)
    
    // 2.4 用户点赞列表
    userLikeKey := s.getUserLikeKey(userID, targetType)
    pipe.ZAdd(ctx, userLikeKey, &redis.Z{
        Score:  float64(time.Now().Unix()),
        Member: targetID,
    })
    
    _, err = pipe.Exec(ctx)
    if err != nil {
        return fmt.Errorf("redis error: %w", err)
    }
    
    // 3. 发送到消息队列(异步写库)
    msg := LikeMessage{
        UserID:     userID,
        TargetID:   targetID,
        TargetType: targetType,
        Action:     "like",
        Timestamp:  time.Now().Unix(),
    }
    
    if err := s.mq.Send("like_topic", msg); err != nil {
        // 记录日志,但不影响用户体验
        // 后续可通过对账补偿
        log.Errorf("send mq error: %v", err)
    }
    
    return nil
}

// Unlike 取消点赞
func (s *LikeService) Unlike(ctx context.Context, userID, targetID int64, targetType int8) error {
    // 1. 检查是否已点赞
    statusKey := s.getLikeStatusKey(userID, targetID, targetType)
    status, err := s.redis.Get(ctx, statusKey).Int()
    
    if err != nil || status != 1 {
        return fmt.Errorf("not liked yet")
    }
    
    // 2. Redis操作
    pipe := s.redis.Pipeline()
    
    // 2.1 点赞数-1(防止负数)
    countKey := s.getLikeCountKey(targetID, targetType)
    pipe.Decr(ctx, countKey)
    
    // 2.2 删除点赞状态
    pipe.Del(ctx, statusKey)
    
    // 2.3 从点赞列表移除
    listKey := s.getLikeListKey(targetID, targetType)
    pipe.ZRem(ctx, listKey, userID)
    
    // 2.4 从用户点赞列表移除
    userLikeKey := s.getUserLikeKey(userID, targetType)
    pipe.ZRem(ctx, userLikeKey, targetID)
    
    _, err = pipe.Exec(ctx)
    if err != nil {
        return fmt.Errorf("redis error: %w", err)
    }
    
    // 3. 发送到消息队列
    msg := LikeMessage{
        UserID:     userID,
        TargetID:   targetID,
        TargetType: targetType,
        Action:     "unlike",
        Timestamp:  time.Now().Unix(),
    }
    
    s.mq.Send("like_topic", msg)
    
    return nil
}

// GetLikeCount 获取点赞数
func (s *LikeService) GetLikeCount(ctx context.Context, targetID int64, targetType int8) (int64, error) {
    countKey := s.getLikeCountKey(targetID, targetType)
    
    // 先从Redis获取
    count, err := s.redis.Get(ctx, countKey).Int64()
    if err == nil {
        return count, nil
    }
    
    // Redis miss,从数据库获取
    var record LikeCount
    err = s.db.Where("target_type = ? AND target_id = ?", targetType, targetID).
        First(&record).Error
    
    if err != nil {
        if err == gorm.ErrRecordNotFound {
            return 0, nil
        }
        return 0, err
    }
    
    // 写回Redis
    s.redis.Set(ctx, countKey, record.LikeCount, 0)
    
    return int64(record.LikeCount), nil
}

// IsLiked 判断是否已点赞
func (s *LikeService) IsLiked(ctx context.Context, userID, targetID int64, targetType int8) (bool, error) {
    statusKey := s.getLikeStatusKey(userID, targetID, targetType)
    
    // 从Redis获取
    status, err := s.redis.Get(ctx, statusKey).Int()
    if err == nil {
        return status == 1, nil
    }
    
    // Redis miss,从数据库获取
    var record LikeRecord
    err = s.db.Where("user_id = ? AND target_type = ? AND target_id = ? AND status = 1",
        userID, targetType, targetID).First(&record).Error
    
    if err != nil {
        if err == gorm.ErrRecordNotFound {
            // 写入Redis(未点赞)
            s.redis.Set(ctx, statusKey, 0, 7*24*time.Hour)
            return false, nil
        }
        return false, err
    }
    
    // 写入Redis(已点赞)
    s.redis.Set(ctx, statusKey, 1, 7*24*time.Hour)
    return true, nil
}

// GetLikeList 获取点赞列表
func (s *LikeService) GetLikeList(ctx context.Context, targetID int64, targetType int8, offset, limit int) ([]int64, error) {
    listKey := s.getLikeListKey(targetID, targetType)
    
    // 从Redis ZSet获取(按时间倒序)
    members, err := s.redis.ZRevRange(ctx, listKey, int64(offset), int64(offset+limit-1)).Result()
    
    if err != nil || len(members) == 0 {
        // Redis miss,从数据库获取
        var records []LikeRecord
        err := s.db.Where("target_type = ? AND target_id = ? AND status = 1", targetType, targetID).
            Order("create_time DESC").
            Offset(offset).
            Limit(limit).
            Find(&records).Error
        
        if err != nil {
            return nil, err
        }
        
        userIDs := make([]int64, len(records))
        for i, record := range records {
            userIDs[i] = record.UserID
        }
        return userIDs, nil
    }
    
    // 转换为int64
    userIDs := make([]int64, len(members))
    for i, member := range members {
        // 这里简化处理,实际需要类型转换
        fmt.Sscanf(member, "%d", &userIDs[i])
    }
    
    return userIDs, nil
}

// BatchGetLikeInfo 批量获取点赞信息
func (s *LikeService) BatchGetLikeInfo(ctx context.Context, userID int64, targetIDs []int64, targetType int8) (map[int64]LikeInfo, error) {
    result := make(map[int64]LikeInfo)
    
    // 使用Pipeline批量查询Redis
    pipe := s.redis.Pipeline()
    
    countCmds := make(map[int64]*redis.StringCmd)
    statusCmds := make(map[int64]*redis.StringCmd)
    
    for _, targetID := range targetIDs {
        countKey := s.getLikeCountKey(targetID, targetType)
        countCmds[targetID] = pipe.Get(ctx, countKey)
        
        statusKey := s.getLikeStatusKey(userID, targetID, targetType)
        statusCmds[targetID] = pipe.Get(ctx, statusKey)
    }
    
    _, err := pipe.Exec(ctx)
    if err != nil && err != redis.Nil {
        return nil, err
    }
    
    // 解析结果
    for _, targetID := range targetIDs {
        info := LikeInfo{
            TargetID: targetID,
        }
        
        // 点赞数
        if count, err := countCmds[targetID].Int64(); err == nil {
            info.LikeCount = count
        }
        
        // 是否已点赞
        if status, err := statusCmds[targetID].Int(); err == nil {
            info.IsLiked = (status == 1)
        }
        
        result[targetID] = info
    }
    
    return result, nil
}

// Key生成方法
func (s *LikeService) getLikeCountKey(targetID int64, targetType int8) string {
    return fmt.Sprintf("like:count:%d:%d", targetType, targetID)
}

func (s *LikeService) getLikeStatusKey(userID, targetID int64, targetType int8) string {
    return fmt.Sprintf("like:status:%d:%d:%d", userID, targetType, targetID)
}

func (s *LikeService) getLikeListKey(targetID int64, targetType int8) string {
    return fmt.Sprintf("like:list:%d:%d", targetType, targetID)
}

func (s *LikeService) getUserLikeKey(userID int64, targetType int8) string {
    return fmt.Sprintf("user:like:%d:%d", userID, targetType)
}

// LikeInfo 点赞信息
type LikeInfo struct {
    TargetID  int64
    LikeCount int64
    IsLiked   bool
}

// LikeMessage 点赞消息
type LikeMessage struct {
    UserID     int64  `json:"user_id"`
    TargetID   int64  `json:"target_id"`
    TargetType int8   `json:"target_type"`
    Action     string `json:"action"` // like/unlike
    Timestamp  int64  `json:"timestamp"`
}

5.2 消息消费者(异步写库)

点击查看完整实现
go
package consumer

import (
    "context"
    "encoding/json"
    "time"
    
    "gorm.io/gorm"
    "gorm.io/gorm/clause"
)

// LikeConsumer 点赞消息消费者
type LikeConsumer struct {
    db *gorm.DB
}

// ConsumeLikeMessage 消费点赞消息
func (c *LikeConsumer) ConsumeLikeMessage(msg []byte) error {
    var likeMsg LikeMessage
    if err := json.Unmarshal(msg, &likeMsg); err != nil {
        return err
    }
    
    if likeMsg.Action == "like" {
        return c.handleLike(likeMsg)
    } else if likeMsg.Action == "unlike" {
        return c.handleUnlike(likeMsg)
    }
    
    return nil
}

// handleLike 处理点赞
func (c *LikeConsumer) handleLike(msg LikeMessage) error {
    return c.db.Transaction(func(tx *gorm.DB) error {
        // 1. 插入点赞记录(如果存在则更新status)
        record := LikeRecord{
            UserID:     msg.UserID,
            TargetType: msg.TargetType,
            TargetID:   msg.TargetID,
            Status:     1,
            CreateTime: time.Unix(msg.Timestamp, 0),
            UpdateTime: time.Unix(msg.Timestamp, 0),
        }
        
        // UPSERT: ON DUPLICATE KEY UPDATE
        err := tx.Clauses(clause.OnConflict{
            Columns:   []clause.Column{{Name: "user_id"}, {Name: "target_type"}, {Name: "target_id"}},
            DoUpdates: clause.AssignmentColumns([]string{"status", "update_time"}),
        }).Create(&record).Error
        
        if err != nil {
            return err
        }
        
        // 2. 更新点赞统计
        err = tx.Exec(`
            INSERT INTO like_count (target_type, target_id, like_count)
            VALUES (?, ?, 1)
            ON DUPLICATE KEY UPDATE like_count = like_count + 1
        `, msg.TargetType, msg.TargetID).Error
        
        return err
    })
}

// handleUnlike 处理取消点赞
func (c *LikeConsumer) handleUnlike(msg LikeMessage) error {
    return c.db.Transaction(func(tx *gorm.DB) error {
        // 1. 更新点赞记录状态
        err := tx.Model(&LikeRecord{}).
            Where("user_id = ? AND target_type = ? AND target_id = ?", 
                msg.UserID, msg.TargetType, msg.TargetID).
            Update("status", 0).
            Update("update_time", time.Unix(msg.Timestamp, 0)).
            Error
        
        if err != nil {
            return err
        }
        
        // 2. 更新点赞统计(防止负数)
        err = tx.Exec(`
            UPDATE like_count 
            SET like_count = GREATEST(like_count - 1, 0)
            WHERE target_type = ? AND target_id = ?
        `, msg.TargetType, msg.TargetID).Error
        
        return err
    })
}

5.3 Java实现(Spring Boot)

java
@Service
public class LikeService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private LikeRecordMapper likeRecordMapper;
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    /**
     * 点赞
     */
    @Transactional(rollbackFor = Exception.class)
    public void like(Long userId, Long targetId, Integer targetType) {
        // 1. 检查是否已点赞
        String statusKey = getLikeStatusKey(userId, targetId, targetType);
        Integer status = (Integer) redisTemplate.opsForValue().get(statusKey);
        
        if (status != null && status == 1) {
            throw new BusinessException("已经点赞过了");
        }
        
        // 2. Redis操作(Pipeline)
        redisTemplate.executePipelined(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                // 点赞数+1
                String countKey = getLikeCountKey(targetId, targetType);
                operations.opsForValue().increment(countKey, 1);
                
                // 记录点赞状态
                operations.opsForValue().set(statusKey, 1, 7, TimeUnit.DAYS);
                
                // 添加到点赞列表
                String listKey = getLikeListKey(targetId, targetType);
                operations.opsForZSet().add(listKey, userId, System.currentTimeMillis());
                operations.expire(listKey, 1, TimeUnit.DAYS);
                
                // 用户点赞列表
                String userLikeKey = getUserLikeKey(userId, targetType);
                operations.opsForZSet().add(userLikeKey, targetId, System.currentTimeMillis());
                
                return null;
            }
        });
        
        // 3. 发送到Kafka
        LikeMessage msg = new LikeMessage();
        msg.setUserId(userId);
        msg.setTargetId(targetId);
        msg.setTargetType(targetType);
        msg.setAction("like");
        msg.setTimestamp(System.currentTimeMillis());
        
        kafkaTemplate.send("like_topic", JSON.toJSONString(msg));
    }
    
    /**
     * 取消点赞
     */
    public void unlike(Long userId, Long targetId, Integer targetType) {
        // 1. 检查是否已点赞
        String statusKey = getLikeStatusKey(userId, targetId, targetType);
        Integer status = (Integer) redisTemplate.opsForValue().get(statusKey);
        
        if (status == null || status != 1) {
            throw new BusinessException("还未点赞");
        }
        
        // 2. Redis操作
        redisTemplate.executePipelined(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                // 点赞数-1
                String countKey = getLikeCountKey(targetId, targetType);
                operations.opsForValue().decrement(countKey, 1);
                
                // 删除点赞状态
                operations.delete(statusKey);
                
                // 从点赞列表移除
                String listKey = getLikeListKey(targetId, targetType);
                operations.opsForZSet().remove(listKey, userId);
                
                // 从用户点赞列表移除
                String userLikeKey = getUserLikeKey(userId, targetType);
                operations.opsForZSet().remove(userLikeKey, targetId);
                
                return null;
            }
        });
        
        // 3. 发送到Kafka
        LikeMessage msg = new LikeMessage();
        msg.setUserId(userId);
        msg.setTargetId(targetId);
        msg.setTargetType(targetType);
        msg.setAction("unlike");
        msg.setTimestamp(System.currentTimeMillis());
        
        kafkaTemplate.send("like_topic", JSON.toJSONString(msg));
    }
    
    /**
     * 获取点赞数
     */
    public Long getLikeCount(Long targetId, Integer targetType) {
        String countKey = getLikeCountKey(targetId, targetType);
        
        // 从Redis获取
        Integer count = (Integer) redisTemplate.opsForValue().get(countKey);
        if (count != null) {
            return count.longValue();
        }
        
        // Redis miss,从数据库获取
        LikeCount likeCount = likeCountMapper.selectByTarget(targetType, targetId);
        if (likeCount == null) {
            return 0L;
        }
        
        // 写回Redis
        redisTemplate.opsForValue().set(countKey, likeCount.getLikeCount());
        
        return likeCount.getLikeCount().longValue();
    }
    
    /**
     * 批量获取点赞信息
     */
    public Map<Long, LikeInfo> batchGetLikeInfo(Long userId, List<Long> targetIds, Integer targetType) {
        Map<Long, LikeInfo> result = new HashMap<>();
        
        // Pipeline批量查询
        List<Object> results = redisTemplate.executePipelined(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                for (Long targetId : targetIds) {
                    // 查询点赞数
                    String countKey = getLikeCountKey(targetId, targetType);
                    operations.opsForValue().get(countKey);
                    
                    // 查询是否已点赞
                    String statusKey = getLikeStatusKey(userId, targetId, targetType);
                    operations.opsForValue().get(statusKey);
                }
                return null;
            }
        });
        
        // 解析结果(results是扁平列表,每个targetId对应2个结果)
        for (int i = 0; i < targetIds.size(); i++) {
            Long targetId = targetIds.get(i);
            Integer count = (Integer) results.get(i * 2);
            Integer status = (Integer) results.get(i * 2 + 1);
            
            LikeInfo info = new LikeInfo();
            info.setTargetId(targetId);
            info.setLikeCount(count != null ? count.longValue() : 0L);
            info.setLiked(status != null && status == 1);
            
            result.put(targetId, info);
        }
        
        return result;
    }
    
    // Key生成方法
    private String getLikeCountKey(Long targetId, Integer targetType) {
        return String.format("like:count:%d:%d", targetType, targetId);
    }
    
    private String getLikeStatusKey(Long userId, Long targetId, Integer targetType) {
        return String.format("like:status:%d:%d:%d", userId, targetType, targetId);
    }
    
    private String getLikeListKey(Long targetId, Integer targetType) {
        return String.format("like:list:%d:%d", targetType, targetId);
    }
    
    private String getUserLikeKey(Long userId, Integer targetType) {
        return String.format("user:like:%d:%d", userId, targetType);
    }
}
java
@RestController
@RequestMapping("/api/like")
public class LikeController {
    
    @Autowired
    private LikeService likeService;
    
    /**
     * 点赞
     */
    @PostMapping("/like")
    public Response<Void> like(@RequestBody LikeRequest request) {
        Long userId = UserContext.getCurrentUserId();
        likeService.like(userId, request.getTargetId(), request.getTargetType());
        return Response.success();
    }
    
    /**
     * 取消点赞
     */
    @PostMapping("/unlike")
    public Response<Void> unlike(@RequestBody LikeRequest request) {
        Long userId = UserContext.getCurrentUserId();
        likeService.unlike(userId, request.getTargetId(), request.getTargetType());
        return Response.success();
    }
    
    /**
     * 获取点赞数
     */
    @GetMapping("/count")
    public Response<Long> getLikeCount(@RequestParam Long targetId, 
                                       @RequestParam Integer targetType) {
        Long count = likeService.getLikeCount(targetId, targetType);
        return Response.success(count);
    }
    
    /**
     * 批量获取点赞信息
     */
    @PostMapping("/batch")
    public Response<Map<Long, LikeInfo>> batchGetLikeInfo(@RequestBody BatchLikeRequest request) {
        Long userId = UserContext.getCurrentUserId();
        Map<Long, LikeInfo> result = likeService.batchGetLikeInfo(
            userId, request.getTargetIds(), request.getTargetType()
        );
        return Response.success(result);
    }
}

六、性能优化

6.1 缓存优化

1. 缓存预热

go
// 热门内容预加载到Redis
func (s *LikeService) PreloadHotContent(ctx context.Context) error {
    // 从数据库查询热门内容
    var hotContents []LikeCount
    err := s.db.Where("like_count > ?", 1000).
        Order("like_count DESC").
        Limit(1000).
        Find(&hotContents).Error
    
    if err != nil {
        return err
    }
    
    // 批量写入Redis
    pipe := s.redis.Pipeline()
    for _, content := range hotContents {
        countKey := s.getLikeCountKey(content.TargetID, content.TargetType)
        pipe.Set(ctx, countKey, content.LikeCount, 0)
    }
    
    _, err = pipe.Exec(ctx)
    return err
}

2. 缓存穿透防护

go
// 使用布隆过滤器
func (s *LikeService) IsContentExist(targetID int64) bool {
    return s.bloomFilter.Test([]byte(fmt.Sprintf("%d", targetID)))
}

6.2 数据库优化

1. 读写分离

go
// 写主库
func (s *LikeService) writeLike(record *LikeRecord) error {
    return s.masterDB.Create(record).Error
}

// 读从库
func (s *LikeService) readLikeCount(targetID int64, targetType int8) (*LikeCount, error) {
    var count LikeCount
    err := s.slaveDB.Where("target_type = ? AND target_id = ?", targetType, targetID).
        First(&count).Error
    return &count, err
}

2. 分表策略

go
// 按target_id分表
func getTableName(targetID int64) string {
    tableIndex := targetID % 16
    return fmt.Sprintf("like_record_%d", tableIndex)
}

6.3 性能数据

指标优化前优化后提升
点赞响应时间200ms15ms13.3x
查询响应时间100ms5ms20x
写QPS50010,00020x
读QPS5,000100,00020x
Redis命中率80%99%-

七、数据一致性

7.1 缓存和数据库一致性

策略:延迟双删 + 定时对账

go
// 延迟双删
func (s *LikeService) UpdateLikeWithDoubleDelete(ctx context.Context, record *LikeRecord) error {
    countKey := s.getLikeCountKey(record.TargetID, record.TargetType)
    
    // 1. 删除缓存
    s.redis.Del(ctx, countKey)
    
    // 2. 更新数据库
    err := s.db.Save(record).Error
    if err != nil {
        return err
    }
    
    // 3. 延迟500ms再删除缓存
    time.AfterFunc(500*time.Millisecond, func() {
        s.redis.Del(context.Background(), countKey)
    })
    
    return nil
}

7.2 定时对账

go
// 定时任务:Redis和MySQL对账
func (s *LikeService) ReconcileData(ctx context.Context) error {
    // 1. 从MySQL查询点赞统计
    var counts []LikeCount
    err := s.db.Find(&counts).Error
    if err != nil {
        return err
    }
    
    // 2. 对比Redis
    for _, count := range counts {
        countKey := s.getLikeCountKey(count.TargetID, count.TargetType)
        redisCount, _ := s.redis.Get(ctx, countKey).Int64()
        
        // 差异超过阈值,修正
        if abs(redisCount-int64(count.LikeCount)) > 10 {
            log.Warnf("data inconsistency: target=%d, redis=%d, mysql=%d", 
                count.TargetID, redisCount, count.LikeCount)
            
            // 以MySQL为准,更新Redis
            s.redis.Set(ctx, countKey, count.LikeCount, 0)
        }
    }
    
    return nil
}

八、防刷机制

8.1 限流

go
// 用户维度限流:每秒最多点赞5次
func (s *LikeService) CheckRateLimit(ctx context.Context, userID int64) error {
    key := fmt.Sprintf("like:rate_limit:%d", userID)
    
    // 滑动窗口限流
    now := time.Now().Unix()
    pipe := s.redis.Pipeline()
    
    // 移除1秒前的记录
    pipe.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", now-1))
    
    // 添加当前时间戳
    pipe.ZAdd(ctx, key, &redis.Z{Score: float64(now), Member: now})
    
    // 统计1秒内的数量
    pipe.ZCard(ctx, key)
    
    // 设置过期时间
    pipe.Expire(ctx, key, 2*time.Second)
    
    cmds, err := pipe.Exec(ctx)
    if err != nil {
        return err
    }
    
    // 获取计数
    count := cmds[2].(*redis.IntCmd).Val()
    if count > 5 {
        return fmt.Errorf("rate limit exceeded")
    }
    
    return nil
}

8.2 风控检测

go
// 异常行为检测
func (s *LikeService) DetectAbnormal(userID int64) bool {
    // 1. 检查短时间内点赞数量
    count := s.getUserLikeCountInMinute(userID)
    if count > 100 {
        return true // 异常:1分钟点赞超过100次
    }
    
    // 2. 检查点赞后立即取消的频率
    cancelRate := s.getCancelRate(userID)
    if cancelRate > 0.8 {
        return true // 异常:取消率超过80%
    }
    
    // 3. 检查是否批量点赞同一作者
    sameAuthorRate := s.getSameAuthorRate(userID)
    if sameAuthorRate > 0.9 {
        return true // 异常:90%点赞同一作者
    }
    
    return false
}

九、面试要点

9.1 常见追问

Q1: 如何保证点赞数的准确性?

A:

  1. Redis原子操作:使用INCR/DECR保证单次操作原子性
  2. 消息队列:异步写库,防止丢失
  3. 定时对账:Redis和MySQL定期对账,发现差异修正
  4. 幂等设计:消费者处理消息幂等,防止重复扣减

Q2: 如何处理缓存和数据库的一致性?

A:

  • 写操作:先更新MySQL,再删除Redis(延迟双删)
  • 读操作:先读Redis,miss再读MySQL并回写
  • 异步补偿:消息队列保证最终一致性
  • 定时对账:发现不一致主动修正

Q3: 如何优化热门内容的点赞性能?

A:

  1. 缓存预热:热门内容提前加载到Redis
  2. 本地缓存:应用层增加Guava Cache
  3. 批量写入:攒批后批量写MySQL(牺牲一致性)
  4. 读写分离:读从库,降低主库压力

Q4: 如何防止恶意刷赞?

A:

  1. 限流:用户维度、IP维度限流
  2. 验证码:高频操作要求验证码
  3. 风控规则:异常行为检测(短时间大量点赞)
  4. 黑名单:封禁刷赞账号

9.2 扩展知识点

十、总结

点赞系统看似简单,但要做好需要考虑:

  1. 性能优化:Redis缓存 + 异步写库,支持10万+QPS
  2. 数据一致性:延迟双删 + 定时对账,保证最终一致
  3. 并发控制:唯一索引 + 原子操作,防止重复点赞
  4. 防刷机制:限流 + 风控,保护系统稳定性

面试中要能说清楚架构设计、缓存策略、一致性保证、性能优化等关键点。


相关场景题

正在精进