Code Bye

一个死锁的问题

先描述下业务场景.
系统会生成单据号,单据号需要按一定规则连续递增. 方法A实现了这个规则.(最新单据号会存在数据库,A方法需要更新数据库)
系统多个模块都需要用到方法A, 所以在方法A用了synchronized 修饰.
各模块运行正常. 现在需求发生变更. 需要支持单据的批处理.
就出现以下场景.
当线程T1执行方法B进行批处理时,循环10次调用方法A, 
线程T2执行方法C进行单条处理.
T1线程的B方法执行5次后,T2线程的C方法获取到了锁,开始执行,需要更新数据库,但是线程B方法事务还没提交,还占用了数据库的锁,方法C就开始等待. 这时方法B因为无法获得到A的锁,剩余次数无法执行,
程序陷入死锁.

求怎么解决?

目前的解决方案有 锁粗化.即将锁加在方法B,C上. 但是类似方法B,C这种,在几十个类中,改动非常大.
有没有简单点的方法呢?

做好线程同步控制,应该可以解决楼主的问题。
楼主可以考虑下用aop控制下,在这些方法上加锁或者类似锁的机制。
或者在获得A的锁后验证数据库条件,得不到锁的话就进入等待状态,释放先前得到的锁。
修改事务传播特性, 方法a在任何情况下都应该重新起一个事务, 而不应该加入已有的事务, 因为方法a本身是一个专门处理单据号的独立的业务逻辑,  这样程序锁和数据库锁的粒度保持一致, 就不会有你现在的问题
引用 3 楼 dgqjava 的回复:

修改事务传播特性, 方法a在任何情况下都应该重新起一个事务, 而不应该加入已有的事务, 因为方法a本身是一个专门处理单据号的独立的业务逻辑,  这样程序锁和数据库锁的粒度保持一致, 就不会有你现在的问题

我觉得是正解。方法a是一个单独的业务逻辑。

引用 3 楼 dgqjava 的回复:

修改事务传播特性, 方法a在任何情况下都应该重新起一个事务, 而不应该加入已有的事务, 因为方法a本身是一个专门处理单据号的独立的业务逻辑,  这样程序锁和数据库锁的粒度保持一致, 就不会有你现在的问题

方法a是一个生成单号的方法, 单号需要连续.
如果事务单独控制, 那么方法b如果出现回滚(b的业务回滚), 方法a也要回滚. 且c方法可能已经在a回滚前的基础上进行了操作(因为a是单独事务),所以方法b回滚后, a方法还不能回滚,只能将b中用掉的单号单独保存,建立废号再利用机制.

这个改动貌似会更大.

引用 5 楼 qq315737546 的回复:
Quote: 引用 3 楼 dgqjava 的回复:

修改事务传播特性, 方法a在任何情况下都应该重新起一个事务, 而不应该加入已有的事务, 因为方法a本身是一个专门处理单据号的独立的业务逻辑,  这样程序锁和数据库锁的粒度保持一致, 就不会有你现在的问题

方法a是一个生成单号的方法, 单号需要连续.
如果事务单独控制, 那么方法b如果出现回滚(b的业务回滚), 方法a也要回滚. 且c方法可能已经在a回滚前的基础上进行了操作(因为a是单独事务),所以方法b回滚后, a方法还不能回滚,只能将b中用掉的单号单独保存,建立废号再利用机制.

这个改动貌似会更大.

你的意思是你十个操作只要有一个失败十个都要回滚, 如果是这样的话那么你的问题不是死锁的问题, 就算程序中不出现死锁, 线程1执行5次a方法, 线程2执行1次a方法, 线程1再执行5次a方法并且失败了, 那么就算不存在死锁仍然会存在你现在说的无法回滚的问题, 而你所说的解决方案在b和c方法上加锁也是不现实的, 这样相当于将事务序列化执行, 并发性能是无法容忍的, 所以你的这个问题本身从设计上就是不对的, 一个批量处理, 相当于多次单条的处理, 每次成功就提交事务, 就算有失败的也不是全部回滚, 而应该最后提示: 成功xxx条, 失败xxx条

引用 6 楼 dgqjava 的回复:
Quote: 引用 5 楼 qq315737546 的回复:
Quote: 引用 3 楼 dgqjava 的回复:

