踩过的坑-多线程并发情况下@Transactional注解和锁的使用注意事项

这是我目前来说使用@Transactional和锁踩过的最隐蔽的坑...我感觉我真的很有必要分享出来。

前景提示

作者的项目中有一个测点推送接口,他接收一个测点类数组的json数据作为参数然后把这个数组里的内容放到队列中,然后另一个方法会开一个线程池通过多线程操作来吧队列里的内容取出来做相应处理然后插入到数据表中持久化。每个线程调的方法大概如下图所示:

相当于每个线程其实只处理一个测点类。这时候我写完上面的代码后感觉不妥,因为设备和测点关联的,所以设备入库和测点入库应该要么都入库成功要么都入库失败,所以应该控制在一个事务里面,于是,我给上面的addNode方法加了个注解,方法就变成了下面这样:

加完注解于是开始调试发现一个很严重的bug:出现了设备重复的数据!!!为什么?

探究重复原因

 为什么会有重复数据?

        首先,因为如果推送一个设备上的10个测点信息过来,那么每个测点类里面的设备信息那一刻都是相同的,所以就有可能会有重复插入的隐患。

        然后,现在设备表里面有很多条一模一样的设备数据,按道理来说应该不会出现这个问题啊,我插入之前不是查询了吗?然后我一想,不对,我现在是多线程并发情况,所以我得给设备入库方法加锁,使得查询和插入变成一个原子操作。于是,我再insertDevice方法上加上了synchronized修饰,如下图所示:

我心想这样总能解决了吧?于是我再次调试发现,仍然会有重复数据!!!我仔细研究实在没找到问题啊,因为也加锁了,查库和入库操作成为了一个原子操作,怎么还会重复插入呢?

@Transactional注解的事务开启和结束

@Transactional注解的事务开启和结束是在哪一个节点开启哪一个节点结束你们知道吗?其实如果弄清楚了这个问题那么也就弄清楚了为什么我加锁之后仍然会出现设备重复插入的问题。

首先说结论,当你给你的方法加上@Transactional注解后,事务的开启节点就在你这个方法执行到第一个操作 InnoDB 表的语句,事务才真正启动。而事务的提交节点在你这个方法结束后事务才会提交。所以,看到这其实大家都懂为什么会出现设备重复插入的问题了吧?如下图所示:

正是因为我加事务的地方在addNode方法上,所以我需要等addNode方法整个结束事务才会提交。也就是说,一个线程进入insertDevice方法查询数据库发现为0,于是插入,然后insertDevice方法结束,释放锁,但是这时候事务还没有提交,但是锁已经释放了,于是第二个线程也进入insertDevice方法。查询数据库发现结果还是0,因为我的项目的数据库的隔离级别是读已提交,因此第二个线程是是看不到第一个线程的插入结果的,所以第二个线程仍然会插入,这时候,设备重复插入的问题就出现了。因此,导致我出现设备重复插入问题的原因就是因为我事物的提交在锁的释放之后。

解决办法

知道出现问题的原因后就很好解决了,我想了大概这几种方法可以解决:

第一:去掉transactional注解,在入库方法里通过编程式事务,我用的是TransactionTemplate来开启和提交事务,同时在入库方法外加锁,保证事务的开启在上锁之前,事务的提交在释放锁之前,这样减小事务的粒度,消除长事务后就解决了重复的问题。如下图所示:

第二:通过在设备表里面添加同步id列为唯一约束,因为该列值可以作为全局唯一标识符,然后入库方法里面在捕获违反唯一约束错误的异常记录到日志中。

第三:修改数据库隔离级别为读未提交,但是该方法只是解决了该问题但是会产生很多其他问题比如数据的脏读幻读不可重复读等问题,不建议使用。

第四:使用select...where... for update,并且走非主键不走索引,这时候会产生表锁,设备入库方法中的先查换成select for update后会对设备表上锁,直到事务提交或回滚后才会释放锁,不推荐,锁表性能很低。

以上的解决办法推荐第一和第二种,我项目中是两种方法都使用了,双保险。第三种第四种只是理论上的解决办法,而且只是解决该问题的方法,会产生很多其他问题,因此当个乐子看看就好了。

为什么@Transactional注解的事务提交是方法结束之后?

        本来想自己讲,结果无意中刷到了这篇博客,发现这个文章的作者讲的太好了,所以就不献丑了,大家可以看看这个作者讲的,结合了源码条理很清晰:当Transactional碰到锁,有个大坑,要小心。 - why技术 - 博客园 (cnblogs.com)

总之,这个坑不仅坑,还很隐蔽,一不小心就会踩,踩完后还很难找...我当时找了快两天才找到原因...希望大家看完这篇文章可以注意这个问题喔。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值