文章目录
- 场景
- 原因分析
- 人为for update忘记提交
- 代码导致的锁表
- 临时方案
- 临时方案一
- 临时方案二
- 代码剖析
- 异常未被捕获到,导致回滚代码未执行
- 异常未被捕获到,用finally中代码回滚可以么
- 用Exception捕获所有未知异常
- @Transactional注解控制事务
- 总结
- @Transactional 事务
- @Transactional 事务设置超时时间
- 手动事务
- for update语句后面加nowait
- for update后面加skip locked
- 锁表原因分析
- 通过任意包含commit的语句,如update语句等来终止事务
- 通过transactionManager.commit(status); 显式的终止事务。
- 通过@Transactional注解实现事务一定终止
- 其他
- 事务可以被多次终止么
- transactionManager的isComplete()方法
- 55
- @Transactional事务 细粒度不足
- forupdate 会被update语句结束表锁
- for update no wait 获取不到会抛出什么异常
- @Transactional
- 加@Transactional一定为了回滚么?
- @Transactional 设置超时时间
- @Transactional设置触发回滚的类型
- rollbackFor设置触发回滚的异常类
- 为什么设置了异常回滚类,还是不回滚
- 如果rollbackFor没有抓到对应的类,事务会回滚么?会结束么?
- noRollbackFor设置不触发回滚的异常类
- for循环中的事务的回滚问题
场景
项目中,某些操作失败,看日志发现是锁表了。
原因分析
人为for update忘记提交
这个就比较恶劣了,例如在pl/sql中对某条记录执行了for update,然后忘了提交。它是不会自动提交的,直到session关闭,也就是关闭掉这个查询窗口(或者关闭掉pl/sql应用也行)。
这种情况要坚决避免,修改完语句要尽快提交。
代码导致的锁表
主要是事务没有结束引起的,可能的情况有2种:
原因 | 特点 |
异常未被捕获,导致事务终止代码未执行 | 这种很隐蔽,尤其是代码多的时候,不好找到错误 |
忘记提交或回滚了 | 细心点可以避免 |
临时方案
临时方案一
oracle命令行kill掉所有锁住的session
临时方案二
重启项目,释放掉引起锁住的服务。
这个要偷偷的来啊,别让客户发现了,如果有2个应用,那么没事,一个一个重启,不影响使用。
代码剖析
虽然有临时方案,但是从根本上解决问题才是最好的办法,先分析下。
异常未被捕获到,导致回滚代码未执行
有一点一定要注意: for update不会自动的提交,要提交for update可以有几种方法:
1、显式的事务终止,如: transactionManager.commit()或者rollback()
2、update执行完毕后会隐式的关闭当前事务,也会关闭for update。
3、@Transactional注解,会把当前方法的所有代码都包在一个事务中。方法完成或异常都会终止事务。
模拟下异常未被捕获到,回滚代码是否执行。代码:
执行一下发现请求一直得不到回应。2句日志也都没有打印出来。
原因:
catch中只捕获了,Thread.sleep
的 InterruptedException
异常。
0/0 的算术异常,没有捕获到。 异常阻断代码的执行,按道理该报错,并返回500。 但是因为事务未终止(rollback或commit都可以终止事务),造成代码一直在等待事务回滚。这个service一直不停止,请求也一直得不到返回。 这个for update也一直不停止。
异常未被捕获到,用finally中代码回滚可以么
finally表示代码总是会被执行,如果异常没被捕获到,我加个finally来关闭事务可以吧。如下代码:
实测无效,finally表示总是执行,但异常后,事务会等待回滚,一直卡住了,根本执行不到finally。
用Exception捕获所有未知异常
对于不确定的异常,用Exception来捕获,然后回滚事务可以么。代码:
实测有效。
catch (Exception e) 能够捕获所有异常,然后执行回滚代码。
@Transactional注解控制事务
手动控制事务太麻烦,用注解试试。
配置类先加上 @EnableTransactionManagement
启动注解服务。
service或者方法上添加 @Transactional
,代码:
实测有效。代码中未被catch的异常会触发回滚。
总结
综上,手动事务还是@Transactional 事务都是可以的。二者没有绝对好坏要看应用场景。
@Transactional 事务
使用简单,代码量少,而且不易出错。机制就是有异常就触发回滚。
@Transactional 事务设置超时时间
相当于给方法加一个定时器,如果超时就会触发SQLTimeoutException。例如:
@Transactional(timeout = 10)
手动事务
更加灵活,可以处理复杂的场景,例如一个方法中多次事务。但是需要一定功底,用不好的话,代码量会大很多,而且容易挖坑。
for update语句后面加nowait
一方面要少用for update,如果非用不可,最好在后面加nowait。
nowait如果获取不到权限,会立刻报错返回,这样即使有个别数据被锁住了,也会阻止后续的数据被锁住。
for update后面加skip locked
for update skip locked 这样如果一条记录已经锁住,就不会查询出来。
锁表原因分析
经常遇到锁表,最主要的原因就是for update之后,事务没有终止。
通过任意包含commit的语句,如update语句等来终止事务
一定要保证update的终止。
例如下面例子,查询id为1的数据,是存在的。 如果age大于9999,就改为99。
这里有个问题,实际中age不会大于9999,所以不会执行update。 这个事务就终止不了,一直缩下去。
通过transactionManager.commit(status); 显式的终止事务。
如果不好控制,可以采用个懒人通用模板。就是一个大try 给他括起来,然后catch 所有Exception。 有个问题就是多次return的代码里不好用。
代码如下:
通过@Transactional注解实现事务一定终止
如何理解@Transactional 呢 ,其实就是相当于把代码用一个事务包上。
@Transactional 保证了事务的完整性,除了细粒度不够之外,还算不错。
其他
事务可以被多次终止么
一个事务只能被终止(回滚或提交)一次,多次操作会报错。所以代码中rollback或commit不要重复了。如果重复操作,提示如下:
transactionManager的isComplete()方法
该方法判断事务是否完成(回滚或提交)。该方法一个主要的作用就是,可以避免重复终止,引起的报错。如代码:
55
select * from system where rownum <10;
(Spring默认的事务传播行为是REQUIRED)
我这里使用的方法是在更新日志表的方法上加上@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
@Transactional事务 细粒度不足
它只会整个提交或者回滚,如果我第一步想提交,第二步想回滚,就做不到。例如:
forupdate 会被update语句结束表锁
forupdate表之后,只要有条update语句,就会释放掉。不再锁住了。
所以如果forupdate多条的时候,最好用事务控制下。
for update no wait 获取不到会抛出什么异常
CannotAcquireLockException 会报这个异常。
@Transactional
加@Transactional一定为了回滚么?
这个还真不一定。
例如为了避免锁表,加@Transactional(timeout =10 ) 保证事务一定结束。
@Transactional 设置超时时间
timeout属性可以设置超时时间。
相当于给方法加一个定时器,如果超时就会触发SQLTimeoutException。例如:
@Transactional(timeout = 10)
timeout 超时异常,会被try catch捕获么,超时异常会发生在引起超时的代码上,所以如果用try到了代码,会捕获到,不用担心代码失控。
@Transactional设置触发回滚的类型
实际中,有的业务需要回滚,有的不需要。
需要回滚的业务:
转账业务(a账户转出,b账户要收到才一致)
不要回滚的业务:
下单支付业务(支付不成功,下单业务当然不用撤销,只需再付款即可)
rollbackFor设置触发回滚的异常类
@Transactiona(rollbackFor={RuntimeException.class})
默认是所有RuntimeException都会回滚。
如果要添加其他触发回滚类,不要忘记默认的 RuntimeException.class :
为什么设置了异常回滚类,还是不回滚
这原因就多了,但是主要就一条,被捕获的异常类和设置的不匹配。
1、如果使用了aop,要注意下,如果用@around注解的方法,里面try catch了异常,那么么异常不会继续抛出。 事务就无法触发了。
如果rollbackFor没有抓到对应的类,事务会回滚么?会结束么?
例如: 锁表引起的SQLTimeoutException就不是运行时异常,这样事务不会回滚,但是仍然会结束。
所以还是有用的,因为他会终止事务,避免持续锁表。
noRollbackFor设置不触发回滚的异常类
@Transactiona(noRollbackFor={RuntimeException.class})
for循环中的事务的回滚问题
for循环中的事务会遇到这样的问题:一条回滚其他也会被回滚。
解决方案:
1、for 中的代码写为service方法,然后在那个service上加@Transaction。
这种其实不好操作。 因为要加接口加方法。 而且service中有service的情况也很多。
2、for循环中的代码加try catch,只记录并不影响整体。
这种可以,第一for循环中单条有记录。其他可以继续执行。
第二,只要不抛出异常,那么事务也不会回滚。
但是这有一个问题,不抛出异常,那么本行代码也不会回滚。 如果是复杂的转账业务。 那么这笔相当于错了。