ITPub博客

首页 > 应用开发 > Javascript > SpringBoot + Elasticsearch + Redis:八年 Java 开发手把手教你做 “不崩、不卡、不冲突” 的大学排课系统

SpringBoot + Elasticsearch + Redis:八年 Java 开发手把手教你做 “不崩、不卡、不冲突” 的大学排课系统

Javascript 作者:tcglv629 时间:2026-03-27 15:57:39 0 删除 编辑

SpringBoot + Elasticsearch + Redis:八年 Java 开发手把手教你做 “不崩、不卡、不冲突” 的大学排课系统

做 Java 开发八年,接过不少教育类系统的活,但大学排课系统绝对是 “看着简单,做着崩溃” 的典型 —— 早期用纯 SpringBoot+MySQL 做,排一门课要关联教师、教室、时间三张表查冲突,慢到管理员拍桌子;选课高峰期学生查课表,数据库 CPU 直接飙到 99%,页面转圈半分钟;期末统计 “计算机学院本学期课程分布”,一条 Group By 语句跑了 12 秒,领导催得急,我手心全是汗。

直到后来用 SpringBoot+Elasticsearch(ES)+Redis+MySQL 重构,才彻底解决这些痛点。今天就从八年开发的视角,带大家拆解这套 “不崩、不卡、不冲突” 的排课系统,从痛点分析到代码实现,再到踩过的坑,全是实战干货,拒绝空谈理论。

一、先聊排课系统的 “痛点暴击”:为什么不能只靠 SpringBoot+MySQL?

很多新手会觉得 “排课不就是 CRUD + 时间匹配吗?用 SpringBoot+MySQL 足够了”—— 这话在学生数少于 1 万的小学校可能成立,但一旦到中大型高校(3 万 + 学生、上千教师、几百间教室),分分钟给你上一课。

先列几个我踩过的 “致命痛点”,看看纯 MySQL 方案为什么扛不住:

痛点 纯 MySQL 方案的坑 后果
三重冲突检测(教师 / 教室 / 时间) 需关联 teacher_course(教师课表)、 classroom_course(教室课表)、 time_slot(时间段)三张表,用多表 JOIN+WHERE 判断,数据量大时查询耗时超 3 秒 管理员排课要等半天,还容易漏判冲突(比如跨院系的教师重复排课)
选课高峰期查课表 学生查个人课表 / 班级课表,全靠 MySQL 单表查或关联查,1 万学生同时查,数据库连接池直接满了 页面转圈、超时,学生投诉量暴涨,教务处电话被打爆
多维度统计分析 比如 “统计某院系本学期各专业课程数”“某教师近 3 个月授课时长”,需用复杂 Group By + 子查询,数据量超 10 万条时,查询耗时超 10 秒 领导要数据时拿不出来,影响教学决策
动态调课同步 临时调课后,学生端课表要实时更新,纯 MySQL 需每次查最新数据,没缓存的话,旧课表会残留 学生按旧课表去上课,发现教室被占,场面混乱

而 SpringBoot+ES+Redis+MySQL 的组合,正好精准解决这些痛点:

  • SpringBoot:作为基础框架,快速整合其他组件,减少 boilerplate 代码(比如用 SpringBoot Starter 整合 ES、Redis);
  • MySQL:存基础数据(学生 / 教师 / 教室 / 课程的核心信息),保证数据持久化和事务性;
  • Elasticsearch:负责多维度检索(比如冲突检测、复杂课表查询)和聚合统计(比如院系课程分布),比 MySQL 快 5-10 倍;
  • Redis:缓存热门数据(学生个人课表、班级课表、公共选修课表),应对高并发查询,减轻 MySQL 压力。

可能有人会问:“为什么不用 Redis 做冲突检测?”——Redis 的 Sorted Set 或 Hash 适合单维度查询(比如查某教师的所有课),但冲突检测是 “教师 + 时间 + 教室” 三重维度,ES 的 bool 查询天生适合这种多条件组合检索,效率比 Redis 高得多。

二、整体架构设计:从 “排课” 到 “查课” 的全链路

用户端(学生/教师/管理员)

       ↓(1. 操作请求)
SpringBoot应用(排课系统核心)
       ↓(2. 身份校验)
Spring Security + JWT(权限控制)
       ↓(3. 校验通过)
SpringBoot应用(排课系统核心)
       ↓(4. 排课/调课)
