React+Spring Boot在线教育平台开发:实时直播课堂与作业管理系统的技术架构实践

2026-07-20 12:11:23 26 次浏览
在线教育直播课堂ReactSpring BootRedis

在线教育行业近年快速发展,直播课堂和互动教学成为核心功能需求。承恒信息科技曾协助一家职业教育机构开发在线学习平台,从需求分析到上线交付过程中,直播延迟、万人同时在线选课的高并发处理以及作业自动批改是三个最具技术挑战的环节。本文将分享这套基于React+Spring Boot技术栈的完整实现方案。

一、平台架构与技术选型

平台前端采用React 18 + Ant Design构建教师端和学生端,后端使用Spring Boot 3 + MyBatis-Plus + MySQL + Redis。直播模块基于WebRTC + SRS(Simple Realtime Server)推流方案,实时互动使用Socket.IO实现双向通信。系统分为课程管理、直播课堂、作业系统、学习数据中心四大模块。

正文图1:在线教育平台架构图

选课高峰期并发量可达平时的20倍,Redis负责缓存课程列表、用户选课状态和直播间人数统计,通过Lua脚本保障扣减库存的原子性。MySQL采用读写分离架构,主库写入选课记录,从库查询课程信息,分摊读压力。

二、高并发选课系统与Redis优化

选课是教育平台的高并发场景,热门课程开放选课时瞬间QPS可达上万。以下是基于Redis + Lua脚本实现的选课核心代码,确保不超卖:

@Service
public class CourseSelectionService {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private CourseMapper courseMapper;
    @Autowired
    private RabbitTemplate rabbitTemplate;

    private static final String COURSE_STOCK_KEY = "course:stock:";
    private static final String SELECTED_SET_KEY = "course:selected:";

    // Lua脚本:原子性检查+扣减库存+记录选课
    private static final String SELECT_LUA = 
        "local stock = redis.call('GET', KEYS[1])\n" +
        "if not stock or tonumber(stock) <= 0 then return -1 end\n" +
        "local selected = redis.call('SISMEMBER', KEYS[2], ARGV[1])\n" +
        "if selected == 1 then return -2 end\n" +
        "redis.call('DECR', KEYS[1])\n" +
        "redis.call('SADD', KEYS[2], ARGV[1])\n" +
        "return tonumber(stock) - 1";

    @Transactional
    public Result selectCourse(Long courseId, Long userId) {
        String stockKey = COURSE_STOCK_KEY + courseId;
        String setKey = SELECTED_SET_KEY + courseId;

        // 执行Lua脚本(原子操作)
        DefaultRedisScript script = new DefaultRedisScript<>(SELECT_LUA, Long.class);
        Long remaining = redisTemplate.execute(script, 
            List.of(stockKey, setKey), userId.toString());

        if (remaining == null || remaining == -1) {
            return Result.fail("课程已满,选课失败");
        }
        if (remaining == -2) {
            return Result.fail("您已选择该课程,请勿重复选课");
        }

        // 异步写入数据库(削峰填谷)
        CourseSelectionMessage msg = new CourseSelectionMessage();
        msg.setCourseId(courseId);
        msg.setUserId(userId);
        msg.setSelectedAt(LocalDateTime.now());
        rabbitTemplate.convertAndSend("course.selection.queue", msg);

        return Result.ok("选课成功", remaining);
    }

    /**
     * 选课结果异步落库消费者
     */
    @RabbitListener(queues = "course.selection.queue")
    public void persistSelection(CourseSelectionMessage msg) {
        try {
            CourseSelection selection = new CourseSelection();
            selection.setCourseId(msg.getCourseId());
            selection.setUserId(msg.getUserId());
            selection.setSelectedAt(msg.getSelectedAt());
            selection.setStatus("ACTIVE");
            courseMapper.insertSelection(selection);
        } catch (DuplicateKeyException e) {
            // 唯一索引兜底,防止重复选课
            log.warn("重复选课被数据库唯一索引拦截: user={}, course={}", 
                msg.getUserId(), msg.getCourseId());
        }
    }

    /**
     * 课程库存预热到Redis
     */
    public void preloadCourseStock(Long courseId) {
        Course course = courseMapper.selectById(courseId);
        if (course != null) {
            int remaining = course.getMaxCapacity() - course.getSelectedCount();
            redisTemplate.opsForValue().set(
                COURSE_STOCK_KEY + courseId, remaining, 24, TimeUnit.HOURS);
            log.info("课程库存预热: courseId={}, remaining={}", courseId, remaining);
        }
    }
}

选课方案通过Lua脚本将库存检查、扣减和选课记录合并为一次原子操作,Redis端响应时间<2ms。数据库写入通过RabbitMQ异步削峰,避免瞬时高并发直接冲击MySQL。在压测中,1万QPS并发选课场景下无超卖,选课结果在30秒内全部落库,数据库CPU峰值仅35%。

三、实时直播课堂与互动

正文图2:直播课堂交互流程图

直播课堂使用WebRTC推流到SRS媒体服务器,学生端通过HLS/FLV协议拉流观看。实时互动(举手、提问、答题)通过Socket.IO双向通信实现。以下是Spring Boot端的Socket.IO互动消息处理代码:

@Configuration
public class SocketIOConfig {

    @Bean
    public SocketIOServer socketIOServer() {
        com.corundumstudio.socketio.Configuration config = 
            new com.corundumstudio.socketio.Configuration();
        config.setPort(9092);
        config.setWorkerThreads(8);
        config.setBossThreads(2);
        config.setPingInterval(30000);
        config.setPingTimeout(60000);
        config.setMaxFramePayloadLength(1024 * 1024); // 1MB
        config.setMaxHttpContentLength(1024 * 1024);
        return new SocketIOServer(config);
    }
}

