教培SaaS教务系统架构设计:从单体到微服务的演进实践

发布时间:2026/7/31 13:41:50
教培SaaS教务系统架构设计:从单体到微服务的演进实践 教培行业的数字化需求这几年变化很快。2023年我们刚起步时机构的核心诉求还比较简单——排课、考勤、课消统计一个单体应用就能跑起来。但随着业务规模增长问题开始集中爆发招生旺季流量峰值翻5倍单体服务扛不住排课模块要迭代算法但不敢动怕影响财务模块多租户数据量上来后单库查询性能急剧下降。本文分享我们从单体架构演进到微服务架构的实践过程重点聊架构决策背后的思考不照搬理论框架。单体阶段快速验证业务初期系统结构很简单[前端SPA] → [Spring Boot单体] → [MySQL单库] ↓ [Redis缓存]单体应用里分了几个模块用户管理、课程排课、考勤签到、课消财务、家校消息。模块间通过本地方法调用共享同一个数据库。这个阶段的好处是开发快、部署简单两个人两周就能上线一个功能模块。对于验证业务模式来说单体是合理的起步选择。但问题在半年后开始显现发布耦合排课算法要更新必须整个应用重新部署财务模块的线上bug修复也被迫跟着排期。性能瓶颈排课是CPU密集型操作旺季批量排课时会占满线程池导致考勤接口响应超时。扩展困难多租户数据都在一个库某个大租户的慢查询会影响所有人。到这个阶段继续在单体上打补丁已经不现实了必须拆。微服务拆分按业务域切分拆分策略我们没有一上来就按技术层拆比如把前后端分开而是按业务域来切。这个决策的依据是康威定律——系统架构要和组织结构匹配。我们的团队就是按业务线分工的按业务域拆服务团队和服务边界对齐迭代效率最高。拆分后的核心服务[API网关] | ---------------------------- | | | [招生CRM服务] [排课考勤服务] [课消财务服务] | | | [学员库] [课程库] [账务库] | | | -------------------------- | | [消息服务] [权限服务] | | [消息库] [权限库]每个服务独立部署、独立数据库通过API网关统一入口。服务间通信以同步HTTP为主异步场景走消息队列。关键设计决策1. 排课服务独立拆出资源隔离排课是整个系统中计算最重的模块尤其是批量排课和冲突检测。拆成独立服务后可以单独配置更高规格的实例即使排课任务把CPU打满也不会影响考勤和财务接口的可用性。// 排课冲突检测核心逻辑简化版publicclassScheduleConflictDetector{publicConflictResultdetect(ScheduleRequestrequest){// 三维冲突检测教师时间、教室占用、学生时段booleanteacherConflictteacherScheduleRepository.hasOverlap(request.getTeacherId(),request.getStartTime(),request.getEndTime());booleanclassroomConflictclassroomOccupancyRepository.hasOverlap(request.getClassroomId(),request.getStartTime(),request.getEndTime());booleanstudentConflictstudentEnrollmentRepository.hasOverlap(request.getStudentIds(),request.getStartTime(),request.getEndTime());if(teacherConflict)returnConflictResult.teacher(request);if(classroomConflict)returnConflictResult.classroom(request);if(studentConflict)returnConflictResult.student(request);returnConflictResult.ok();}}2. 课消与财务合并为一个服务有些团队会把课消和财务拆开但我们最终决定合并。原因是课消数据和财务数据强一致要求高拆开后跨服务事务太复杂。合并后课消产生的记录直接在同一个事务里写入财务流水数据一致性有保障。3. 消息服务异步化用爱耕云家校通知、排课提醒这些消息推送。用RabbitMQ做消息队列排课服务产生事件后丢到队列消息服务消费后处理推送。这样即使推送通道抖动也不会阻塞主流程。[排课完成] → 发布事件 → [RabbitMQ] → [消息服务消费] → [微信推送]多租户数据隔离多租户隔离我们采用的是共享数据库独立Schema方案。每个租户使用独立的数据库Schema应用层通过租户上下文路由数据源。publicclassTenantDataSourceRouterextendsAbstractRoutingDataSource{OverrideprotectedObjectdetermineCurrentLookupKey(){returnTenantContext.getCurrentTenantId();}}// 请求拦截器设置租户上下文ComponentpublicclassTenantInterceptorimplementsHandlerInterceptor{OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){StringtenantIdrequest.getHeader(X-Tenant-Id);TenantContext.setCurrentTenant(tenantId);returntrue;}OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){TenantContext.clear();}}这个方案在租户数量可控几百级别时性能和隔离性平衡得比较好。如果租户规模上千会考虑迁移到独立数据库方案。踩过的坑坑一拆得太细。中间有一版把权限服务拆得过细导致一次用户登录要调三个服务做权限校验延迟增加了200ms。后来把权限相关逻辑收敛回一个服务链路调用减少延迟降下来了。拆分粒度要看实际调用链路不是越细越好。坑二分布式事务处理。课消服务写入成功后要通知财务服务中间网络抖动导致数据不一致的情况出现过两次。最终引入了本地消息表方案——课消服务在本地事务里写一条消息记录定时任务轮询发送保证最终一致性。-- 本地消息表CREATETABLElocal_message(idBIGINTPRIMARYKEYAUTO_INCREMENT,business_keyVARCHAR(64)NOTNULL,message_bodyTEXTNOTNULL,statusTINYINTDEFAULT0,-- 0:待发送 1:已发送 2:失败retry_countINTDEFAULT0,create_timeDATETIMEDEFAULTCURRENT_TIMESTAMP,INDEXidx_status(status));坑三监控缺失。微服务拆完后初期没有完善链路追踪出问题排查全靠看日志效率很低。后来接入了SkyWalking做全链路追踪配合PrometheusGrafana做指标监控问题定位时间从小时级降到分钟级。架构现状与后续规划当前架构支撑了数千家租户的日常使用核心接口P99响应在200ms以内旺季排课峰值QPS能扛住日常的5倍。整体可用性稳定在99.9%以上。后续规划的方向排课服务引入弹性伸缩基于CPU利用率自动扩缩容应对招生旺季的突发流量。数据层读写分离部分报表查询走只读库减轻主库压力。服务网格探索随着服务数量增加服务间治理复杂度上升考虑引入Istio做流量管理。总结从单体到微服务的演进核心不是技术选型而是想清楚每个阶段的业务痛点是什么架构为业务服务。单体没有错微服务也不是银弹。拆分的关键在于找到合适的边界——按业务域切、按团队结构对齐、按性能瓶颈隔离。如果觉得有帮助欢迎收藏备用后续会继续分享教培SaaS技术实践中的其他主题。