业务模块{
  冲突检测模块
  课表查询模块
  统计分析模块
  调课同步模块
}
├─ 冲突检测模块 →(5. 多维度冲突检索)→ Elasticsearch(课表索引)
│                →(6. 最终排课结果存储)→ MySQL(基础数据表)
│                →(7. 同步排课数据到ES)→ Elasticsearch(课表索引)
├─ 课表查询模块 →(8. 优先查缓存)→ Redis(课表缓存)
│                →(9. 缓存未命中)→ Elasticsearch(课表索引)
│                →(10. 查后回填缓存)→ Redis(课表缓存)
├─ 统计分析模块 →(11. 多维度统计)→ Elasticsearch(课表索引)
│                →(12. 统计结果返回)→ 用户端
└─ 调课同步模块 →(13. 调课更新)→ MySQL(基础数据表)
                 →(14. 删除旧缓存)→ Redis(课表缓存)
                 →(15. 同步更新ES)→ Elasticsearch(课表索引)

整个链路的核心逻辑是 “ 数据分层处理”:

  1. 写链路(排课 / 调课) :先通过 ES 做冲突检测→检测通过后写 MySQL 存最终结果→同步数据到 ES(供查询和统计)→删除 Redis 旧缓存(避免脏数据);
  2. 读链路(查课表) :先查 Redis→未命中查 ES→查后回填 Redis;
  3. 统计链路:直接查 ES(用聚合功能),避免查 MySQL 拖慢性能。

这种设计的好处很明显:写操作保证 “不冲突”,读操作保证 “不卡顿”,统计操作保证 “不耗时”。

三、核心功能实现:代码 + 八年经验技巧

接下来分模块讲核心功能的实现,每个模块都贴关键代码,并穿插我总结的避坑技巧。

3.1 基础准备:组件整合(SpringBoot+ES+Redis+MySQL)

先搞定组件依赖和配置,这是后续开发的基础。

3.1.1 引入依赖(pom.xml)

<!-- SpringBoot核心 -->

<
dependency>
    
<
groupId>org.springframework.boot
</
groupId>
    
<
artifactId>spring-boot-starter-web
</
artifactId>

</
dependency>


<!-- MySQL + MyBatis-Plus(简化CRUD) -->

<
dependency>
    
<
groupId>com.baomidou
</
groupId>
    
<
artifactId>mybatis-plus-boot-starter
</
artifactId>
    
<
version>3.5.3.1
</
version>

</
dependency>

<
dependency>
    
<
groupId>mysql
</
groupId>
    
<
artifactId>mysql-connector-java
</
artifactId>
    
<
version>8.0.33
</
version>

</
dependency>


<!-- Elasticsearch(SpringBoot Starter) -->

<
dependency>
    
<
groupId>org.springframework.boot
</
groupId>
    
<
artifactId>spring-boot-starter-data-elasticsearch
</
artifactId>

</
dependency>


<!-- Redis -->

<
dependency>
    
<
groupId>org.springframework.boot
</
groupId>
    
<
artifactId>spring-boot-starter-data-redis
</
artifactId>

</
dependency>


<!-- 工具类(JSON/日期等) -->

<
dependency>
    
<
groupId>com.alibaba
</
groupId>
    
<
artifactId>fastjson2
</
artifactId>
    
<
version>2.0.32
</
version>

</
dependency>

<
dependency>
    
<
groupId>org.projectlombok
</
groupId>
    
<
artifactId>lombok
</
artifactId>
    
<
optional>true
</
optional>

</
dependency>
3.1.2 核心配置(application.yml)

spring:
  
# 数据源配置(MySQL)
  
datasource:
    
url: 
jdbc:mysql://localhost:3306/college_timetable?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    
username: 
root
    
password: 
123456
    
driver-class-name: 
com.mysql.cj.jdbc.Driver

  
# Redis配置
  
redis:
    
host: 
localhost
    
port: 
6379
    
password:
    
database: 
0
    
timeout: 
5000ms
    
lettuce:
      
pool:
        
max-active: 
20  
# 最大连接数(选课高峰期调大)
        
max-idle: 
10
        
min-idle: 
5

  
# Elasticsearch配置
  
elasticsearch:
    
uris: 
http://localhost:9200
    
username: 
elastic
    
password: 
123456
    
rest:
      
connection-timeout: 
3000ms


# MyBatis-Plus配置

mybatis-plus:
  
mapper-locations: 
classpath:mapper/**/*.xml
  
type-aliases-package: 
com.college.timetable.entity
  
configuration:
    
map-underscore-to-camel-case: 
true  
# 下划线转驼峰


# 日志配置(方便调试ES和MySQL)