@Component
public class ClassroomEventHandler {

    @Autowired
    private SocketIOServer server;
    @Autowired
    private RedisTemplate redisTemplate;

    private static final String ROOM_KEY = "classroom:room:";

    @OnConnect
    public void onConnect(SocketIOClient client) {
        String roomId = client.getHandshakeData().getSingleUrlParam("roomId");
        String userId = client.getHandshakeData().getSingleUrlParam("userId");
        String role = client.getHandshakeData().getSingleUrlParam("role");

        client.joinRoom(roomId);
        // Redis记录在线状态
        redisTemplate.opsForHash().put(ROOM_KEY + roomId, userId, role);

        // 广播有人加入
        OnlineCountEvent event = new OnlineCountEvent();
        event.setRoomId(roomId);
        event.setOnlineCount(redisTemplate.opsForHash().size(ROOM_KEY + roomId));
        server.getRoomOperations(roomId).sendEvent("userJoined", event);
    }

    @OnEvent("raiseHand")
    public void onRaiseHand(SocketIOClient client, RaiseHandRequest request) {
        String roomId = client.getHandshakeData().getSingleUrlParam("roomId");
        // 仅教师端接收举手通知
        server.getRoomOperations(roomId)
            .sendEvent("studentRaisedHand", request);
    }

    @OnEvent("submitAnswer")
    public void onSubmitAnswer(SocketIOClient client, AnswerRequest request) {
        String roomId = client.getHandshakeData().getSingleUrlParam("roomId");
        // 统计答题结果并实时推送
        String statsKey = "classroom:answer:" + roomId + ":" + request.getQuestionId();
        redisTemplate.opsForHash().increment(statsKey, request.getAnswer(), 1);

        Map stats = redisTemplate.opsForHash().entries(statsKey);
        AnswerStatsEvent statsEvent = new AnswerStatsEvent();
        statsEvent.setQuestionId(request.getQuestionId());
        statsEvent.setStats(stats);
        server.getRoomOperations(roomId).sendEvent("answerStats", statsEvent);
    }

    @OnDisconnect
    public void onDisconnect(SocketIOClient client) {
        String roomId = client.getHandshakeData().getSingleUrlParam("roomId");
        String userId = client.getHandshakeData().getSingleUrlParam("userId");
        redisTemplate.opsForHash().delete(ROOM_KEY + roomId, userId);

        OnlineCountEvent event = new OnlineCountEvent();
        event.setRoomId(roomId);
        event.setOnlineCount(redisTemplate.opsForHash().size(ROOM_KEY + roomId));
        server.getRoomOperations(roomId).sendEvent("userLeft", event);
    }
}

直播互动方案通过Socket.IO实现毫秒级双向通信,举手、答题等互动事件延迟<100ms。在线人数通过Redis Hash实时统计,直播间最大支持5000人同时在线互动。答题统计使用Redis Hash增量计数,教师端可实时看到每个选项的选择人数分布,答题数据延迟<50ms。

四、作业自动批改与部署

正文图3:作业批改系统流程图

作业系统支持客观题自动批改和主观题AI辅助评分。客观题通过标准答案比对自动计分,主观题通过自然语言处理算法给出参考分。以下是作业批改服务和Docker部署配置:

# docker-compose.yml 在线教育平台部署
version: '3.8'
services:
  # Spring Boot 后端
  education-api:
    build: ./education-backend
    ports: ["8080:8080"]
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - MYSQL_HOST=mysql-master
      - REDIS_HOST=redis
      - RABBITMQ_HOST=rabbitmq
      - SRS_SERVER=srs:1985
    depends_on: [mysql-master, mysql-slave, redis, rabbitmq, srs]
    deploy:
      replicas: 3
      resources:
        limits: { cpus: '2', memory: 1G }

  # React 前端(Nginx托管)
  education-web:
    build: ./education-frontend
    ports: ["80:80", "443:443"]
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl

  # Socket.IO 互动服务
  socketio-server:
    build: ./socketio-service
    ports: ["9092:9092"]
    environment:
      - REDIS_HOST=redis
    deploy:
      replicas: 2

  # SRS 直播推流服务器
  srs:
    image: ossrs/srs:5
    ports: ["1935:1935", "8080:8080", "1985:1985"]
    volumes:
      - ./srs.conf:/usr/local/srs/conf/srs.conf

  # MySQL 读写分离
  mysql-master:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: education
    volumes: [mysql_master_data:/var/lib/mysql]

  mysql-slave:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: education
      MYSQL_MASTER_HOST: mysql-master
    depends_on: [mysql-master]
    volumes: [mysql_slave_data:/var/lib/mysql]

  redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru --appendonly yes
    volumes: [redis_data:/data]

  rabbitmq:
    image: rabbitmq:3.12-management
    ports: ["5672:5672", "15672:15672"]

volumes:
  mysql_master_data:
  mysql_slave_data:
  redis_data:

平台采用完整的容器化部署方案,后端3副本负载均衡,MySQL主从读写分离,SRS独立部署处理直播推流。生产环境运行指标:直播首屏加载时间1.2秒,互动消息平均延迟80ms,选课系统支持1万QPS并发,作业自动批改客观题准确率100%,主观题AI评分与人工评分相关系数达0.85。平台已稳定服务3万余名学生,日均直播课时超过500小时。


关于承恒信息科技

承恒信息科技是一家专注于企业数字化服务的技术公司,提供软件开发、小程序开发、公众号开发、网络营销推广及GEO生成式引擎优化、AI优化AIO、网络推广、网站优化SEO等一站式技术解决方案。技术栈涵盖Java、.NET Core、Python、Node.js、React、Vue等主流技术,专注为各行业企业提供高性能、高可用的系统架构设计与开发服务。


🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。