作者:尤东威
⼀、背景
在介绍背景前先理解⼏个名词概念
课程权益:指的是⽤户拥有哪些课程(e.g. 语⽂体验课L1级别48周)
融合项⽬:英语、语⽂、思维三科各⾃维护的项⽬重构的新项⽬
预排课:推算的课表信息
延期调级:修改⽤户的开课时间和课程级别
去年3⽉份,融合英语、融合语⽂、思维三科进⾏业务融合,参与了融合项⽬从零到⼀的过程。业务融合的同时,系统架构也要进⾏融合升级。从三科各⾃维护⾃⼰的系统到融合,涉
及到技术栈的统⼀、资源的融合、数据的迁移等等。
我主要负责课中排课中⼼的架构涉及和开发⼯作,下⾯就详细说下这块的设计。
⼆、整体架构
下⾯是排课中⼼⽐较详细的架构图
从上⾯的架构图可以看到排课的主要功能
- 权益(管理订单、⽩名单、活动等来源的课程权益)
- 排课(课表的⽣产和输出)
- 课程的延期、调级、退费
- B端的⼀些查询和通知机制
2.1 业务的时序性设计
从上⾯的架构的图中可以看到系统⽤到了kafka,kafka在这⾥主要是⽤来做异步解耦的,包括:
- 订单消息的消费
- 通知其他模块课表的变更
- ⾯向辅导侧的业务通知(结课、课程升级)
从业务⻆度看排课中⼼扮演的⻆⾊是上下游业务的中转站:
⼀个⽤户完整的购课流程是,⾸先从排课中⼼获取⽤户当前的权益信息(⽐如在读还是新购⽤户)来决定售卖什么样的商品;⽤户下单⽀付完成后,订单通知排课中⼼,排课中⼼消费到消息后进⾏权益的⽣成;排课中⼼⽣产权益后会转发消息到另外⼀个队列,下游服务会根据⽤户的课表信息(如结课时间、权益等)进⾏物流发货、活动命中等处理。
可以看到整个流程下游⼀些业务是依赖于⽤户的课表的,所以不能直接接收订单的消息,要先等排课中⼼处理完成之后再进⾏消费。经过⼀层消息转发就解决可业务的时序问题。
2.2 排课的设计和⽅案选择
在旧的业务系统设计中,融合英语是下单完成后直接⽣成全部课表,语⽂思维是每周定期去排课。两种⽅式的优缺点都和明显
⼀次全排优点:
- 课表所⻅即所得,⽤户未来进度⼀⽬了然
- 对⼀些圈定⽤户的业务查询⽅便
- 不依赖脚本
缺点:
- 校历变更(即新增了停课周)需要重排
- 课包变更(⽤户未来上的课包发⽣了变化)需要重排
定期排课和⼀次全排刚好相反。业务融合之后,校历和课包的会经常变更,如果是⼀次性全排可能⼀次变更就要更新⽤户所有的课表数据,本身课表数据量较⼤、增⻓也快,不适合做
频繁的变更,更适合使⽤定期排课。
预排课,上⾯说到定期排课的缺点就是没办法直接读取数据库⾥⽤户完整的课表,但是我们知道⽤户买了多少课、什么时候开课、怎么调整了。有了这些我们就能推送出⼀个在校历和课包不变的前提下的⼀个准确性的课表,这⾥称他为预排课。
2.3 数据架构
C端主要是⾯向的群体是购课⽤户,场景是课表和权益的输出,通常是⽐较简单的数据查询,⼀般是按⽤户维度来的;
B端主要的⽤户群体是辅导⽼师和运营⽼师,还有B端也会进⾏全量⽤户扫描的⼀些业务逻辑,包括主动缓存、结课通知、圈定⽤户等;通常是有⼀些复杂的查询逻辑;
要完全做到互不影响就要做资源的隔离:
- B端和C端分别部署在不同的服务器(⽬前已经在逐步上云,天然的隔离)。
- 数据层⾯的隔离(C端⽤DRDS,B端⽤ADB,通过DTS同步数据)。
数据选型
DRDS是阿⾥云的分布式数据库,适合做分库分表,C端按uid hash进⾏分库分表,降低单表的数据量
ADB是列式存储,适合⼤量数据复杂sql的查询
2.4 数据一致性
上⾯说到C端通常是⼀些单个⽤户维度的业务场景,但是有时也可能涉及到全表扫描的业务场景。⽐如⽤户插班报的场景:
⽤户A上完了语⽂系统课L1级别24周课程(共48周),过了很久没续费后再次续费(这时候⽤户已经没有班级了),需要给⽤户安排⼀个符合进度的班级(理论上肯定存在)。这个时候就需要查找是否与之匹配进度的⼀批学⽣,如果有就找最近的⼀期给学⽣排课。
这个场景就需要扫描课表了,显然不可能直接在C端库这么⼲。所以先在B端把第2~48周(因为第⼀周不算插班报)进度的开课时间直接缓存起来,C端直接读取缓存即可。
2.5 数据⼀致性怎么保证的
从上⾯的业务时序图可以看到排课中⼼作为上下游的中转站,在数据流转的过程中肯定会存在数据⼀致性的问题。针对这种问题我们与上下游共建了数据核验平台来保证数据的最终⼀致性。
如上图所示:上游(订单会定时检测10分钟内的订单)⾸先会进⾏⾃我⽀付核验,订单模块核验完成之后依据订单信息触发排课中⼼权益核验、及其他下游核验。如果排课中⼼核验失败(可能是消费订单消息时发送了不可⾃动恢复的错误),订单向下游会尝试重新下发消息;只要下游各个模块做好幂等性,那么就可以重新按照上⾯的业务时序重新处理。
三、未来的⼀些优化
类标签系统的设计来解决圈⽤户问题
上⾯的预排课机制解决了⽤户未来课表的展示,但是对于圈定⽤户的需求还是难以满⾜;⽐如需要给⼀批⽤户推送⼀场直播, ⽤户的圈定范围是:学科-英语、2021-10-01剩余课时为1周的⽤户 ;⼀般来说我们设计数据表的时候都着重于拆,但是拆分的后果就是查询复杂,上⾯说到使⽤ADB进⾏复杂查询,但是在数据量过⼤的情况下使⽤join也⽆法解决,这⾥有⼀个设想就是把拆分的属性进⾏聚合。
解决⽅案:
我们把⽤户的学科、进度、剩余课时等条件作为⽤户的属性给他打上标签。假设存储选择MongoDB,可以设计以下⽂档:
{ "user_id":1, "subject_list":[ { "subject_type":2,//英语 "surplus_duration":1//剩余1周 }, { "subject_type":1,//语⽂ "surplus_duration":2//剩余2周 } ] }
查询条件则为
{ "subject_list": { $elemMatch: { "subject_type": 2, "surplus_duration ": 1} } }
- 怎么触发更新
对于单个⽤户:利⽤下单、退课、调级、延期等课程权益变更时触发的队列消息进⾏实时变更
对于校历和课包变更:变更时也采⽤通知机制,异步执⾏⽤户标签的变更
综上所述:定期排课保证了⽤户课表的准确性,避免C端数据⼤量变更,仅在每周⼀定期更新当周课程。⽤户标签数据量⼩,且数据变更不影响c端上课⽤户
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。