突然有一天,仓库的同事发消息说有两位员工领取了同一个发货单。拣货的时候报错该发货单已拣货。这可就奇了怪了,为了防止并发,且这个仓库是单节点部署的,记得是加了锁的。
@GetMapping(path = "test") @ResponseBody public void test(Pageable request){ for (int i = 0; i < 100; i++) { //堆代码 duidaima.com //新建线程处理 new Thread(() -> { userInfoService.testDemo(); }).start(); } }肯定是并发导致的,这里模拟一下高并发的情况
@Transactional(rollbackOn = Exception.class) public synchronized void testDemo() { UserInfo byUserId = userRepository.findByUserId(1); byUserId.setAge(byUserId.getAge() + 1); userRepository.save(byUserId); }可以看到业务逻辑里有个锁,并且有事务。数据大概长这样,我拿之前我写的demo的表来处理,修改这个age 100次。
@GetMapping(path = "test") @ResponseBody public void test(Pageable request) { for (int i = 0; i < 100; i++) { //新建线程处理 new Thread(() -> { synchronized (UserController.class) { userInfoService.testDemo(); } }).start(); } }将锁放到整个事务的外层,这样事务提交之后才会释放锁。
@Transactional(rollbackOn = Exception.class) public void testDemo() { UserInfo byUserId = userRepository.findByUserId(1); log.info("当前线程:{},当前年龄:{}",Thread.currentThread().getName(),byUserId.getAge()); byUserId.setAge(byUserId.getAge() + 1); userRepository.save(byUserId); log.info("当前线程:{},当前年龄:{}",Thread.currentThread().getName(),byUserId.getAge()); }看log,也是没有问题的。
欸欸欸,好了。搞定,提代码,打包,发包,一气呵成。解决bug就是这么迅速。
过了几天,仓库又反馈了,这bug又发生了,啊啊啊?奇了怪了。还没锁住?在研究了好久,觉得这锁没问题啊,是哪里出了问题呢?各种权衡之下,先加了个分布式锁,以求解决问题。然而不出所料,过了两天又又又发生了。又发生了,那就不是锁的问题了,那是什么的问题呢?这段业务很简单,拿到发货单,然后修改状态,然后存进去。就这,也没啥bug可以发生啊。
再从数据看,发现一个奇怪的地方,所有重复领取的发货单,都是在整点领取,然后发生bug的。这绝不是偶然,我在整点干啥了呢?想起来了,我前一阵加了个功能,使用定时任务批量请求货代的打印面单接口然后将面单的url存到了发货单表里,这样,发货的时候就不用一个个请求这个接口了。这个任务就是整点跑的。。。。。
然后员工获取发货单数据,这时候因为修改url的线程需要调用api会稍微慢点,员工会把发货单修改为待拣货,然后保存入库,修改url的线程执行完了,由于我是执行的jpa的save方法,他会把自己读取到「新建状态」的数据,修改个url再保存回表,这时候,表里的数据又变成新建状态了。这样之后的员工又能取到这条发货单数据,然后这样不就重了。
找到原因了解决就很简单了,修改url的时候只修改这一个字段,其他的不修改就好了。至此,bug解决掉了,修改数据库数据的时候save方法还是少用啊。一波三折啊,之前那种写法也是有问题的,幸亏一起改掉了,不然之后肯定也会继续发生。
简单点儿说就是定时任务查询的时候查出来的对象是新建状态,然后员工改成了待拣货状态,之后定时任务更新整个对象,因为定时任务取出来的对象还是新建状态,所以更新的时候,又把待拣货状态改成新建状态了。