logging:
  
level:
    
com.college.timetable.mapper: 
debug  
# MySQL SQL日志
    
org.springframework.data.elasticsearch: 
debug  
# ES操作日志

八年经验技巧:

  • Redis 的 max-active要根据选课高峰期的并发量调整(比如 3 万学生,按 10% 并发查课表,设 20-30 足够);
  • ES 的 connection-timeout别设太短(3 秒足够),避免网络波动导致连接失败;
  • 开启 MyBatis-Plus 和 ES 的 debug 日志,排错时能快速定位问题(比如 SQL 写错、ES 查询语句不对)。

3.2 核心功能 1:三重冲突检测(教师 / 教室 / 时间)

这是排课系统的 “生命线”—— 一旦出现冲突,比如 “教师 A 周一 1-2 节同时在两个教室上课”,后果很严重。用 ES 做冲突检测,比 MySQL 多表联查快太多。

3.2.1 ES 索引设计(课表索引)

先在 ES 中创建 “课表索引”,字段要覆盖冲突检测的所有维度:


/**
 * ES课表索引实体(对应MySQL的timetable表)
 */

@Data

@Document(indexName = 
"college_timetable", shards = 3, replicas = 1)  
// 3分片1副本(根据数据量调整)

public 
class 
TimetableEsEntity {
    
@Id  
// ES文档ID(与MySQL的timetable_id一致)
    
private 
Long timetableId;

    
@Field(type = FieldType.Long, index = true)  
// 教师ID(索引,用于冲突检测)
    
private 
Long teacherId;

    
@Field(type = FieldType.Long, index = true)  
// 教室ID(索引,用于冲突检测)
    
private 
Long classroomId;

    
@Field(type = FieldType.Integer, index = true)  
// 周次(比如第1-16周,索引)
    
private Integer week;

    
@Field(type = FieldType.Integer, index = true)  
// 星期(1=周一,7=周日,索引)
    
private Integer weekday;

    
@Field(type = FieldType.Integer, index = true)  
// 节次(1=1-2节,2=3-4节,索引)
    
private Integer section;

    
@Field(type = FieldType.Long, index = true)  
// 课程ID(索引,用于查课表)
    
private 
Long courseId;

    
@Field(type = FieldType.Long, index = true)  
// 院系ID(索引,用于统计)
    
private 
Long deptId;

    
@Field(type = FieldType.Date, format = DateFormat.basic_date_time)  
// 创建时间(非索引)
    
private Date createTime;
}

索引设计技巧:

  • 分片数(shards):按院系分,比如 3 个分片对应 3 个院系,查询时指定分片,减少扫描范围;
  • 只有用于查询和过滤的字段才设 index = true(比如 createTime 不用查,设 index = false),减少索引体积;
  • 周次、星期、节次用 Integer 类型(比如 “周一 1-2 节” 存为 weekday=1,section=1),比字符串(“周一 1-2 节”)查询快。
3.2.2 冲突检测实现(用 ES bool 查询)

/**
 * 排课服务(核心:冲突检测)
 */

@Service

