事务失效场景

8/2/2023 aop事务

# 方法不是public

如果方法不是publicSpring事务也会失败,因为Spring的事务管理源码AbstractFallbackTransactionAttributeSource中有判断computeTransactionAttribute()。如果目标方法不是公共的,则TransactionAttribute返回null

kotlin复制代码// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
  return null;
}
1
2
3
4

# 使用 final 或 static

final和static失效原因: 因为Cglib使用字节码增强技术生成被代理类的子类并重写被代理类的方法来实现代理。如果被代理的方法的方法使用finalstatic关键字,则子类不能重写被代理的方法。

# 发生自身调用

事务失效自身调用目标类内容

事务失效自身调用切面内容

如上,如果直接在controller中调用A方法,当在B方法中添加一条异常语句,事务不会回滚,原因是应为方法A调用方法B为自身调用,不会走AOP代理。

事务失效自身调用Controller层调用

可以看到通过@Autowired注解注入到的对象已经进行了代理

事务失效自身调用服务内部调用

在方法A内直接调用方法B则没有生成代理,只是一个普通对象

事务失效自身调用解决办法

通过AopContext类,可以拿到当前对象的代理对象,前提需要开启Aop配置:

在主启动类下开启如下配置:

@EnableAspectJAutoProxy(exposeProxy = true)
1

然后在需要拿到自身代理对象的地方,通过AopContext类的静态方法获取

DemoService that = (DemoService) AopContext.currentProxy();
that.methodB();
1
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;
    }

}

1
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);
        ... 
    }
}
1
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();

        ...
    }
}
1
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("发生异常了");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18