51mee - AI智能招聘平台Logo
模拟面试题目大全招聘中心会员专区

设计一个支持百万级学生同时在线答题的在线考试系统,要求答题过程实时反馈成绩,且系统在考试高峰(如开学季)能稳定运行。请描述系统架构设计,包括前端、后端、数据库、缓存等组件的选型与部署,以及如何保证数据一致性和系统容错性。

深圳大学广发证券难度:中等

答案

1) 【一句话结论】
系统采用微服务+分布式架构,通过分库分表(ShardingSphere)、Redis缓存、Kafka消息队列、WebSocket实时通信,结合读写分离、动态限流等手段,保障百万级并发下的实时反馈与系统稳定性。

2) 【原理/概念讲解】
老师会解释高并发场景的系统设计逻辑:

  • 分库分表策略:为应对百万级用户,数据库需水平扩展。按用户ID哈希取模分库(如用户ID % 8 = 0则分配到db_user_0),按考试ID时间维度(如年份+月份)分表(如2024-09分到t_exam_202409表),结合读写分离(主库写,从库读),提升并发能力。借助ShardingSphere实现,支持分布式事务,解决热点数据集中问题(通过负载均衡和周期性数据再平衡)。
  • 缓存雪崩处理:Redis缓存成绩数据,设置合理过期时间(如5分钟),并采用“互斥锁防雪崩”:当缓存失效时,通过分布式锁(Redis SETNX)保证同一时间仅一个实例加载数据并更新缓存,锁时间设为2倍缓存失效频率(如5分钟过期,锁10分钟),避免锁竞争。
  • 消息队列动态限流:Kafka作为消息队列,队列容量设为100万条,消费端根据实时流量动态调整分区数(流量大时增加分区)和消费组大小(如每秒1000条消费能力),队列积压时触发告警,人工干预。
  • 实时反馈延迟控制:前端与后端建立WebSocket长连接,后端通过Redis发布高优先级消息(延迟<50ms),前端订阅并批量推送(每秒10条),结合心跳检测(每3秒发送心跳,超时重连)维护连接。
  • 数据一致性与容错:采用最终一致性,答题记录写入数据库(主库),成绩通过消息队列异步同步(Kafka),定期(每分钟)同步数据库与消息队列数据确保一致性。数据库主从切换(主库故障自动切换),Kafka持久化(日志存储),前端降级(网络异常时显示“计算中”)。

3) 【对比与适用场景】

技术选型定义特性使用场景注意点
分库分表(ShardingSphere)分布式数据库中间件,支持分库分表、读写分离、分布式事务自动化分库分表,支持复杂分片规则,事务一致性百万级用户/数据量的数据库水平扩展需全局唯一ID,避免数据分散;配置复杂度较高
缓存(Redis集群)多个Redis实例组成的集群,支持高并发读写分片存储,读写分离,高可用热点数据缓存(如成绩、题目库)需配置哨兵/集群模式,避免单点故障;数据持久化需额外配置
消息队列(Kafka)分布式消息系统,支持高吞吐、持久化消息持久化,支持消费组,可水平扩展异步通信(成绩推送、日志)需配置生产者/消费者组,避免消息丢失;需监控队列状态
WebSocket基于TCP的长连接协议实时双向通信,低延迟实时反馈(成绩、题目更新)需维护连接状态,处理断连重连