public 
class 
TimetableService {
    
@Autowired
    
private 
ElasticsearchRestTemplate esTemplate;  
// ES操作模板
    
@Autowired
    
private 
TimetableMapper timetableMapper;  
// MySQL Mapper
    
@Autowired
    
private 
RedisTemplate<
String, 
Object> redisTemplate;  
// Redis操作

    
// Redis缓存Key前缀(学生个人课表)
    
private 
static final 
String 
STUDENT_TIMETABLE_KEY = 
"timetable:student:%s";

    
/**
     * 排课前检测冲突
     * 
@param dto 排课DTO(含教师ID、教室ID、周次、星期、节次)
     * 
@return true=无冲突,false=有冲突
     */
    
public 
boolean 
checkConflict(
TimetableDTO dto) {
        
// 构建ES bool查询:同时匹配“教师+时间”或“教室+时间”→有匹配则冲突
        
NativeSearchQuery searchQuery = 
new 
NativeSearchQueryBuilder()
                .
withQuery(
QueryBuilders.
boolQuery()
                        
// 时间条件(周次、星期、节次必须完全匹配)
                        .
must(
QueryBuilders.
termQuery(
"week", dto.
getWeek()))
                        .
must(
QueryBuilders.
termQuery(
"weekday", dto.
getWeekday()))
                        .
must(
QueryBuilders.
termQuery(
"section", dto.
getSection()))
                        
// 冲突条件:要么教师冲突,要么教室冲突
                        .
should(
QueryBuilders.
termQuery(
"teacherId", dto.
getTeacherId()))
                        .
should(
QueryBuilders.
termQuery(
"classroomId", dto.
getClassroomId()))
                        .
minimumShouldMatch(
1)  
// 至少满足一个should条件
                )
                .
withSize(
1)  
// 只需要判断是否有数据,不用查全量
                .
build();

        
// 执行ES查询
        
SearchHits<
TimetableEsEntity> searchHits = esTemplate.
search(
                searchQuery, 
TimetableEsEntity.
class
        );

        
// 有命中结果→冲突;无命中→无冲突
        
return searchHits.
getTotalHits() == 
0;
    }

    
/**
     * 排课(冲突检测通过后执行)
     */
    
public 
void 
addTimetable(
TimetableDTO dto) {
        
// 1. 先检测冲突
        
boolean noConflict = 
checkConflict(dto);
        
if (!noConflict) {
            
throw 
new 
BusinessException(
"排课冲突!请检查教师、教室或时间");
        }

        
// 2. 保存到MySQL(用MyBatis-Plus)
        
TimetableEntity entity = 
new 
TimetableEntity();
        
BeanUtils.
copyProperties(dto, entity);
        entity.
setCreateTime(
new 
Date());
        timetableMapper.
insert(entity);

        
// 3. 同步数据到ES(供后续查询和统计)
        
TimetableEsEntity esEntity = 
new 
TimetableEsEntity();
        
BeanUtils.
copyProperties(entity, esEntity);
        esTemplate.
save(esEntity);

        
// 4. (可选)删除相关缓存(比如教师课表缓存,避免旧数据)
        
deleteRelatedCache(dto.
getTeacherId(), 
null);

        log.
info(
"排课成功!课表ID:{}", entity.
getTimetableId());
    }

    
/**
     * 删除相关缓存(调课/删课时用)
     */
    
private 
void 
deleteRelatedCache(
Long teacherId, Long studentId) {
        
if (teacherId != 
null) {
            
String teacherKey = 
"timetable:teacher:" + teacherId;
            redisTemplate.
delete(teacherKey);
        }
        
if (studentId != 
null) {
            
String studentKey = 
STUDENT_TIMETABLE_KEY + studentId;
            redisTemplate.
delete(studentKey);
        }
    }
}

八年经验技巧:

  • ES 查询时用 withSize(1):只要查到 1 条数据就说明冲突,不用查全量,效率提升 50%;
  • 冲突检测的 minimumShouldMatch(1):必须满足 “教师冲突” 或 “教室冲突” 中的一个,逻辑别写错;
  • 排课后同步 ES 时,用 esTemplate.save()而不是 update():确保 ES 数据与 MySQL 一致。

3.3 核心功能 2:高并发课表查询(学生 / 教师端)

选课高峰期,上千学生同时查课表,靠 Redis 缓存扛住压力,避免 MySQL 崩溃。

3.3.1 学生个人课表查询(Redis+ES)

/**
 * 课表查询服务
 */

@Service

