【数据库,事务】【转载】@Transactional 踩坑记录(不生效,并发,回滚问题)

原文:https://blog.csdn.net/JinglongSource/article/details/105026665

1、@Transactional 不生效?

1. 是否添加依赖?

新项目经常会忘记添加各种依赖导致(Transactional依赖AOP实现,因此需要导入aop相关依赖)

2. 方法是否是公开的( pubilc ) ?

(在idea 里面,加事务注解的方法必须是 public 的,所以这种情况还行,比较容易发现)

@Transactional
public void test() {
	// 要求 test() 方法必须是 public 修饰, 如果是IDEA编辑器, 甚至直接警告了
	// TODO
}

3. @Transactional 所属类被 spring 所管理? 类上是否包含 @Controller | @Service | @Component …

@Transactional 是依赖AOP实现的,它所在的类也必须要在 spring 管理啦~

@Service
// 要求所属类必须被 spring 容器所管理, 否则不生效那就很正常啦
public class TestService {

    @Transactional
    public void test() {
    }
}

4. @Transactional 有些异常没有回滚? 注明 rollbackFor (阿里巴巴规范也要求)

可查的异常(checked exceptions):Exception下除了RuntimeException外的异常
不可查的异常(unchecked exceptions):RuntimeException及其子类和错误(Error)

@Transactional 事务的回滚仅仅对于unchecked的异常有效。对于checked异常无效。也就是说事务回滚仅仅发生在出现RuntimeException或Error的时候。
显示设置 rollbackFor 非常有必要,否则程序出现莫名异常可能会导致事务没有回滚生效

@Transactional(rollbackFor = Throwable.class)
public void test() {
}

5. 查看数据库或表,设置的引擎。MyISAM是不支持事务的,必须改为InnoDB

image




2: 本类方法调用本类事务方法会导致事务不生效

(就是一个非事务的方法A 调用一个加了事务注解的方法B,这个时候,方法B的事务会失效,发生异常时,不回滚)—— 这种场景好容易就会出现。。。)


问题源码

public void doBusiness() {
    // ...
    //
    // update sql
    this.update();
    // ...
}


@Transactional(rollbackFor = Throwable.class)
public void update() {
    // do many update sql
    // ...
    // throw new RuntimeException();
    // 这个时候 update() 并不会回滚
}

简单问题说明:
image

解决方案1: 配置暴露aop代理类

1、配置类添加配置 暴露代理类
@EnableAspectJAutoProxy(exposeProxy = true)
2、使用 AopContext.currentProxy() 获取当前代理对象调用目标方法
// this.update();
((TestService) AopContext.currentProxy()).update();

解决方案2: 利用ApplicationContext 获取实例调用目标方法

这里的实例可以理解为代理对象, 但是又不是真正的代理对象 (就是从容器里面把这个 bean 取出来,再去调用它的方法。上面描述事务不生效的场景是因为被调用的方法虽然加了事务,但它不是在当前类的第一层被调用,所以它无法被AOP识别到。如果在这个方法里面,再把容器bean 取出来,再去调用,就相当于在这个 bean 直接调用这个方法了,那这样事务就可以生效了)

@Autowired
private ApplicationContext applicationContext;

public void doBusiness() {
    // ...
    // update sql
    // this.update();
    applicationContext.getBean(this.getClass()).update();
    throw new RuntimeException();
}

解决方案3: 注入自身实例

这个方法其实和方法2是同一个意思,只要这个方法是被这个bean的第一层调用,事务就能生效。那从context 里面取也好,注入自身也好,都是一个意思。

@Service
@Slf4j
public class TestService {

    @Autowired
    private TestService testService; // 注入自身实例

    public void doBusiness() {
        // ...
        // update sql
        // this.update();
        testService.update(); // 利用注入实例调用目标方法
    }

    @Transactional(rollbackFor = Throwable.class, propagation = Propagation.NESTED)
    public void update() {
        // do many update sql
    }
}




坑3: TRANSACTIONAL 结合 TRY-FINALLY 使用偶尔感觉 FINALLY 方法块不执行?

问题源码

@Transactional(rollbackFor = Throwable.class)
public void update() {
    try {
        // 操作一些业务,
    } finally {
        // 业务最终都需要 删除一些临时数据, 因此确保代码执行, 放在finally代码块中
    }
}