修改事务传播特性, 方法a在任何情况下都应该重新起一个事务, 而不应该加入已有的事务, 因为方法a本身是一个专门处理单据号的独立的业务逻辑,  这样程序锁和数据库锁的粒度保持一致, 就不会有你现在的问题

方法a是一个生成单号的方法, 单号需要连续.
如果事务单独控制, 那么方法b如果出现回滚(b的业务回滚), 方法a也要回滚. 且c方法可能已经在a回滚前的基础上进行了操作(因为a是单独事务),所以方法b回滚后, a方法还不能回滚,只能将b中用掉的单号单独保存,建立废号再利用机制.

这个改动貌似会更大.

你的意思是你十个操作只要有一个失败十个都要回滚, 如果是这样的话那么你的问题不是死锁的问题, 就算程序中不出现死锁, 线程1执行5次a方法, 线程2执行1次a方法, 线程1再执行5次a方法并且失败了, 那么就算不存在死锁仍然会存在你现在说的无法回滚的问题, 而你所说的解决方案在b和c方法上加锁也是不现实的, 这样相当于将事务序列化执行, 并发性能是无法容忍的, 所以你的这个问题本身从设计上就是不对的, 一个批量处理, 相当于多次单条的处理, 每次成功就提交事务, 就算有失败的也不是全部回滚, 而应该最后提示: 成功xxx条, 失败xxx条

是的.目前的解决办法是将批处理中的每条作为单独事务了. 

但是引发了另外一个问题. 在某个方法d中, 需要生成2个单据号(一次操作,生成2张单据). 调用了2次方法a, 这种因为必须保证d在一个事务中,这样就也可能会存在上面说的问题. 请问这种情况怎么解决呢?


40分
引用 7 楼 qq315737546 的回复:
Quote: 引用 6 楼 dgqjava 的回复:
Quote: 引用 5 楼 qq315737546 的回复:
Quote: 引用 3 楼 dgqjava 的回复:

修改事务传播特性, 方法a在任何情况下都应该重新起一个事务, 而不应该加入已有的事务, 因为方法a本身是一个专门处理单据号的独立的业务逻辑,  这样程序锁和数据库锁的粒度保持一致, 就不会有你现在的问题

方法a是一个生成单号的方法, 单号需要连续.
如果事务单独控制, 那么方法b如果出现回滚(b的业务回滚), 方法a也要回滚. 且c方法可能已经在a回滚前的基础上进行了操作(因为a是单独事务),所以方法b回滚后, a方法还不能回滚,只能将b中用掉的单号单独保存,建立废号再利用机制.

这个改动貌似会更大.

你的意思是你十个操作只要有一个失败十个都要回滚, 如果是这样的话那么你的问题不是死锁的问题, 就算程序中不出现死锁, 线程1执行5次a方法, 线程2执行1次a方法, 线程1再执行5次a方法并且失败了, 那么就算不存在死锁仍然会存在你现在说的无法回滚的问题, 而你所说的解决方案在b和c方法上加锁也是不现实的, 这样相当于将事务序列化执行, 并发性能是无法容忍的, 所以你的这个问题本身从设计上就是不对的, 一个批量处理, 相当于多次单条的处理, 每次成功就提交事务, 就算有失败的也不是全部回滚, 而应该最后提示: 成功xxx条, 失败xxx条

是的.目前的解决办法是将批处理中的每条作为单独事务了. 

但是引发了另外一个问题. 在某个方法d中, 需要生成2个单据号(一次操作,生成2张单据). 调用了2次方法a, 这种因为必须保证d在一个事务中,这样就也可能会存在上面说的问题. 请问这种情况怎么解决呢?

这样的需求只能做成序列化事务, d方法和a方法公用一个锁

你的A方法的逻辑就是生成连续的单号?
那用一个sequence不就解决问题了么。 干嘛搞这么复杂。

20分
为什么不给方法A传入一个参数:生成单据号的数量?
解决办法如下:
问题一:批量处理.
      解决方法:将批量处理中每单条一个事务.
问题二.一个事务操作中需要2个单号,调用2次方法a
      解决方法:新增方法d,生成指定数量的单号. 应用层a,d写锁互斥,数据库层用悲观锁锁行

CodeBye 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权 , 转载请注明一个死锁的问题