背景:最近看自己以前写的博客。有一个有关秒杀的博客:秒杀系统设计思路。觉得不够细节,或者曾经有些理想化,不够落地、比较稚嫩。其实实际业务,一定要注重:简单、实用。能不用某个中间件尽量不用,能不做的多余设计尽量不做。
真正设计一个类似的业务后端核心会如何设计。
例子:简单点,以学校的选修课选课系统为例。
工具:使用redis缩短一组原子操作的时长。使用mq异步解决任务多防止数据丢失。
业务场景:每次开学初,整个学校的学生,会在同一时间,抢选修课。
表设计:
1.课程目录表:course
字段:课程id、学生总数、已有学生数、老师id、老师名 ...
2.课程学生关联表:course_student
字段: 课程id(course_id)、teacher_id、学生id(student_Id)、...
需要的操作
1.查询:
查询课程信息:课程名、老师名、学生总数、已有学生数
select * from course
学生查询已选课程:
select * from course_student where student_id = ?
教师查询课程学生:
select * from course_student where teacher_id = ? and course_id= ?
2.更新:
抢课:
加锁:锁住courseId、再锁住studentId
查询课程剩余人数:
select * from course where course_id = ?
if num > 0
则:insert into course_student;
update course set choose_num = choose_num + 1 where course_id = ?
释放锁。
问题
主要等待时间在:锁courseId之后更新数据,这个系列操作上。
假设:这一组操作,时长,100ms,则100个学生同时抢1个course,则总时长:100 * 100 = 10000ms = 10s。
可优化的地方
锁courseId,是肯定要锁的,计划从缩短更新数据操作的时长,从100ms,优化到10ms,则 总时长:100 * 10 = 1000ms。
方案:使用redis。
课程剩余人数第一次查询db后存入redis。
1、加锁:分布式锁:锁住courseId、再锁住studentId
2、key courseseId:leave_num
3、if leave_num is null
select * from course where course_id = ?
set courseId:leave_num ?
else
return leave_num
4、insert into course_student。(操作基本不会失败。可以异步操作)
5、更新redis剩余人数:set courseId:leave_num = leave_num - 1
6、更新db剩余人数:update course set choose_num = choose_num + 1 where course_id = ?(异步操作).
7、释放锁。
异步操作假设用线程池做,则:1.万一服务中途挂了,则数据丢失就不一致了。2.线程池会排队很多任务。
所以可以使用mq解决这个问题。即使服务挂了,也不丢数据。排队很多任务也不怕。