public 
class 
TimetableQueryService {
    
@Autowired
    
private ElasticsearchRestTemplate esTemplate;
    
@Autowired
    
private RedisTemplate<String, Object> redisTemplate;
    
@Autowired
    
private StudentCourseMapper studentCourseMapper;  
// 学生-课程关联表(查学生选的课程)

    
// Redis缓存Key:学生个人课表(过期时间2小时,课表不会频繁变)
    
private 
static 
final 
String 
STUDENT_TIMETABLE_KEY 
= 
"timetable:student:%s";
    
private 
static 
final 
long 
CACHE_EXPIRE 
= 
2 * 
60 * 
60;  
// 2小时

    
/**
     * 学生查询个人课表
     * 
@param studentId 学生ID
     * 
@param week 当前周次
     * 
@return 个人课表列表
     */
    
public List<TimetableVO> 
getStudentTimetable
(Long studentId, Integer week) {
        
String 
cacheKey 
= STUDENT_TIMETABLE_KEY + studentId;

        
// 1. 先查Redis缓存
        
Object 
cacheObj 
= redisTemplate.opsForValue().get(cacheKey);
        
if (cacheObj != 
null) {
            log.info(
"学生{}查课表,命中Redis缓存", studentId);
            
return JSON.parseObject(cacheObj.toString(), 
new 
TypeReference<List<TimetableVO>>() {});
        }

        
// 2. 缓存未命中,查ES(先查学生选的课程ID,再查课表)
        
// 2.1 查学生选的课程ID(从MySQL查,数据量小)
        List<Long> courseIds = studentCourseMapper.selectCourseIdByStudentId(studentId);
        
if (CollectionUtils.isEmpty(courseIds)) {
            
return Collections.emptyList();
        }

        
// 2.2 查ES:匹配课程ID+周次
        
NativeSearchQuery 
searchQuery 
= 
new 
NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.boolQuery()
                        .must(QueryBuilders.termsQuery(
"courseId", courseIds))  
// 学生选的课程
                        .must(QueryBuilders.termQuery(
"week", week))  
// 当前周次
                )
                .withSort(SortBuilders.fieldSort(
"weekday").order(SortOrder.ASC))  
// 按星期升序
                .withSort(SortBuilders.fieldSort(
"section").order(SortOrder.ASC))   
// 按节次升序
                .build();

        SearchHits<TimetableEsEntity> searchHits = esTemplate.search(
                searchQuery, TimetableEsEntity.class
        );

        
// 3. 转换为VO(给前端返回)
        List<TimetableVO> result = searchHits.stream()
                .map(hit -> {
                    
TimetableEsEntity 
esEntity 
= hit.getContent();
                    
TimetableVO 
vo 
= 
new 
TimetableVO();
                    BeanUtils.copyProperties(esEntity, vo);
                    
// 补充课程名称、教师名称(从缓存或MySQL查,这里省略)
                    
return vo;
                })
                .collect(Collectors.toList());

        
// 4. 回填Redis缓存
        redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(result), CACHE_EXPIRE, TimeUnit.SECONDS);
        log.info(
"学生{}查课表,缓存未命中,查ES后回填缓存", studentId);

        
return result;
    }
}

八年经验技巧:

  • 缓存过期时间设 2 小时:课表不会频繁变,2 小时足够,太长会导致调课后缓存脏读;
  • 查 ES 前先从 MySQL 查 “学生选的课程 ID”:因为 “学生 - 课程” 关联数据量小(每个学生选 5-10 门课),MySQL 查更快,避免在 ES 中查大量数据;
  • 课表排序:按 “星期→节次” 升序,符合学生看课表的习惯(比如周一 1-2 节、周一 3-4 节……)。

3.4 核心功能 3:多维度统计分析(院系 / 教师)

领导经常需要 “统计计算机学院本学期各专业课程数”“统计教师 A 近 3 个月授课时长”,用 ES 的聚合功能比 MySQL 的 Group By 快 10 倍以上。

3.4.1 院系课程分布统计(ES 聚合)

/**
 * 统计分析服务
 */

@Service
public class TimetableStatService {
    
@Autowired
    private ElasticsearchRestTemplate esTemplate;

    
/**
     * 统计某院系本学期各专业的课程数量
     * @param deptId 院系ID
     * @param semester 学期(比如2024-2025-1)
     * @return 统计结果(专业ID→课程数)
     */
    public Map<Long, Integer> 
statCourseByMajor(Long deptId, String semester) {
        
// 1. 构建ES聚合查询:按“专业ID”分组,统计每组的课程数
        NativeSearchQuery searchQuery = new 
NativeSearchQueryBuilder()
                
.withQuery(QueryBuilders.boolQuery()
                        
.must(QueryBuilders.termQuery("deptId", deptId))  
// 指定院系
                        
.must(QueryBuilders.termQuery("semester", semester))  
// 指定学期
                )
                
// 聚合:按majorId分组(专业ID),统计count
                
.addAggregation(AggregationBuilders.terms("major_course_count")
                        
.field("majorId")  
// 按专业ID分组
                        
.size(
100)  
// 最多返回100个专业(足够)
                )
                
.withSize(
0)  
// 只需要聚合结果,不需要原始数据
                
.build();

        
// 2. 执行查询
        SearchHits<TimetableEsEntity> searchHits = esTemplate
.search(
                searchQuery, TimetableEsEntity.class
        );

        
// 3. 解析聚合结果
        Aggregations aggregations = searchHits
.getAggregations();
        Terms termsAgg = aggregations
.get("major_course_count");

        Map<Long, Integer> result = new HashMap<>();
        for (Terms.Bucket bucket : termsAgg.getBuckets()) {
            Long majorId = Long
.parseLong(bucket.getKeyAsString());  
// 专业ID
            long count = bucket
.getDocCount();  
// 课程数
            result
.put(majorId, (int) count);
        }

        return result;
    }
}

