相信大家平时CRUD肯定少不了用事务处理问题,说到事务第一个想到的肯定是 @Transactional 注解,这玩意方便,放到方法头就能开盖即食,但是方便的同时也有一些弊端,最近就刚好碰到事务常见的一个问题:长事务。
先提前预热一下事务的相关常识
1、什么是事务
事务是数据一致性最基本的保证,也就是说一个事务中的操作要么都成功,要么都失败,不允许部分成功。我们常说的事务就是jdbc事务
2、事务的传播属性
1) REQUIRED(默认属性)
如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
被设置成这个级别时,会为每一个被调用的方法创建一个逻辑事务域。如果前面的方法已经创建了事务,那么后面的方法支持当前的事务,如果当前没有事务会重新建立事务。
2) MANDATORY
支持当前事务,如果当前没有事务,就抛出异常。
3) NEVER
以非事务方式执行,如果当前存在事务,则抛出异常。
4) NOT_SUPPORTED
以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
5) REQUIRES_NEW
新建事务,如果当前存在事务,把当前事务挂起。
6) SUPPORTS
支持当前事务,如果当前没有事务,就以非事务方式执行。
7) NESTED
支持当前事务,新增Savepoint点,与当前事务同步提交或回滚。
嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
3、长事务的危害
一个事务如果过长会有什么影响?简单来说,在事务被开启到commit为止,数据库会一直占用锁资源,其次所有的事务视图会一直保存着,暂用存储空间,如果涉及到大量数据变更或io连接的话,就更加难堪了,最严重的就是数据库挂掉,这要是到生产影响可就不是一毛两毛的事了
4、解决方案
解决长事务第一个想到的就是缩小事务的粒度,把需要控制事务的方法抽出来独立加事务处理;@Transactional 也被称为声明式事务管理,通过AOP的方式由Spring容器集中管理,另一种就是编程式事务管理了,可以自由的控制事务的范围,虽然没有@Transactional 那么舒服,但是至少透明可控啊,代码如下
解决长事务第一个想到的就是缩小事务的粒度,把需要控制事务的方法抽出来独立加事务处理;@Transactional** 也被称为声明式事务管理,通过AOP的方式由Spring容器集中管理,另一种就是编程式事务管理了,可以自由的控制事务的范围,虽然没有@Transactional 那么舒服,但是至少透明可控啊,代码如下
定义一个执行接口
public interface TransactionCallBack { T doInTransaction(TransactionStatus status) throws Exception; }
定义编程式事务工具
@Component
@Slf4j
public class TransactionTemplate extends DefaultTransactionDefinition implements InitializingBean {
/**
* 事务管理器
*/
@Autowired
private PlatformTransactionManager transactionManager;
@Override
public void afterPropertiesSet() {
// 校验管理器是否被spring注入
if (this.transactionManager == null) {
throw new IllegalArgumentException("Property 'transactionManager' is required");
}
}
/**
* 事务执行器
* @param action
* @param <T>
* @return
*/
@Transactional
public <T> T execute(TransactionCallBack<T> action){
TransactionStatus status = this.transactionManager.getTransaction(this);
T result = null;
try {
result = action.doInTransaction(status);
}catch (Exception e){
// 事务回滚
this.transactionManager.rollback(status);
log.error("事务执行异常",e);
return result;
}
// 事务提交
this.transactionManager.commit(status);
return result;
}
}
如果不声明传播属性的话,默认是REQUIRED,当然也可以根据实际情况设置不同的方式
@Component
public class CustomizeTransactionTemplate extends TransactionTemplate{
/**
* 设置事务传播行为
* PROPAGATION_REQUIRED : 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务
* PROPAGATION_MANDATORY : 支持当前事务,如果当前没有事务,就抛出异常。
* PROPAGATION_NEVER : 以非事务方式执行,如果当前存在事务,则抛出异常。
* PROPAGATION_NOT_SUPPORTED : 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
* PROPAGATION_REQUIRES_NEW : 新建事务,如果当前存在事务,把当前事务挂起。
* PROPAGATION_SUPPORTS : 支持当前事务,如果当前没有事务,就以非事务方式执行。
* PROPAGATION_NESTED : 支持当前事务,新增Savepoint点,与当前事务同步提交或回滚。
*
* @return
* @author gqq 2022/9/27 - 15:50
**/
public void setPropagationName(String constantName){
this.setPropagationBehaviorName(constantName);
}
}
测试类如下
public class DemoTest {
@Autowired
private StoreInfoMapper storeInfoMapper;
@Autowired
private CustomizeTransactionTemplate transactionTemplate;
public void transactionTest(){
transactionTemplate.execute(status -> {
StoreInfo storeInfo = storeInfoMapper.selectById(1);
storeInfo.setUpdateTime(new Date());
// 更新时间
storeInfoMapper.updateById(storeInfo);
System.out.println(1/0);
return status;
});
}
}
执行结果
2022-09-27 17:36:59.005 ERROR 12324 --- [ main] c.c.f.c.s.t.TransactionTemplate : 事务执行异常
java.lang.ArithmeticException: / by zero
at com.food.service.sub.job.DemoTest.lambda$transactionTest$0(DemoTest.java:34) [classes/:na]
at com.food.common.starter.transaction.TransactionTemplate.execute(TransactionTemplate.java:47) ~[classes/:na]
at com.food.common.starter.transaction.TransactionTemplate$$FastClassBySpringCGLIB$$508a8f86.invoke(<generated>) [classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) [spring-core-5.3.8.jar:5.3.8]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779) [spring-aop-5.3.8.jar:5.3.8]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) [spring-aop-5.3.8.jar:5.3.8]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) [spring-aop-5.3.8.jar:5.3.8]
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) [spring-tx-5.3.8.jar:5.3.8]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) [spring-tx-5.3.8.jar:5.3.8]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) [spring-tx-5.3.8.jar:5.3.8]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) [spring-aop-5.3.8.jar:5.3.8]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) [spring-aop-5.3.8.jar:5.3.8]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) [spring-aop-5.3.8.jar:5.3.8]
......
由于1/0的原因,当然这个sql是不会被更新的,直接回滚,用到后面会发现,其实编程式事务管理还是很香的,只需注入CustomizeTransactionTemplate就可以直接使用
5、总结
事务还有很深的学问,这里就不一一多说了,平时开发还是要尽量避免长事务的代码,减少事务粒度,就能少吃点bug了