业务场景如下:
在单体应用高并发场景, 全校所有学生开始选课, 选课大家都是抢着选择热门的选修课的, 所以会产生高并发, 热门课程的席位是有限的, 比如某个热门课程仅限 100 名学生, 但是高并发场景下,执行以下代码, 经常超过100名学生抢到该热门课程
以下代码只是大概, 并非可完整执行, 只是用于解释问题;
//可重入锁,默认是非公平锁
private Lock lock = new ReentrantLock(true);
@Autowired
private CourseMapper courseMapper;
@Autowired
private CourseStudentMapper courseStudentMapper;
@Override
@Transactional
public Result updateCourseNum(String courseId, String studentId) {
try {
//这里加锁了, 但是总是会超过100个学生能抢到热门课程, 难道锁不起作用、lock是同一个对象
lock.lock();
//getCourseNumber的SQL语句大概: "SELECT number FROM course WHERE course_id = ? ";
Course course = courseMapper.getCourseNumber(String courseId);
Integer number = course.getNumber;
if( number > 0 ){
// updateCourseNumber的SQL语句大概 "UPDATE course SET number = number-1 WHERE course_id=?";
//课程席位数减1
courseMapper.updateCourseNumber(courseId);
//关联课程与学生的关系
courseStudentMapper.saveRelativeCourseStudent(courseId,studentId)
}else{
return Result.error("课程席位已满人");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放锁
lock.unlock();
}
return Result.ok("恭喜您,成功选到您心仪的课程");
}
问题分析:
service是单例的lock锁的是同一个对象, 没问题, 我的又不是分布式应用, 只是一个单体应用; 最终把问题锁定在事务上, 不加@Transactional没问题, 加了@Transactional就出现问题了
这里我简单说一下事务@Transactional的原理, 真的很简单, 就一张图
请看下入解释:
- 脏读
脏读发生在一个事务A读取了被另一个事务B修改,但是还未提交的数据。假如B回退,则事务A读取的是无效的数据。 - 原因就是:
当 number = 1 的时候 , 最后一个席位, 线程A 先进来抢到了席位, 线程A业务处理完成之后, 释放了锁, 但是 事务还未提交, 数据的 course表的number = 1 , 此时线程B在线程A释放锁之后, 立刻进入一顿操作猛如虎, 查询数据库course的number=1; 并且处理业务, 线程B也抢到的席位, 线程B提交事务, 此时线程A也提交了事务; 最终到时 number值为 -1 了 , 超出课程席位了