所谓大事务,就是耗时较长的事务。

Spring 有两种方式实现事务,分别是声明式事务编程式事务,如果不开启事务,MySQL 默认自定提交事务,也就是语句执行完后自动提交。

大事务产生的场景

  1. 操作的数据量大,或者事务中包含慢查询。
  2. 在事务中调用了外部 HTTP接口 或 RPC 服务。
  3. 大量的锁竞争。
  4. 执行了比较耗时的计算。

大事务造成的影响

  1. 大并发情况下,数据库连接池容易挤满。
  2. 锁定太多的数据,造成大量的阻塞和锁超时,导致数据库性能下降。
  3. 执行时间长,容易造成主从同步延迟时间长。
  4. 发生异常,回滚所需的时间比较长。
  5. undo log 日志膨胀,不仅增加了存储空间,而且可能降低查询的性能。

优化方式

使用编程式事务

在实际项目开发中,我们通常使用声明式事务,直接在方法上使用 @Transactional注解。

1
2
3
4
5
6
7
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public Member insert(...) {
Member member = memberMapper.selectByPhone(vo.getMobile());
...
return createNewUserResult == null ? member : createNewUserResult.member;
}

但是声明式事务有以下两个问题:

  1. @Transactional 注解是通过 AOP 实现的,但是如果使用不当,事务可能会失效,如果经验不丰富的话,很难排查问题。
  2. 直接加在方法上,粒度太粗,不好控制事务范围,很容易造成大事务。

所以,我们可以使用编程式事务,通过 TransactionTemplate 类来手动实现事务控制。

代码如下:

以下使用的是带有返回值的 TransactionCallback,如果你不需要返回值,则可以使用 TransactionCallbackWithoutResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CreateNewUserResult createNewUserResult = transactionTemplate.execute(new TransactionCallback < CreateNewUserResult > () {
@Override
public CreateNewUserResult doInTransaction(TransactionStatus transactionStatus) {
CreateNewUserResult createNewUserResult = null;
try {
createNewUserResult = createNewUser(cne, vo, member);
...
} catch (Exception e) {
log.error("注册用户,事务执行失败,开始回滚:", e);
transactionStatus.setRollbackOnly();
}
return createNewUserResult;
}
});

如果是业务简单的代码,直接使用 @Taansactional 也是可以的,但是要注意事务失效问题。

将 select 语句放到事务外

在事务中执行 select 语句,会导致事务中包含慢查询,从而导致大事务。我们可以将 select 语法的方法放到事务外,查询相关操作是不需要事务的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Member member = memberMapper.selectByPhone(vo.getMobile())

CreateNewUserResult createNewUserResult = transactionTemplate.execute(new TransactionCallback < CreateNewUserResult > () {
@Override
public CreateNewUserResult doInTransaction(TransactionStatus transactionStatus) {
CreateNewUserResult createNewUserResult = null;
try {
createNewUserResult = createNewUser(cne, vo, member);
...
} catch (Exception e) {
log.error("注册用户,事务执行失败,开始回滚:", e);
transactionStatus.setRollbackOnly();
}
return createNewUserResult;
}
});

避免请求外部HTTP接口或RPC服务

在业务中调用系统的接口是不可避免的,因为网络因素,可能接口响应时间较长,如果放在事务中,很可能就造成大事务了。

避免一次性处理太多数据

比如为了操作方便,可能会一次批量更新1000条数据,这样会导致大量数据锁等待,在并发大的时候尤为明显,解决办法是分页处理,比如前端每次请求只处理 50 条数据,这样可以大大减少大事务的出现。

异步处理

很好理解,减少事务的执行时间。

@Transactional 失效场景

  1. @Transactional 应用在非 public 方法上。
  2. @Transactional 注解属性 rollbackFor 设置错误,也就是异常无法兼容,Spring 默认抛出 unchecked 异常(继承自 RuntimeException )或者 Error 才会回滚事务,其它异常不会触发事务回滚,需要自己指定 rollbackFor
  3. 同一个类中方法调用,比如 A 方法没有声明注解,而 调用的 B 方法声明了注解,则方法 B 的事务不会生效。这也是最容易犯错的地方。解决方法也很简单,将 B 方法移到另外一个类中,或者使用 AopContext.currentProxy() 获取代理对象,然后调用 B 方法。
  4. 异常被手动 catch 了。

参考文章

一口气说出 6种,@Transactional注解的失效场景