java代码中避免oracle锁表的措施(事务)

文章目录

    • 场景
    • 原因分析
      • 人为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注解,会把当前方法的所有代码都包在一个事务中。方法完成或异常都会终止事务。

模拟下异常未被捕获到,回滚代码是否执行。代码:

// 异常未被捕获到,回滚代码是否执行
@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.sleepInterruptedException 异常。
0/0 的算术异常,没有捕获到。 异常阻断代码的执行,按道理该报错,并返回500。 但是因为事务未终止(rollback或commit都可以终止事务),造成代码一直在等待事务回滚。这个service一直不停止,请求也一直得不到返回。 这个for update也一直不停止。

异常未被捕获到,用finally中代码回滚可以么

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来捕获,然后回滚事务可以么。代码:

// 异常未被捕获到,用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) 能够捕获所有异常,然后执行回滚代码。

@Transactional注解控制事务

手动控制事务太麻烦,用注解试试。
配置类先加上 @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 事务都是可以的。二者没有绝对好坏要看应用场景。

@Transactional 事务

使用简单,代码量少,而且不易出错。机制就是有异常就触发回滚。

@Transactional 事务设置超时时间

相当于给方法加一个定时器,如果超时就会触发SQLTimeoutException。例如:
@Transactional(timeout = 10)

手动事务

更加灵活,可以处理复杂的场景,例如一个方法中多次事务。但是需要一定功底,用不好的话,代码量会大很多,而且容易挖坑。

for update语句后面加nowait

一方面要少用for update,如果非用不可,最好在后面加nowait。
nowait如果获取不到权限,会立刻报错返回,这样即使有个别数据被锁住了,也会阻止后续的数据被锁住。

select * from t_user for update nowait;

for update后面加skip locked

for update skip locked 这样如果一条记录已经锁住,就不会查询出来。

select * from t_user where sex='男' for update skip locked

锁表原因分析

经常遇到锁表,最主要的原因就是for update之后,事务没有终止。

通过任意包含commit的语句,如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); // 更新
	}
}

通过transactionManager.commit(status); 显式的终止事务。

如果不好控制,可以采用个懒人通用模板。就是一个大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注解实现事务一定终止

如何理解@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

transactionManager的isComplete()方法

该方法判断事务是否完成(回滚或提交)。该方法一个主要的作用就是,可以避免重复终止,引起的报错。如代码:

if(!status.isCompleted()){
	System.out.println("事务未完成 进行回滚");
	transactionManager.rollback(status);
}

55

select * from system where rownum <10;
(Spring默认的事务传播行为是REQUIRED)
我这里使用的方法是在更新日志表的方法上加上@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)

@Transactional事务 细粒度不足

它只会整个提交或者回滚,如果我第一步想提交,第二步想回滚,就做不到。例如:

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表之后,只要有条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 :

@Transactional(rollbackFor = {RuntimeException.class,ClassNotFoundException.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循环中单条有记录。其他可以继续执行。
第二,只要不抛出异常,那么事务也不会回滚。
但是这有一个问题,不抛出异常,那么本行代码也不会回滚。 如果是复杂的转账业务。 那么这笔相当于错了。

你可能感兴趣的:(spring)