每天一道面试题之架构篇|设计千万级高并发点赞/收藏系统架构

面试官:"请设计一个支持千万级用户同时点赞/收藏的系统,要求高可用、高性能、强一致性。"

点赞/收藏功能看似简单,但在高并发场景下却是一个典型的系统设计挑战。今天我们来深入剖析如何构建一个稳健的高并发互动系统。

一、需求分析与核心难点

核心功能需求

  • 点赞/取消点赞操作
  • 实时计数查询
  • 用户点赞列表查询
  • 内容被点赞列表查询

非功能性需求

  • 高可用性(99.99%)
  • 低延迟(<50ms)
  • 高吞吐量(>10w TPS)
  • 数据最终一致性
  • 水平扩展能力

核心难点

arduino 复制代码
/**
 * 高并发点赞系统核心挑战
 */
public class CoreChallenges {
    // 1. 写热点问题:热门内容瞬间千万级点赞请求
    // 2. 计数准确性:防重复、防丢失、实时性要求
    // 3. 列表查询性能:百万级粉丝列表分页查询
    // 4. 数据一致性:缓存与数据库的一致性保证
    // 5. 系统扩展性:应对业务快速增长
}

二、整体架构设计

分层架构

复制代码
客户端 → API网关 → 业务服务层 → 缓存层 → 持久层
    ↓           ↓           ↓
监控系统     消息队列     大数据平台

组件职责

  • API网关:限流、鉴权、负载均衡
  • 业务服务:点赞操作、查询服务
  • 缓存层:Redis集群,存储热点数据
  • 持久层:MySQL集群,数据持久化
  • 消息队列:异步处理、削峰填谷

三、详细设计方案

3.1 数据模型设计

Redis数据结构

ini 复制代码
// 点赞关系存储
String likeKey = "like:content:" + contentId; // Set<userId>
String userLikeKey = "user:like:" + userId;   // Hash<contentId, timestamp>

// 计数器设计
String countKey = "count:content:" + contentId; // String计数
String hotKey = "hot:content";                 // ZSet热门排行

MySQL表设计

kotlin 复制代码
/**
 * 点赞关系表
 */
@Entity
@Table(name = "user_like_relation")
public class UserLikeRelation {
    private Long id;
    private Long userId;
    private Long contentId;
    private Integer contentType;
    private Date createTime;
    private Integer status; // 1:有效, 0:取消
}

/**
 * 内容计数表
 */
@Entity
@Table(name = "content_like_count") 
public class ContentLikeCount {
    private Long id;
    private Long contentId;
    private Long likeCount;
    private Date updateTime;
}

3.2 核心业务实现

点赞服务核心代码

typescript 复制代码
@Service
@Slf4j
public class LikeServiceImpl implements LikeService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 点赞操作 - 原子性实现
     */
    @Override
    public boolean like(Long userId, Long contentId, Integer contentType) {
        // 1. 防重检查
        String likeKey = "like:content:" + contentId;
        if (Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(likeKey, userId))) {
            return false;
        }
        
        // 2. 原子性操作
        redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) {
                operations.multi();
                
                // 添加点赞关系
                operations.opsForSet().add(likeKey, userId);
                
                // 增加计数
                operations.opsForValue().increment("count:content:" + contentId);
                
                // 记录用户行为
                operations.opsForHash().put("user:like:" + userId, 
                    contentId.toString(), System.currentTimeMillis());
                
                return operations.exec();
            }
        });
        
        // 3. 异步持久化
        sendLikeMessage(userId, contentId, contentType, 1);
        return true;
    }
    
    /**
     * 获取点赞计数
     */
    public long getLikeCount(Long contentId) {
        String countKey = "count:content:" + contentId;
        Object count = redisTemplate.opsForValue().get(countKey);
        
        if (count != null) {
            return Long.parseLong(count.toString());
        }
        
        // 缓存未命中,查询数据库
        Long dbCount = likeDao.selectLikeCount(contentId);
        if (dbCount != null) {
            redisTemplate.opsForValue().set(countKey, dbCount, 1, TimeUnit.HOURS);
            return dbCount;
        }
        
        return 0L;
    }
}

3.3 异步处理机制

消息队列处理

less 复制代码
@Component
@Slf4j
public class LikeMessageConsumer {
    
    @KafkaListener(topics = "like-topic", groupId = "like-group")
    public void consumeLikeMessage(String message) {
        try {
            LikeMessage likeMessage = JSON.parseObject(message, LikeMessage.class);
            
            // 批量写入数据库
            likeDao.batchInsertLikeRelations(Collections.singletonList(likeMessage));
            
            // 更新计数
            likeDao.updateLikeCount(likeMessage.getContentId(), 
                likeMessage.getAction() == 1 ? 1 : -1);
                
        } catch (Exception e) {
            log.error("处理点赞消息失败: {}", message, e);
            // 进入死信队列或重试机制
        }
    }
}

四、高性能优化策略

多级缓存设计

kotlin 复制代码
/**
 * 多级缓存实现
 */
@Service
public class MultiLevelCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 本地缓存
    private Cache<Long, Long> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .build();
    
    /**
     * 获取计数 - 多级缓存查询
     */
    public long getCountWithMultiCache(Long contentId) {
        // 1. 查询本地缓存
        Long count = localCache.getIfPresent(contentId);
        if (count != null) {
            return count;
        }
        
        // 2. 查询Redis
        String countKey = "count:content:" + contentId;
        Object redisCount = redisTemplate.opsForValue().get(countKey);
        if (redisCount != null) {
            count = Long.parseLong(redisCount.toString());
            localCache.put(contentId, count);
            return count;
        }
        
        // 3. 查询数据库
        count = likeDao.selectLikeCount(contentId);
        if (count != null) {
            // 回写缓存
            redisTemplate.opsForValue().set(countKey, count, 1, TimeUnit.HOURS);
            localCache.put(contentId, count);
            return count;
        }
        
        return 0L;
    }
}

