事务失效场景
# 方法不是public
如果方法不是public
,Spring
事务也会失败,因为Spring
的事务管理源码AbstractFallbackTransactionAttributeSource
中有判断computeTransactionAttribute()。
如果目标方法不是公共的,则TransactionAttribute
返回null
。
kotlin复制代码// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
2
3
4
# 使用 final 或 static
final和static失效原因: 因为Cglib
使用字节码增强技术生成被代理类的子类并重写被代理类的方法来实现代理。如果被代理的方法的方法使用final
或static
关键字,则子类不能重写被代理的方法。
# 发生自身调用
如上,如果直接在controller中调用A方法,当在B方法中添加一条异常语句,事务不会回滚,原因是应为方法A调用方法B为自身调用,不会走AOP
代理。
可以看到通过@Autowired
注解注入到的对象已经进行了代理
在方法A内直接调用方法B则没有生成代理,只是一个普通对象
通过AopContext
类,可以拿到当前对象的代理对象,前提需要开启Aop
配置:
在主启动类下开启如下配置:
@EnableAspectJAutoProxy(exposeProxy = true)
然后在需要拿到自身代理对象的地方,通过AopContext
类的静态方法获取
DemoService that = (DemoService) AopContext.currentProxy();
that.methodB();
2
# 异常类型不匹配或没有抛出
Spring 默认只会在遇到运行时异常RuntimeException
或者error
时进行回滚,而IOException
等检查异常不会影响回滚。同时如果业务自身捕获了异常,也不会进行回滚。
因此,我们在进行事务操作的时候,如果操作步骤包含IO操作,则可能需要进行捕获,否则,事务将不会回滚。
Spring事务的切面优先级最低,因此其他切面如果对异常进行了捕获,则事务不会回滚,例子如下:
See More
@Service
public class TransactionService {
@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
}
@Component
public class AuditAspect {
@Autowired
private auditService auditService;
@Around(value = "execution (* com.alvin.*.*(..))")
public Object around(ProceedingJoinPoint pjp) {
try {
Audit audit = new Audit();
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] strings = methodSignature.getParameterNames();
audit.setMethod(signature.getName());
audit.setParameters(strings);
Object proceed = pjp.proceed();
audit.success(true);
return proceed;
} catch (Throwable e) {
log.error("{}", e);
audit.success(false);
}
auditService.save(audit);
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 多线程环境
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
比如,我有一个汽车服务方法,按照前端输入的汽车数据,将汽车各个模块的参数数据进行保存,我们创建的轮子需要依赖底盘,因此先创建底盘,然后组装轮子,在进行后续步骤。
public class CarService {
public void createCar(Car car) {
// 创建底盘
createChassis(car);
// 组装第一个轮子
createFirstWheel(car);
// 组装第二个轮子
createSeoncdWheel(car);
// 组装第三个轮子
createThirdWheel(car);
// 组装第四个轮子
createLastWheel(car);
...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
有人可能会认为四个轮子的创建互不相干,我们可以使用多线程提高数据写入效率,于是有了下面这种写法:
public class CarFactory {
public void createCar(Car car) throws InterruptedException {
// 创建底盘
createChassis(car);
CountDownLatch latch = new CountDownLatch(4);
ExecutorService pool = Executors.newFixedThreadPool(10);
// 组装第一个轮子
pool.submit(() -> {
createFirstWheel(car);
latch.countDown();
});
// 组装第二个轮子
pool.submit(() -> {
createSeoncdWheel(car);
latch.countDown();
});
// 组装第三个轮子
pool.submit(() -> {
createThirdWheel(car);
latch.countDown();
});
// 组装第四个轮子
pool.submit(() -> {
createLastWheel(car);
latch.countDown();
});
latch.await();
...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
先不说效率是否有提升,暂时看着没啥问题,但实际上各个线程的执行环境已经进行了隔离,由于都需要进行数据库操作,因此都需要获取数据库连接,最终提交的时候也是线程池内的单独提交,互补影响。如果主线程报错,需要回滚,由于各个线程拿到的Connection
和主线程不同,因此不会回滚,导致事务失效。
demo:
@Transactional
public void multiThreadTx() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
jdbcTemplate.execute("insert into person(name, addr, age) values ('valA', 'GZ', 22)");
pool.submit(() -> {
jdbcTemplate.execute("insert into person(name, addr, age) values ('valB', 'GZ', 22)");
latch.countDown();
});
pool.submit(() -> {
jdbcTemplate.execute("insert into person(name, addr, age) values ('valC', 'GZ', 22)");
latch.countDown();
});
latch.await();
throw new RuntimeException("发生异常了");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18