文章目录
1. 本地事务和分布式事务
这部分是题外话,查看@Transactional注解跳过此节
- 本地事务是指单个数据源(如一个数据库实例或一个消息队列等)内执行的事务操作。数据库系统本身能保证事务的四个ACID特性(原子性、一致性、隔离性和持久性)。代码中使用
Spring
的@Transactional
注解就能实现本地事务 - 分布式事务是指跨数据源(如多个数据库实例、数据库等)之间执行的事务操作。分布式事务管理复杂,需要协调多个参与者来确保事务的一致性,常见的协调机制两阶段提交,三阶段提交,TCC,XA等。由于网络延迟、系统故障等问题,分布式事务难以严格保证 ACID 特性。很多系统选择牺牲某些属性(如短暂不一致)来提高系统的可用性,系统允许某一时刻数据不一致,但是通过补偿或者重试机制,最终达到最终一致性
注意到本地事务是单个数据源,所以说分库分表中,有一种逻辑分库使用本地事务就能解决。因为逻辑分库只是将表放在不同地数据库,而这些数据库任然运行在同一个数据库实例中,对应地物理分库就需要分布式事务解决了
2. @Transactional注解使用
spring
中事务解决方案有两种,编程式事务和声明式事务,@Transactional
就是声明式事务地解决方案
@Tranasctional
注解是Spring 框架提供的声明式注解事务解决方案,我们在开发中使用事务保证方法对数据库操作的原子性,要么全部成功,要么全部失败,对应地只需要在方法或类中添加@Tranasctional
即可。
常见的使用方式:使用rollbackFor
属性来定义回滚的异常类型,使用 propagation
属性定义事务的传播行为。
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void processPayment(Order order) throws Exception {
// 如果抛出 Exception 或其子类的异常,将回滚事务
if (paymentFailed()) {
throw new Exception("Payment failed");
}
}
@Transactional
注解只能回滚非检查型异常,具体为RuntimeException
及其子类和Error
子类,可以从Spring
源码的DefaultTransactionAttribute
类里找到判断方法rollbackOn
。
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
2.1 @Transactional属性
propagation
(事务传播行为)isolation
(事务隔离级别)timeout
(事务超时时间)readOnly
(是否只读)rollbackFor
(回滚条件,指定哪些异常会触发回滚)noRollbackFor
(不回滚条件,指定哪些异常不会触发回滚)rollbackForClassName
(根据异常类名回滚)noRollbackForClassName
(根据异常类名不回滚)transactionManager
(自定义事务管理器)value
(别名,等同于transactionManager
)
2.2 @Transactional事务失效
使用@Transactional
注解时可能会出现事务失效:
-
底层数据库本身不支持事务
-
@Transactional
注解只能用在public
方法上,如果用在protected
或者private
的方法上,不会报错,但是该注解不会生效。 -
@Transactional
注解不能回滚被try{}catch()
捕获的异常。 -
@Transactional
注解只能对在被Spring
容器扫描到的类下的方法生效。 -
类内部调用调用类内部@Transactional标注的事务方法,事务失效
public class Test{ public void A(){ //插入一条数据 //调用B方法 B(); } @Transactional public void B(){ //插入数据 } }
为什么会失效呢?其实原因很简单,
Spring
在扫描Bean的时候会自动为标注了@Transactional
注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。
2.3 @Transactional事务失效分析
这部分为摘录他人笔记整理!!!@Transactional注解不起作用解决办法及原理分析_transactional注解作用异常-CSDN博客
主要时分析
@Transactional
标识非public方法,或者类内部调用事务方法,事务方法异常被try{}catch
原因简述:
@Transactional
事务管理基于动态代理实现
@Transactional
标识非public方法,spring
将不会对bean
进行代理对象创建或者不会对方法进行代理调用- 类内部调用类内部的事务方法,这个调用事务方法的过程并不是通过代理对象来调用的,而是直接通过this对象来调用方法,绕过了代理对象,肯定就是没有代理逻辑
- 注解没有捕获异常,相应事务方法失效
2.3.1 非public方法事务失效
@Transactional
注解标注方法修饰符为非public时,@Transactional
注解将会不起作用。这里分析 的原因是,@Transactional
是基于动态代理实现的,@Transactional
注解实现原理中分析了实现方法,在bean初始化过程中,对含有@Transactional
标注的bean实例创建代理对象,这里就存在一个spring扫描@Transactional
注解信息的过程,不幸的是源码中体现,标注@Transactional的方法如果修饰符不是public,那么就默认方法的@Transactional
信息为空,那么将不会对bean进行代理对象创建或者不会对方法进行代理调用
@Transactional
注解实现原理中,介绍了如何判定一个bean是否创建代理对象,大概逻辑是。根据spring创建好一个aop切点BeanFactoryTransactionAttributeSourceAdvisor
实例,遍历当前bean的class的方法对象,判断方法上面的注解信息是否包含@Transactional
,如果bean任何一个方法包含@Transactional
注解信息,那么就是适配这个BeanFactoryTransactionAttributeSourceAdvisor切点。则需要创建代理对象,然后代理逻辑为我们管理事务开闭逻辑。
spring源码中,在拦截bean的创建过程,寻找bean适配的切点时,运用到下面的方法,目的就是寻找方法上面的@Transactional信息,如果有,就表示切点BeanFactoryTransactionAttributeSourceAdvisor
能够应用(canApply)到bean中,
AopUtils#canApply(org.springframework.aop.Pointcut, java.lang.Class<?>, boolean)
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
Assert.notNull(pc, "Pointcut must not be null");
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
MethodMatcher methodMatcher = pc.getMethodMatcher();
if (methodMatcher == MethodMatcher.TRUE) {
// No need to iterate the methods if we're matching any method anyway...
return true;
}
IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
}
//遍历class的方法对象
Set<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
classes.add(targetClass);
for (Class<?> clazz : classes) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
if ((introductionAwareMethodMatcher != null &&
introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
//适配查询方法上的@Transactional注解信息
methodMatcher.matches(method, targetClass)) {
return true;
}
}
}
return false;
}
我们可以在上面的方法打断点,一步一步调试跟踪代码,最终上面的代码还会调用如下方法来判断。在下面的方法上断点,回头看看方法调用堆栈也是不错的方式跟踪
AbstractFallbackTransactionAttributeSource#getTransactionAttribute
AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
// Don't allow no-public methods as required.
//非public 方法,返回@Transactional信息一律是null
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
//后面省略.......
}
@Transactional注解标识非public方法,spring不会创建代理对象或者不进行代理调用导致事务失效
第一种:不创建代理对象
测试代码:
/**
* @author zhoujy
**/
@Component
public class TestServiceImpl {
@Resource
TestMapper testMapper;
@Transactional
void insertTestWrongModifier() {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
}
@Component
public class InvokcationService {
@Resource
private TestServiceImpl testService;
public void invokeInsertTestWrongModifier(){
//调用@Transactional标注的默认访问符方法
testService.insertTestWrongModifier();
}
}
//测试用例
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Resource
InvokcationService invokcationService;
@Test
public void testInvoke(){
invokcationService.invokeInsertTestWrongModifier();
}
}
所以,如果所有方法上的修饰符都是非public的时候,那么将不会创建代理对象。以上述测试代码为例,如果正常的修饰符的testService是下面图片中的,经过cglib创建的代理对象,如下:
如果class中的方法都是非public的那么将不是代理对象。
第二种:不进行代理调用
考虑一种情况,如下面代码所示。两个方法都被@Transactional注解标注,但是一个有public修饰符一个没有,那么这种情况我们可以预见的话,一定会创建代理对象,因为至少有一个public修饰符的@Transactional注解标注方法。
创建了代理对象,insertTestWrongModifier就会开启事务吗?答案是不会。
/**
* @author zhoujy
**/
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Override
@Transactional
public void insertTest() {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
@Transactional
void insertTestWrongModifier() {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
}
原因是在动态代理对象进行代理逻辑调用时,在cglib创建的代理对象的拦截函数中CglibAopProxy.DynamicAdvisedInterceptor#intercept
,有一个逻辑如下,目的是获取当前被代理对象的当前需要执行的method适配的aop逻辑。
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
而针对@Transactional注解查找aop逻辑过程,相似地,也是执行一次
AbstractFallbackTransactionAttributeSource#getTransactionAttribute
AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
也就是说还需要找一个方法上的@Transactional注解信息,没有的话就不执行代理@Transactional对应的代理逻辑,直接执行方法。没有了@Transactional注解代理逻辑,就无法开启事务
2.3.2 类内部调用事务方法
在类内部调用调用类内部@Transactional标注的方法。这种情况下也会导致事务不开启。
经过对第一种的详细分析,对这种情况为何不开启事务管理,原因应该也能猜到;
既然事务管理是基于动态代理对象的代理逻辑实现的,那么如果在类内部调用类内部的事务方法,这个调用事务方法的过程并不是通过代理对象来调用的,而是直接通过this对象来调用方法,绕过的代理对象,肯定就是没有代理逻辑了。
其实我们可以这样玩,内部调用也能实现开启事务,代码如下。
/**
* @author zhoujy
**/
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Resource
TestServiceImpl testServiceImpl;
@Transactional
public void insertTestInnerInvoke() {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
public void testInnerInvoke(){
//内部调用事务方法
testServiceImpl.insertTestInnerInvoke();
}
}
上面就是使用了代理对象进行事务调用,所以能够开启事务管理,但是实际操作中,没人会闲的蛋疼这样子玩~
2.3.3 事务方法中异常被捕获
事务方法内部捕捉了异常,没有抛出新的异常,导致事务操作不会进行回滚。
这种的话,可能我们比较常见,问题就出在代理逻辑中,我们先看看源码里卖弄动态代理逻辑是如何为我们管理事务的。
TransactionAspectSupport#invokeWithinTransaction
代码如下
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
//开启事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
//反射调用业务方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
//异常时,在catch逻辑中回滚事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
//提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
else {
//....................
}
}
所以看了上面的代码就一目了然了,事务想要回滚,必须能够在这里捕捉到异常才行,如果异常中途被捕捉掉,那么事务将不会回滚
3. Spring事务的传播行为
事务的传播行为指在方法调用过程中,调用方法和被调用方法的事务如何在传播或管理。换句话说,一个事务方法里调用另一个方法A,那么方法A的事务如何处理?
Spring
事务的传播行为一共有7种,定义在spring-tx
模块的Propagation
枚举类里,常量分别对应int
值0~6
PROPAGATION_REQUIRED | 支持当前事务,如果当前没有事务,则创建一个事务,这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务来执行 |
PROPAGATION_MANDATORY | 支持当前事务,如果没有当前事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务执行操作,如果当前存在事务,则当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED 类似的操作。 |