这个问题我是在实际开发中遇到的,当时排查了十几分钟大概知道问题所在,问题导致原因是因为 try-finally 中 try 可能会发生异常, 那就会抛出异常,因此就算执行到了finally代码块中, 执行了业务(删除一些临时数据),但是由于整个方法没有catch住异常,导致异常上抛,事务生效,给人假象就是 finally 没有执行一样


(嗯,个人理解一下,就是finally 部分执行了,但因为事务还没有提交的,同时因为有异常抛出,整个事务回滚了,finally 执行了,但又被回滚,于是就仿佛没有执行。

解决方案1: 添加 catch 代码块 —— 添加 catch 代码块捕获异常, 不让异常上抛

(像这样把异步捕获住,异常不往上抛,事务就能够提交,finally 块就能执行。但是这样得看业务是否允许把异常 catch 掉。视业务而定)

@Transactional(rollbackFor = Throwable.class)
public void update() {
    try {
        // 操作一些业务,
    } catch(Exception e) {
		log.warn("update() ", e);
    } finally {
        // 业务最终都需要 删除一些临时数据, 因此确保代码执行, 放在finally代码块中
    }
}

解决方案2: 独立finally代理块, 开启新事物提交

独立finally代理块, 开启新事物提交
(感觉这种会比较好一些,因为 finally 无论是否异常都要执行,所以它更适合自己一个事务。把 finally 块做一个单独的方法,并且加事务注解,但也要需要注意比较容易出现 “类方法调用类事务方法不生效”的问题,所以这里的 finally 块要从容器里面获取 bean,再通过这个bean 去再调用一下)

@Transactional(rollbackFor = Throwable.class)
public void update() {
    try {
        // 操作一些业务
    } finally {
        // 业务最终都需要 删除一些临时数据, 因此确保代码执行, 再放finally代码块中
        applicationContext.getBean(this.getClass()).releaseData();
    }
}

@Transactional(rollbackFor = Throwable.class, propagation = Propagation.REQUIRES_NEW)
public void releaseData() {
    // do many update sql
}





坑4: Transactional 结合 synchronized 使用仍存在并发问题

这里假设单实例环境,并且不考虑事务级别问题

问题源码

@Transactional(rollbackFor = Throwable.class)
public synchronized void doProcess(String id) {
    // 操作一些业务
    // 根据ID查询 如果存在就更新, 否则新增
    if(this.getById(id) == null) {
        // 根据 id 新增一条记录
        // ...
        // 这里操作多张表 do many update sql
    } else {
        // 根据ID 更新某些字段
        // ...
        // 这里操作另外一些表 do many update sql
    }
}

产生问题:在并发情况下有可能会新增多条重复的记录
会有这种情况:A线程根据ID查询了不存在,新增流程往下执行,出了synchronized 作用域准备提交事务的时候(还没提交),被B线程抢夺CPU执行权了,获得执行权的B线程携带ID刚好就是A线程的ID,因此B线程查询了也是不存在,执行新增流程,最终就会有多个新增记录

嗯,就是一个边界问题,因为这个原子方法被事务包了一层,事务并不是原子的,在多线程并发下,会出现问题的

image

解决方案1: 数据库表设置限制

数据库表设置唯一主键或者唯一索引进行限制,那么就会确保不会新增重复记录,如果重复记录就会抛出异常

解决方案2: synchronized 作用域包含 proxy 处理事务

synchronized 作用域包含 proxy 处理事务即可, 如下:

因为原问题代码的原因是,方法是原子方法,但事务不是原子的 —— 那如果把事务放在原子方法里面就好了。
就是方法里面套事务 —— 那就是原子方法套事务方法
然后事务方法调用的时候,要从context获取bean 再去调用,就好了。。。(自身注解也行。)

public synchronized void doProcess(String id) {
    // 开始事务 在 synchronized 作用域
    applicationContext.getBean(this.getClass()).doBusiness(id);
    // 提交或者回滚 在 synchronized 作用域
}

@Transactional(rollbackFor = Throwable.class)
public void doBusiness(String id) {
    // 操作一些业务
    // 根据ID查询 如果存在就更新, 否则新增
    if(this.getById(id) == null) {
        // 根据 id 新增一条记录
        // ...
        // 这里操作多张表 do many update sql
    } else {
        // 根据ID 更新某些字段
        // ...
        // 这里操作另外一些表 do many update sql
    }
}




(完)
感谢原博主的清晰描述。

posted @ 2023-04-22 11:06  aaacarrot  阅读(559)  评论(0编辑  收藏  举报