Spring事务、事务传播机制、TransactionTemplate的使用
Spring事务三大基础
1. PlatformTransactionManager
最顶层事务接口:约束了最基础的事务方法:
public interface PlatformTransactionManager extends TransactionManager { TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException; // 提交 void commit(TransactionStatus status) throws TransactionException; // 回滚 void rollback(TransactionStatus status) throws TransactionException; }
不同的数据库实现此接口,来支持Spring事务;
2. TransactionDefinition
TransactionDefinition定义Spring事务属性特点:
一般用于编程式事务,相当于配置声明式事务注解中的参数,主要参数如下:
- 事务的传播特性
- 事务的隔离性
- 事务的回滚规则:并不是所有异常都回滚;
- 事务超时时间
- 事务是否只读:对于RR隔离级别,即使是读操作,也需要避免不可重复读问题,有可能存在多次读取数据不一致现象,因此即使是读操作,也需要加上事务,而使用只读事务,可以进一步提高事务性能;
- 如果是单个查询,不需要加事务;
- 存在多查询,并且存在关联,需要数据一致,需要加事务;
3. TransactionStatus
可以通过TransactionStatus查看事务状态信息;
编程式事务,进行回滚、提交时需要此对象;
Spring事务的实现
声明式事务
当类或方法被
@Transactional
注解标记时,Spring通过AOP(面向切面编程)生成代理对象;
- 代理对象拦截目标方法,在方法执行前后添加事务管理逻辑(开启事务、提交/回滚);
事务的范围边界:
- 代理方法开始时,开启事务并关闭事务自动提交;
- 方法执行成功,则提交事务;
- 方法执行失败(抛出指定异常),则回滚事务;
声明式事务的回滚依赖于抛出异常:
- 默认当方法抛出Error和RuntimeException(下面高亮部分)时,执行回滚;如果是检查型异常则不会回滚;
Throwable ├── Error │ ├── OutOfMemoryError │ ├── StackOverflowError │ └── ... └── Exception ├── RuntimeException │ ├── NullPointerException │ ├── IndexOutOfBoundsException │ └── ... └── 检查型异常(Checked Exceptions) ├── IOException ├── SQLException └── ...
使用
@Transactional
:
- rollbackFor:指定要回滚的异常;
- propagation:事务传播行为,后续讨论;
- isolation:事务隔离级别;
- timeout:事务超时时间,具体事务的超时处理,在传播机制后再讨论;
@Transactional( rollbackFor = Throwable.class, propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 30 ) public void transfer(String fromUserId, String toUserId, Double amount) { UserAccountEntity fromUser = userAccountMapper.selectByUserId(fromUserId); UserAccountEntity toUser = userAccountMapper.selectByUserId(toUserId); // 转账 Double fromUserBalance = fromUser.getBalance() - amount; Double toUserBalance = toUser.getBalance() + amount; userAccountMapper.updateBalance(fromUser.getUserId(), fromUserBalance); userAccountMapper.updateBalance(toUser.getUserId(), toUserBalance); }
编程式事务
手动编码指定开启事务,执行数据库操作,如果失败,手动操作事务回滚;
编码级别可以控制事务的粒度,根据一些动态数据条件,判断是否应该回滚;
- 使用PlatformTransactionManager手动执行事务,注意其中回滚后的其他数据库操作,此时事务已结束,其他数据库操作不会回滚;
@Resource private PlatformTransactionManager transactionManager; public void transfer(String fromUserId, String toUserId, Double amount) { // 1. 开启事务 DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); def.setTimeout(30); TransactionStatus status = transactionManager.getTransaction(def); try { // 2. 执行数据库操作 doTransfer(fromUserId, toUserId, amount); // 3. 手动提交 transactionManager.commit(status); } catch (Exception e) { // 4. 手动回滚 transactionManager.rollback(status); // 5. 更新订单状态为失败,此处不会回滚,事务已经结束 transferRecordMapper.updateStatus(recordId, "FAIL", LocalDateTime.now()); } }
- 使用TransactionTemplate执行编程式事务,比PlatformTransactionManager更方便,但缺少灵活;
- 可以依赖异常,当抛出Error、RuntimeException则自动回滚;
- 也可以手动标记回滚,方法execute方法执行完后,自动回滚;
- 注意execute或executeWithoutResult中的所有操作都在事务内,都会一起提交或回滚;如代码中的高亮部分:
@Resource private TransactionTemplate transactionTemplate; // 示例1: public String transfer(String fromUserId, String toUserId, Double amount) { // 1. 开启事务 transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); transactionTemplate.setTimeout(30); // 超时时间为 30 秒 transactionTemplate.executeWithoutResult(status -> { // 2. 执行数据库操作,如果抛出Error、RuntimeException则自动回滚 doTransfer(fromUserId, toUserId, amount); }); } // 示例2: public String transfer(String fromUserId, String toUserId, Double amount) { // 1. 开启事务 transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); transactionTemplate.setTimeout(30); // 超时时间为 30 秒 transactionTemplate.executeWithoutResult(status -> { try { // 2. 执行数据库操作 doTransfer(fromUserId, toUserId, amount); } catch (Throwable e) { // 3. 手动标记事务回滚 status.setRollbackOnly(); // 4. 此处的SQL即使执行成功,最终也会被回滚 transferRecordMapper.updateStatus(recordId, "FAIL", LocalDateTime.now()); } }); }
事务的传播
事务的传播行为:不同的事务方法嵌套调用时,针对后续的事务,是共用事务、新建事务、还是不是用事务
可以用在父事务上来约束子事务,也可以用在子事务上,决定是否开启新的事务;
- PROPAGATION_REQUIRED(默认):共用一个事务,如果没有事务,就开启一个新的;
- PROPAGATION_REQUIRES_NEW:用于子事务中;
- 子事务新建一个事务,并把父事务挂起,当前事务单独执行,执行结束不受父事务回滚影响
- 子事务的异常也不会影响到父事务
- PROPAGATION_NESTED:放在父事务上,向下嵌套;
- 父事务回滚,子事务一起回滚
- 子事务回滚,父事务不受影响;但是子事务的异常,会影响父事务,父事务可能需要catch
- PROPAGATION_SUPPORTS:放在子事务上;
- 父事务存在,就用事务
- 不存在,就不用事务
- PROPAGATION_NOT_SUPPORTED:放在子事务上;
- 无论怎么样都不使用事务,如果存在父事务,挂起父事务
- 但是子方法抛出异常,会影响父事务
- PROPAGATION_MANDATORY:MANDATORY(强制性的)放在父事务,向下嵌套;
- 存在父事务,并且是MANDATORY的,如果没有子事务,就抛出异常
- PROPAGATION_NEVER:用于子事务;
- 不使用事务,如果存在父事务,就抛出异常
事务的超时回滚
- 超时检测:Spring通过事务管理器(DataSourceTransactionManager)在事务执行过程中周期性检查耗时。
- 标记回滚:一旦超时,Spring将当前事务标记为仅回滚(rollback-only)
- 抛出异常:抛出TransactionTimedOutException;
- 回滚事务:事务拦截器捕获异常后,调用PlatformTransactionManager.rollback();
事务的超时与传播机制:
- 如果存在嵌套事务,外层事务未超时,内层事务超时,则会导致外层事务标记为回滚;
- 若内层事务使用REQUIRES_NEW,此时如果子事务开启后,父事务是挂起的,子事务超时仅回滚内层独立事务,不影响外层。
事务失效场景
失效的本质:没有使用代理对象,或代理对象无法感知到指定异常,或数据库不支持事务;
- 异常被捕获,没有抛出,代理对象无法感知
@Transactional(rollbackFor = Exception.class) public void insertWithTransaction(Order order) { try{ // ... orderMapper.insert(order); } catch(Exception e){ // .. } }
- this方法调用,且注解在this方法上,主要看能不能使用代理对象执行事务方法;this无法使用代理对象来执行;
// @Transactional 加在这里事务不会失效! public void failCondition1() { Order order = OrderUtils.getOrder(); orderMapper.insert(order); this.insertOrderWithThisTransaction(order); } // 这里事务会失效,不可AOP,两条数据都会插入 @Transactional(rollbackFor = Exception.class) public void insertOrderWithThisTransaction(Order order) { orderMapper.insert(order); throw new RuntimeException(); }
- private、final、static修饰的方法,这些方法都无法实现代理;
- 数据库不支持事务,Spring事务本质上还是底层数据库的事务,数据库引擎需要支持事务;
如何处理大事务
首先是:避免长事务(耗时过久的事务);
- 长时间占用数据库连接;
- 事务涉及的数据库资源范围大,数据被锁,阻塞其他线程;
- 如果回滚了,也会需要很长时间;
如何避免事务时间过长:
- 避免事务内耗时任务:RPC、大计算任务;
- 要执行的事务并发过高,事务执行时锁竞争严重;
- 减少声明式事务,使用编程式事务,在需要事务的时候才开启事务;