问题背景:
最近在写一个活动报名功能,会有多个人同时报名某个活动,要求当参与人数超过限制人数的时候,就报出人数已满的信息。
不考虑并发性,正常的逻辑如下:
ServiceImpl.java
@Override
public JSONObject signupActivity(Integer actId, String userId) {
// 前面的逻辑省略.......
Integer currentAttendCount = activity.getAttendCount(); // 从数据库中得到当前已经报名的人数
if (currentAttendCount >= attendLimit) { // 如果已经报名的人数超过限制人数
json.put(CommonConst.MESSAGE, "报名人数已满");
return json;
}
// 修改活动已参加的人数并更新数据库表中的这个字段
Integer attendCount = currentAttendCount + 1;
activity.setAttendCount(attendCount);
activityMapper.updateByPrimaryKeySelective(activity);
// 往报名表里添加一条用户信息......
json.put(CommonConst.MESSAGE, "活动报名成功");
return json;
}
但是,在高并发下,这段代码就会有问题。比如现在有 A, B, C 三个学生同时报名,他们从数据库中得到的 currentAttendCount
字段都是20(前面已经报名了20人),而限制报名人数 attendLimit
是 22 人,那么代码中的 if
条件都不会执行,这样问题就出现了。
刚开始,设置了一个活动的限制人数 attendLimit
为 480。在 Jmeter 中进行测试,每秒开 500 个线程(每秒线程数 TPS = 500),报名了 500 人(数据库中有 500条记录),但是由于上述原因, currentAttendCount
并不是 500,而且远远小于 500。这样本来到了 480 人就应该提示报名已满,但是现在并不会停止,还可以继续报名。
在网上查了一下解决办法,也试了试乐观锁、悲观锁这些,但是效果并不好。突然 get 到 Java 中的 synchronized 关键字。
问题解决:synchronized 关键字
因为 synchronized 关键字可以修饰代码块,所以第一次我就把函数里面会出现并发问题的代码包含在 synchronized 里,用法如下:
synchronized {
Integer currentAttendCount = activity.getAttendCount(); // 从数据库中得到当前已经报名的人数
//.......
return json;
}
重新测试,结果发现好了一些(currentAttendCount
虽然仍然不是 500,但已经很接近500了)。
上网一查,在代码块中加入 synchronized 还是不能完全解决高并发问题。原因是synchronized 代码块的执行是在事务之内执行的,可以推断在 synchronized 代码块执行完时,事务还未提交,其他线程进入 synchronized 的代码块后,读取的库存数据不是最新的。
因此,可以将 synchronized 关键字加入到控制层 Controller 层,使 synchronized 锁的范围大于事务控制的范围。
来到对应的控制层 Controller,找到调用上述函数的接口,在接口方法上加上 synchronized 关键字,问题解决,完美!代码如下所示:
Controller.java
@RequestMapping(value = "/signup", method = RequestMethod.POST)
@ResponseBody
public synchronized JSONObject signupActivity(@RequestBody HashMap<String, Object> reqData) {
return participationService.signupActivity(actId, userId); // 包含并发操作的上面那个函数
}
问题总结
有一篇博客给我们总结了几点,我觉得很好 spring(基础18) Sprin事务和synchronized锁的一些问题,以下是引用:
以上事务与锁之间存在的问题是:由于事务范围大于锁代码块范围,在锁代码块执行完成后,此时事务还未提交,导致此时进入锁代码块的其他线程,读到的仍是原有的库存数据。所以,要保证锁范围大于代码块范围才行。
关于程序加锁自己的一点见解:
- 建议程序中尽量不要加锁;
- 尽量在业务和代码层,解决线程安全的问题,实现无锁的线程安全;
- 如果以上两点都做不到,一定要加锁,尽量使用
java.util.concurrent
包下的锁(因为是非阻塞锁,基于CAS算法实现,具体可以查看AQS类的实现); - 如果以上三点仍然都做不到,一定要加阻塞锁:synchronized 锁,两个原则:
(1)尽量减小锁粒度;
(2)尽量减小锁的代码范围(在代码块中加锁就能解决问题的就不要在接口方法上加锁)。
网友评论