如何解决数据库并发下课程注册超员的业务规则违规问题?
兄弟,你遇到的这个是典型的并发竞态条件问题——多个请求同时读取到相同的课程人数,都通过校验后插入记录,最终突破人数上限。你之前看到的timestamp/row version主要用于解决更新冲突的乐观锁,但这个场景得针对性调整方案,我给你几个实用的解决思路,按可靠性排序:
1. 数据库层面的原子操作(最推荐)
数据库本身的事务和锁机制是解决这类问题最可靠的手段,核心是把「查询人数+校验+插入/更新」变成一个原子性的操作,避免中间被其他请求打断。
方案1.1 带条件的INSERT ... SELECT
直接在插入注册记录的SQL里关联课程表的条件判断,这样整个操作由数据库保证原子性。举个MySQL的示例:
INSERT INTO course_registrations (course_id, student_id, created_at) SELECT #{courseId}, #{studentId}, NOW() FROM courses WHERE course_id = #{courseId} AND current_students < max_students LIMIT 1;
执行完这条SQL后,检查受影响行数:
- 如果是1,说明注册成功;
- 如果是0,说明课程已经满员,直接返回用户“名额已满”的提示。
方案1.2 先更新人数再插入记录
另一种思路是先尝试更新课程的当前学生数,只有当人数未达上限时才更新成功,再插入注册记录:
UPDATE courses SET current_students = current_students + 1 WHERE course_id = #{courseId} AND current_students < max_students;
同样检查受影响行数:
- 若受影响行数为1,说明更新成功,接着执行插入注册记录的操作;
- 若为0,直接返回满员提示。
这种方式的优势是把人数更新变成原子操作,彻底避免竞态,而且逻辑简单易懂。
2. 应用层面加分布式锁(多实例部署场景)
如果你的Web应用是多台服务器部署的,除了数据库层面的约束,还可以配合分布式锁来控制同一时间只有一个请求能处理某门课程的注册逻辑。比如用Redis实现:
# 伪代码示例(Python + Redis) lock_key = f"course_reg_lock:{course_id}" # 加锁,设置10秒过期时间避免死锁 if redis_client.set(lock_key, "locked", ex=10, nx=True): try: # 这里执行查询课程、校验人数、插入记录的逻辑 course = Course.query.get(course_id) if course.current_students < course.max_students: reg = CourseRegistration(course_id=course_id, student_id=student_id) db.session.add(reg) course.current_students += 1 db.session.commit() return {"status": "success", "msg": "注册成功"} else: return {"status": "fail", "msg": "课程已满"} finally: # 无论成功失败都释放锁 redis_client.delete(lock_key) else: return {"status": "fail", "msg": "当前注册人数较多,请稍后重试"}
注意锁的过期时间要设置合理,既要覆盖注册逻辑的执行时间,又不能太长导致其他请求等待过久;同时一定要在finally块里释放锁,避免异常情况下锁无法释放。
3. 乐观锁适配场景(结合版本号)
你之前了解的timestamp/row version也能解决这个问题,只是需要调整逻辑。给课程表加一个version字段,每次更新时带上版本号:
UPDATE courses SET current_students = current_students + 1, version = version + 1 WHERE course_id = #{courseId} AND current_students < max_students AND version = #{currentVersion};
应用层的逻辑步骤:
- 查询课程的
current_students、max_students和version; - 先校验
current_students < max_students; - 执行上面的UPDATE语句,检查受影响行数;
- 如果受影响行数为1,说明更新成功,接着插入注册记录;
- 如果受影响行数为0,说明在你查询之后有其他请求修改了课程数据,这时候可以重试2-3次,或者直接返回满员提示。
这种方式适合并发不是特别高的场景,好处是不用加排他锁,性能相对较好,但需要处理重试逻辑。
内容的提问来源于stack exchange,提问作者san1127




