项目中,某些操作失败,看日志发现是锁表了。
这个就比较恶劣了,例如在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注解,会把当前方法的所有代码都包在一个事务中。方法完成或异常都会终止事务。
模拟下异常未被捕获到,回滚代码是否执行。代码:
// 异常未被捕获到,回滚代码是否执行
@ResponseBody
@RequestMapping("/demo1")
public String demo1()
{
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userMapper.forupdate("张三"); // for update占用这条记录
System.out.println(0/0); // 模拟异常
Thread.sleep(1000);
transactionManager.commit(status);
System.out.println("已提交");
} catch (InterruptedException e) {
e.printStackTrace();
transactionManager.rollback(status);
System.out.println("异常了 回滚");
}
return "demo1";
}
执行一下发现请求一直得不到回应。2句日志也都没有打印出来。
原因:
catch中只捕获了,Thread.sleep
的 InterruptedException
异常。
0/0 的算术异常,没有捕获到。 异常阻断代码的执行,按道理该报错,并返回500。 但是因为事务未终止(rollback或commit都可以终止事务),造成代码一直在等待事务回滚。这个service一直不停止,请求也一直得不到返回。 这个for update也一直不停止。
finally表示代码总是会被执行,如果异常没被捕获到,我加个finally来关闭事务可以吧。如下代码:
// 异常未被捕获到, 再finally中添加回滚代码有用么
@ResponseBody
@RequestMapping("/demo2")
public String demo2()
{
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userMapper.forupdate("张三"); // for update占用这条记录
System.out.println(0/0); // 模拟异常
Thread.sleep(1000);
transactionManager.commit(status);
System.out.println("已提交");
} catch (InterruptedException e) {
e.printStackTrace();
transactionManager.rollback(status);
System.out.println("异常了 回滚");
}finally {
if(!status.isCompleted()){
System.out.println("事务未完成 进行回滚");
transactionManager.rollback(status);
}
}
return "demo2";
}
实测无效,finally表示总是执行,但异常后,事务会等待回滚,一直卡住了,根本执行不到finally。
对于不确定的异常,用Exception来捕获,然后回滚事务可以么。代码:
// 异常未被捕获到,用Exception捕获所有异常,然后执行回滚代码
@ResponseBody
@RequestMapping("/demo3")
public String demo3()
{
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userMapper.forupdate("张三"); // for update占用这条记录
System.out.println(0/0); // 模拟异常
Thread.sleep(1000);
transactionManager.commit(status);
System.out.println("已提交");
} catch (InterruptedException e) {
e.printStackTrace();
transactionManager.rollback(status);
System.out.println("InterruptedException异常了 回滚");
}catch (Exception e) {
e.printStackTrace();
transactionManager.rollback(status);
System.out.println("Exception异常了 回滚");
}
return "demo3";
}
实测有效。
catch (Exception e) 能够捕获所有异常,然后执行回滚代码。
手动控制事务太麻烦,用注解试试。
配置类先加上 @EnableTransactionManagement
启动注解服务。
service或者方法上添加 @Transactional
,代码:
// 事务注解,看能搞定么 只要抛出了异常,事务就会触发回滚
@Transactional
@ResponseBody
@RequestMapping("/demo4")
public String demo4()
{
try {
userMapper.forupdate("张三"); // for update占用这条记录
System.out.println(0/0); // 模拟异常
Thread.sleep(1000);
System.out.println("已提交");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("异常了 回滚");
}
return "demo4";
}
实测有效。代码中未被catch的异常会触发回滚。
综上,手动事务还是@Transactional 事务都是可以的。二者没有绝对好坏要看应用场景。
使用简单,代码量少,而且不易出错。机制就是有异常就触发回滚。
相当于给方法加一个定时器,如果超时就会触发SQLTimeoutException。例如:
@Transactional(timeout = 10)
更加灵活,可以处理复杂的场景,例如一个方法中多次事务。但是需要一定功底,用不好的话,代码量会大很多,而且容易挖坑。
一方面要少用for update,如果非用不可,最好在后面加nowait。
nowait如果获取不到权限,会立刻报错返回,这样即使有个别数据被锁住了,也会阻止后续的数据被锁住。
select * from t_user for update nowait;
for update skip locked 这样如果一条记录已经锁住,就不会查询出来。
select * from t_user where sex='男' for update skip locked
经常遇到锁表,最主要的原因就是for update之后,事务没有终止。
一定要保证update的终止。
例如下面例子,查询id为1的数据,是存在的。 如果age大于9999,就改为99。
这里有个问题,实际中age不会大于9999,所以不会执行update。 这个事务就终止不了,一直缩下去。
UserBean userBean = new UserBean();
userBean.setId(1l);
List<UserBean> users = userMapper.forupdateUser(userBean);// for update占用全表
if (!CollectionUtils.isEmpty(users)){
if(users.get(0).getAge()>9999){ // 如果大于9999
users.get(0).setAge(99l); // 改为99
userMapper.updateUser(userBean); // 更新
}
}
如果不好控制,可以采用个懒人通用模板。就是一个大try 给他括起来,然后catch 所有Exception。 有个问题就是多次return的代码里不好用。
代码如下:
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 代码...
// 代码...
} catch (Exception e) {
e.printStackTrace();
System.out.println("异常了");
if(!status.isCompleted()){
transactionManager.rollback(status); // 回滚
}
}finally {
if(!status.isCompleted()){
transactionManager.commit(status); // 提交
}
}
如何理解@Transactional 呢 ,其实就是相当于把代码用一个事务包上。
savepoint a; // 开启事务
try{
//code
//code
}catch(RuntimeException e){
rollback to a; // 运行时异常触发回滚
}
commit; // 提交
@Transactional 保证了事务的完整性,除了细粒度不够之外,还算不错。
一个事务只能被终止(回滚或提交)一次,多次操作会报错。所以代码中rollback或commit不要重复了。如果重复操作,提示如下:
do not call commit or rollback more than once per transaction
该方法判断事务是否完成(回滚或提交)。该方法一个主要的作用就是,可以避免重复终止,引起的报错。如代码:
if(!status.isCompleted()){
System.out.println("事务未完成 进行回滚");
transactionManager.rollback(status);
}
select * from system where rownum <10;
(Spring默认的事务传播行为是REQUIRED)
我这里使用的方法是在更新日志表的方法上加上@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
它只会整个提交或者回滚,如果我第一步想提交,第二步想回滚,就做不到。例如:
select * from t_user for update; // 锁定t_user表
update t_user set age=‘35’ where id='1'; // 更新一条数据
System.out.println(0/0);
update t_user set age=‘40’ where id='2'; // 在更新一条数据
forupdate表之后,只要有条update语句,就会释放掉。不再锁住了。
所以如果forupdate多条的时候,最好用事务控制下。
CannotAcquireLockException 会报这个异常。
这个还真不一定。
例如为了避免锁表,加@Transactional(timeout =10 ) 保证事务一定结束。
timeout属性可以设置超时时间。
相当于给方法加一个定时器,如果超时就会触发SQLTimeoutException。例如:
@Transactional(timeout = 10)
timeout 超时异常,会被try catch捕获么,超时异常会发生在引起超时的代码上,所以如果用try到了代码,会捕获到,不用担心代码失控。
实际中,有的业务需要回滚,有的不需要。
需要回滚的业务:
转账业务(a账户转出,b账户要收到才一致)
不要回滚的业务:
下单支付业务(支付不成功,下单业务当然不用撤销,只需再付款即可)
@Transactiona(rollbackFor={RuntimeException.class})
默认是所有RuntimeException都会回滚。
如果要添加其他触发回滚类,不要忘记默认的 RuntimeException.class :
@Transactional(rollbackFor = {RuntimeException.class,ClassNotFoundException.class})
这原因就多了,但是主要就一条,被捕获的异常类和设置的不匹配。
1、如果使用了aop,要注意下,如果用@around注解的方法,里面try catch了异常,那么么异常不会继续抛出。 事务就无法触发了。
例如: 锁表引起的SQLTimeoutException就不是运行时异常,这样事务不会回滚,但是仍然会结束。
所以还是有用的,因为他会终止事务,避免持续锁表。
@Transactiona(noRollbackFor={RuntimeException.class})
for循环中的事务会遇到这样的问题:一条回滚其他也会被回滚。
解决方案:
1、for 中的代码写为service方法,然后在那个service上加@Transaction。
这种其实不好操作。 因为要加接口加方法。 而且service中有service的情况也很多。
2、for循环中的代码加try catch,只记录并不影响整体。
这种可以,第一for循环中单条有记录。其他可以继续执行。
第二,只要不抛出异常,那么事务也不会回滚。
但是这有一个问题,不抛出异常,那么本行代码也不会回滚。 如果是复杂的转账业务。 那么这笔相当于错了。