4) 【示例】

  • 分库分表配置(ShardingSphere XML):
    <!-- 分库规则(用户ID哈希分库) -->
    <sharding-rule>
      <table-rule>
        <table-name>user</table-name>
        <sharding-column>user_id</sharding-column>
        <sharding-algorithm class="com.shardingjdbc.algorithm.sharding.modulo.ModuloShardingAlgorithm">
          <property name="sharding-count">8</property> <!-- 8个数据库实例 -->
        </sharding-algorithm>
      </table-rule>
      <!-- 分表规则(考试ID按月分表) -->
      <table-rule>
        <table-name>exam</table-name>
        <sharding-column>exam_id</sharding-column>
        <sharding-algorithm class="com.shardingjdbc.algorithm.sharding.time.modulo.TimeModuloShardingAlgorithm">
          <property name="time-unit">month</property> <!-- 按月分表 -->
          <property name="sharding-count">12</property> <!-- 12个月份表 -->
        </sharding-algorithm>
      </table-rule>
    </sharding-rule>
    
  • 缓存雪崩互斥锁(Python伪代码):
    import redis
    from threading import Lock
    
    r = redis.Redis(host='redis-cluster', port=6379)
    lock = Lock()
    
    def get_score(user_id, exam_id):
        key = f"score:{user_id}:{exam_id}"
        score = r.get(key)
        if score:
            return int(score)
        # 缓存失效,加锁
        with lock:
            if r.setnx(f"lock:{user_id}:{exam_id}", "1", ex=10):  # 10秒锁(2倍缓存过期时间)
                score = db.get_score(user_id, exam_id)  # 从数据库获取
                r.setex(key, 300, score)  # 5分钟过期
                r.delete(f"lock:{user_id}:{exam_id}")
                return score
        return None
    
  • 消息队列动态限流(Kafka配置):
    # 生产者配置
    producer:
      bootstrap-servers: kafka:9092
      acks: all
      batch-size: 16384
      buffer-memory: 33554432
    # 消费者配置(动态调整)
    consumer:
      group-id: exam-score
      auto-offset-reset: earliest
      max-poll-records: 1000
      partitions: 16  # 初始16个分区,流量大时增加
    
  • 实时反馈(前端+后端):
    前端(JS):
    const socket = new WebSocket('wss://exam.wangfazheng.com/ws');
    socket.onmessage = (e) => {
        const data = JSON.parse(e.data);
        if (data.type === 'score') {
            // 批量更新(每秒10条)
            if (Date.now() % 1000 < 100) {
                updateScore(data.score);
            }
        }
    };
    socket.onclose = () => {
        setTimeout(() => socket.close(), 5000); // 重连
    };
    
    后端(Java):
    @ServerEndpoint("/ws")
    public class ScoreWebSocket {
        @OnMessage
        public void onMessage(String message) {
            JSONObject json = JSON.parseObject(message);
            if ("answer".equals(json.getString("type"))) {
                int score = calculateScore(json.getString("questionId"), json.getString("answer"));
                // 写入Redis(高优先级)
                redisTemplate.opsForValue().set("score:" + json.getString("userId"), score, 5, TimeUnit.MINUTES);
                // 发布消息(高优先级)
                kafkaProducer.send("score-topic", score, json.getString("userId"));
            }
        }
    }
    

5) 【面试口播版答案】
“面试官您好,针对百万级学生在线考试系统,我的设计采用微服务+分布式架构。前端用WebSocket实现实时交互,后端拆分为答题、成绩计算、消息队列等微服务。数据库通过ShardingSphere按用户ID分库、考试ID分表,结合读写分离提升并发。缓存用Redis集群缓存成绩,设置5分钟过期时间,采用互斥锁防雪崩。消息队列用Kafka,队列容量100万条,消费端动态限流。实时反馈通过WebSocket长连接,结合Redis高优先级消息,延迟控制在50ms内。数据一致性采用最终一致性,答题记录写入数据库,成绩异步同步。容错方面,数据库主从切换、Kafka持久化,前端降级处理异常,确保考试高峰稳定运行。”

6) 【追问清单】

  • 问题1:如何处理缓存雪崩时的锁竞争?
    回答要点:锁时间为2倍缓存失效频率(如5分钟过期,锁10分钟),避免多个实例同时加锁。
  • 问题2:消息队列积压时如何调整?
    回答要点:动态增加Kafka分区数,扩大消费组大小,触发告警后人工干预。
  • 问题3:系统扩展性如何?
    回答要点:微服务按功能拆分,数据库分库分表,水平扩展,流量大时增加实例。
  • 问题4:实时反馈延迟如何控制?
    回答要点:WebSocket心跳检测,批量推送,Redis高优先级消息,延迟<50ms。
  • 问题5:数据一致性如何保证?
    回答要点:最终一致性,定期同步数据库与消息队列数据,确保一致性。

7) 【常见坑/雷区】

  • 忽略分库分表工具:只说数据库集群,没提ShardingSphere,导致扩展性描述不具体。
  • 缓存雪崩处理不当:没提锁时间与缓存失效频率的关系,导致系统崩溃。
  • 消息队列限流静态配置:没考虑动态调整,导致积压。
  • 实时反馈用HTTP轮询:没提WebSocket,导致延迟高。
  • 数据一致性只说强一致性:百万并发下强一致性成本高,应采用最终一致性。
51mee.com致力于为招聘者提供最新、最全的招聘信息。AI智能解析岗位要求,聚合全网优质机会。
产品招聘中心面经会员专区简历解析Resume API
联系我们南京浅度求索科技有限公司admin@51mee.com
联系客服
51mee客服微信二维码 - 扫码添加客服获取帮助
© 2025 南京浅度求索科技有限公司. All rights reserved.
公安备案图标苏公网安备32010602012192号苏ICP备2025178433号-1