@Transactional 踩坑记录(不生效,并发,回滚问题)

本文详细解析了Spring框架中@Transactional注解在实际项目应用中可能遇到的问题,包括事务不生效、本类方法调用导致事务失效、结合try-finally使用的注意事项及并发问题,并提供了解决方案。

该博文主要记录我在实际项目开发中遇到的 Transactional 坑,因为我遇到好几次了类似的不太相同坑了,所以周末决定还是花点时间整理一下,并且深入了解一下 Transactional 实现机制以及记录一下这几个坑。。。

坑1: @Transactional 不生效?

解决思路如下:

1. 是否添加依赖?

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

compile 'org.springframework.boot:spring-boot-starter-aop'

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

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

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

@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

在这里插入图片描述


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

问题源码

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


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

这里面涉及挺多知识点,在这里主要简单说一下原因就好了,其实也不能说是 @Transactional 的坑, 应该说是 aop 的坑, 因为所有的aop都会有这个问题,原因如下:

我们都知道aop(切面,方法增强),至于它是怎么做到的?其实就涉及到设计模式的代理问题了,了解更多设计模式之代理问题请看我的另一篇博客

简单问题说明:

在这里插入图片描述

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

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

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

这里的实例[1]可以理解为代理对象, 但是又不是真正的代理对象

@Autowired
private ApplicationContext applicationContext;

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

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

@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 没有执行一样

解决方案1: 添加 catch 代码块

添加 catch 代码块捕获异常, 不让异常上抛

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

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

独立finally代理块, 开启新事物提交

@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线程查询了也是不存在,执行新增流程,最终就会有多个新增记录

在这里插入图片描述

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

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

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

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


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
    }
}

追求卓越,然时间、经验、能力有限。写出来的博文有很多不足的地方,希望大家包容、不吝赐教,给我提意见、建议。感谢你们!

blog: https://ihouyu.cn
mail : for.houyu@foxmail.com
csdn: https://blog.youkuaiyun.com/jinglongsou

### @Transactional 和 @Async 同时注解导致事务回滚失效的原因 当 `@Transactional` 和 `@Async` 注解同时应用在一个方法上时,可能会遇到事务管理不按预期工作的情况。具体来说,在异步方法中标记的事务可能不会按照期望的方式进行回滚。 #### 原因分析 1. **代理机制差异** Spring 使用不同的代理策略来处理 `@Transactional` 和 `@Async` 的行为。对于 `@Transactional`,通常采用的是基于 JDK 动态代理或 CGLIB 字节码增强技术;而 `@Async` 则依赖于特定的任务执行器(TaskExecutor)。这两种代理模式之间存在冲突,使得两者不能很好地协同工作[^2]。 2. **线程上下文隔离** 当一个带有 `@Async` 的方法被执行时,它会在一个新的线程中运行。然而,默认情况下,Spring 的事务传播机制是在当前线程范围内有效的。因此,一旦进入新的线程环境,原有的事务上下文就会丢失,从而导致即使发生了异常也无法触发回滚操作[^3]。 3. **异常捕捉范围有限** 如果在异步方法内部发生的异常被捕获并吞掉了,则外部主线程无从知晓这些异常的发生情况,进而无法做出相应的反应如回滚等动作[^4]。 ```java @Service public class AsyncService { @Async @Transactional(rollbackFor = Exception.class) public void asyncMethodWithTransaction() { // Some database operations here... throw new RuntimeException("Simulated exception"); } } ``` 上述代码片段展示了尝试将两个注解组合使用的错误实践案例。尽管指定了 `rollbackFor=Exception.class` 参数试图让任何类型的未检查异常都能引起回滚,但由于以上提到的技术限制,实际效果往往不如人意。 --- ### 解决方案建议 为了使 `@Transactional` 能够正常运作并与 `@Async` 协同合作,可以采取以下几种措施: 1. **分离职责** 将涉及数据库交互的操作放在同步的服务层方法里,并在此处添加 `@Transactional` 注解。而在另一个单独的方法上调用此服务层方法的同时加上 `@Async` 来实现真正的异步化处理流程。 2. **调整事务边界** 修改业务逻辑设计,确保所有必要的持久化变更都在同一个非异步的方法体内完成,这样就可以保证整个过程处于同一事务控制之下。 3. **利用编程式事务管理** 对于某些复杂场景下难以通过声明式的手段解决问题的情形,考虑改用编程的方式来显式开启/提交/回滚事务,以便更灵活地应对多变的需求条件。 ```java @Service public class TransactionalService { private final PlatformTransactionManager transactionManager; @Autowired public TransactionalService(PlatformTransactionManager transactionManager){ this.transactionManager = transactionManager; } @Async public Future<Boolean> performOperationInNewThread(){ DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(definition); try{ // Execute some business logic that requires a transaction transactionManager.commit(status); return CompletableFuture.completedFuture(true); }catch(Exception e){ transactionManager.rollback(status); throw e; } } } ``` 这段示例说明了如何借助编程方式创建和管理事务状态,以适应更为复杂的并发处理需求。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值