八年经验技巧:

  • 聚合查询时用 withSize(0):不需要返回原始课表数据,只需要聚合结果,减少数据传输量;
  • terms聚合的 size设足够大(比如 100):避免专业数量超过默认 size(10)导致统计不全;
  • 统计结果可以缓存到 Redis(比如设 1 小时过期):因为统计数据不会实时变,减少 ES 压力。

四、踩坑实录:这些坑我替你踩过了

做排课系统的八年里,踩过不少 “血坑”,分享 3 个最典型的,帮你少走弯路:

4.1 坑 1:ES 索引没分片,数据量大了查询慢

问题:早期把 ES 索引设为 1 个分片,当课表数据超过 10 万条后,冲突检测查询从 50ms 涨到 500ms,排课时明显卡顿。

原因:1 个分片只能用 1 个 ES 节点的 CPU,数据量大时处理不过来。

解决方案:

  1. 重建索引,设 3-5 个分片(根据数据量和节点数调整,比如 3 个节点设 3 个分片);
  2. 按 “院系 ID” 做路由分片:查询时指定分片,比如 “计算机学院(deptId=1)” 的数据只存在分片 1,减少扫描范围。

// 按院系ID路由分片(新增课表时指定)
IndexQuery indexQuery = new 
IndexQueryBuilder()
        
.withObject(esEntity)
        
.withRouting(String.valueOf(esEntity.getDeptId()))  
// 路由键=院系ID
        
.build();
esTemplate
.index(indexQuery);

4.2 坑 2:调课后 Redis 缓存没更新,学生看到旧课表

问题:管理员给学生 A 调课后,学生 A 查课表还是旧的,需要刷新页面才显示新的,投诉 “课表不对”。

原因:调课后只更新了 MySQL 和 ES,没删除 Redis 中的旧缓存,导致缓存脏读。

解决方案:

  1. 调课时,主动删除对应学生 / 教师的 Redis 缓存;
  2. 用 Redis 的发布订阅机制:调课服务发 “调课消息”,缓存服务收到消息后删除对应缓存。

// 调课时删除缓存(关键代码)
public void 
updateTimetable(TimetableDTO dto) {
    
// 1. 更新MySQL
    
// ...(省略更新逻辑)

    
// 2. 更新ES
    
// ...(省略更新逻辑)

    
// 3. 删除相关缓存(学生+教师)
    Long studentId = dto
.getStudentId();
    Long teacherId = dto
.getTeacherId();
    
deleteRelatedCache(teacherId, studentId);  
// 调用之前写的删除缓存方法
}

4.3 坑 3:纯 MySQL 做统计,期末时查询超时

问题:期末统计 “全校各院系课程总数”,用 MySQL 的 GROUP BY dept_id查询,数据量 50 万条时,耗时 12 秒,领导催得急,我只能重启数据库临时缓解。

原因:MySQL 的 Group By 需要扫描全表,数据量大时性能极差。

解决方案:

  1. 把统计功能迁移到 ES,用聚合查询替代 MySQL 的 Group By;
  2. 定时(比如每天凌晨)预计算统计结果,存到 Redis,领导查的时候直接取缓存,不用实时计算。

五、总结:排课系统的 “设计原则”

做了八年排课系统,总结出 3 个核心设计原则,适用于所有教育类系统:

  1. 数据分层,各司其职:MySQL 存基础数据(保证持久化),ES 做检索和统计(保证快),Redis 做缓存(保证高并发),不要让一个组件干所有活;
  2. 冲突前置,避免补救:排课前必须做冲突检测,不要等排完课再发现冲突(事后补救成本太高,比如要通知学生 / 教师调课);
  3. 高可用兜底:ES 挂了降级查 MySQL,Redis 挂了降级查 ES,避免单点故障导致系统崩溃(比如用 Spring Cloud CircuitBreaker 做熔断)。

最后给同行一个建议:做排课系统不要一开始就堆复杂组件 —— 如果学校规模小(学生 < 1 万),用 SpringBoot+MySQL 足够;等规模大了再加 ES 和 Redis,循序渐进,避免过度设计。

如果你的项目也在做排课系统,希望这篇文章能帮你少走弯路。有其他问题的话,欢迎在评论区交流 —— 教育类系统的坑,我们一起踩,一起填!

<"<image="zq.c5zb.cn"><image="sjb.c5zb.cn"><image="nba.c5zb.cn"><image="cba.c5zb.cn"><image="lq.c5zb.cn">">

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70048944/viewspace-3114764/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2026-01-05

  • 博文量
    74
  • 访问量
    81655