批量处理优化

ini 复制代码
/**
 * 批量操作优化
 */
@Service
public class BatchOperationService {
    
    /**
     * 批量点赞计数查询
     */
    public Map<Long, Long> batchGetCounts(List<Long> contentIds) {
        Map<Long, Long> result = new HashMap<>();
        List<String> keys = contentIds.stream()
            .map(id -> "count:content:" + id)
            .collect(Collectors.toList());
        
        // 批量Redis查询
        List<Object> counts = redisTemplate.opsForValue().multiGet(keys);
        
        for (int i = 0; i < contentIds.size(); i++) {
            Long contentId = contentIds.get(i);
            Object count = counts.get(i);
            
            if (count != null) {
                result.put(contentId, Long.parseLong(count.toString()));
            } else {
                // 异步加载缺失数据
                asyncLoadMissingData(contentId);
            }
        }
        
        return result;
    }
}

五、高可用保障

限流降级策略

kotlin 复制代码
/**
 * 限流降级服务
 */
@Service
public class RateLimitService {
    
    /**
     * 用户级别限流
     */
    public boolean canUserLike(Long userId, Long contentId) {
        String limitKey = "limit:user:" + userId + ":" + contentId;
        Long count = redisTemplate.opsForValue().increment(limitKey);
        redisTemplate.expire(limitKey, 1, TimeUnit.MINUTES);
        
        // 1分钟内最多操作5次
        return count <= 5;
    }
    
    /**
     * 服务降级策略
     */
    @HystrixCommand(fallbackMethod = "getCountFallback")
    public long getCountWithCircuitBreaker(Long contentId) {
        return getLikeCount(contentId);
    }
    
    public long getCountFallback(Long contentId) {
        // 返回缓存中的近似值或默认值
        return 0L;
    }
}

数据一致性保障

less 复制代码
/**
 * 数据同步保障
 */
@Component
@Slf4j
public class DataConsistencyService {
    
    @Scheduled(fixedRate = 300000) // 5分钟同步一次
    public void syncRedisToDB() {
        // 扫描Redis中的计数数据
        Set<String> keys = redisTemplate.keys("count:content:*");
        
        for (String key : keys) {
            Long contentId = extractIdFromKey(key);
            Object count = redisTemplate.opsForValue().get(key);
            
            if (count != null) {
                // 批量更新数据库
                likeDao.updateCountFromCache(contentId, Long.parseLong(count.toString()));
            }
        }
    }
}

六、扩展性设计

分库分表方案

arduino 复制代码
/**
 * 分片策略
 */
public class ShardingStrategy {
    
    /**
     * 按用户ID分片
     */
    public String getShardKey(Long userId) {
        int shardCount = 16;
        return "db_" + (userId % shardCount);
    }
    
    /**
     * 全局ID生成
     */
    public Long generateId() {
        // 雪花算法生成分布式ID
        return IdGenerator.nextId();
    }
}

七、监控与告警

关键监控指标

  • 点赞操作QPS/TPS
  • 接口响应时间P99
  • Redis缓存命中率
  • 消息队列堆积情况
  • 数据库连接池使用率

八、面试深度问答

Q1:为什么选择Redis作为主要存储?

A: Redis具有极高的读写性能(10w+ QPS),丰富的数据结构支持,原子操作能力,非常适合点赞这种高频写场景。

Q2:如何保证数据不丢失?

A: 采用多级保障:1)Redis持久化机制 2)消息队列持久化 3)数据库最终落盘 4)定期数据校对

Q3:如何处理热点Key问题?

A: 采用多级缓存、本地计数、Key分片、异步合并等策略,对极端热点启用特殊处理流程。

Q4:系统的扩展性如何?

A: 支持水平扩展:无状态服务层、Redis集群扩展、数据库分库分表、消息队列分区消费。

Q5:如何保证数据一致性?

A: 采用最终一致性模型,通过消息队列、定时任务、重试机制保证数据最终一致。

面试技巧

  1. 从业务场景出发阐述设计思路
  2. 重点突出解决的核心难点
  3. 展示对性能、可用性、一致性的权衡思考
  4. 结合实际经验分享实战教训
  5. 准备数据支撑和性能指标

本文由微信公众号"程序员小胖"整理发布,转载请注明出处。

相关推荐
网络小白不怕黑2 小时前
IPv6核心技术与NDP协议深度解析:从地址架构到邻居发现
开发语言·架构·php
踏浪无痕2 小时前
乐观锁和悲观锁,到底该怎么选?
后端·面试·架构
北京中邦兴业2 小时前
GMP洁净环境监测法规深度解读:构建以风险为核心的动态防御体系
数据库·人工智能·面试·职场和发展
别动哪条鱼2 小时前
FFmpeg模块化架构
架构·ffmpeg
milanyangbo2 小时前
像Git一样管理数据:深入解析数据库并发控制MVCC的实现
服务器·数据库·git·后端·mysql·架构·系统架构
xhxxx2 小时前
一个空函数,如何成就 JS 继承的“完美方案”?
javascript·面试·ecmascript 6
www_stdio2 小时前
JavaScript 原型继承与函数调用机制详解
前端·javascript·面试
twl2 小时前
AI Agent 十二要素方法论:构建生产级智能体的完整指南
架构
张人大 Renda Zhang2 小时前
Spring Cloud / Dubbo 是 2 楼,Kubernetes 是 1 楼,Service Mesh 是地下室:Java 微服务的“三层楼模型”
spring boot·spring cloud·云原生·架构·kubernetes·dubbo·service_mesh