隐蔽的事务失效...


欢迎关注公众号(通过文章导读关注),发送笔记可领取 Redis、JVM 等系列完整 pdf!
【11来了】文章导读地址:点击查看文章导读!

事务失效你了解吗?

隐蔽的事务失效..._第1张图片

事务介绍

当使用 SpringBoot 进行项目开发,如果需要使用事务的话,只需要通过在方法上添加注解 @Transactional 就可以开启该方法的事务执行

但是如果不正确地使用事务,会导致 SpringBoot 中的事务失效,如果没及时发现,可能导致严重的生产问题!

因此,在使用事务之前,需要了解事务在哪些场景下会失效,否则,如果事务失效可能会导致 数据不一致 问题的出现

事务传播类型

在学习事务失效之前,首先了解一下 事务的传播类型,在事务处理中,事务的传播类型(Propagation)是指在多个事务方法相互调用的情况下,事务如何进行传播和协调的方式。Spring 提供了多种事务传播类型,以便为不同的业务需求提供灵活的事务管理机制:

//如果有事务, 那么加入事务, 没有的话新建一个(默认)
@Transactional(propagation=Propagation.REQUIRED)
//容器不为这个方法开启事务 
@Transactional(propagation=Propagation.NOT_SUPPORTED)
//不管是否存在事务, 都创建一个新的事务, 原来的挂起, 新的执行完毕, 继续执行老的事务 
@Transactional(propagation=Propagation.REQUIRES_NEW) 
//必须在一个已有的事务中执行, 否则抛出异常
@Transactional(propagation=Propagation.MANDATORY) 
//必须在一个没有的事务中执行, 否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.NEVER) 
//如果其他bean调用这个方法, 在其他bean中声明事务, 那就用事务, 如果其他bean没有声明事务, 那就不用事务
@Transactional(propagation=Propagation.SUPPORTS) 

上边的这个是 Spring 中提供的事务的传播类型设置,还可以使用 isolation 设置底层数据库的事务隔离级别,这些就很熟悉了,在 MySQL 中就已经学过很多了:

// 读取未提交数据(会出现脏读, 不可重复读) 基本不使用
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
// 读取已提交数据(会出现不可重复读和幻读) 
@Transactional(isolation = Isolation.READ_COMMITTED)
// 可重复读(会出现幻读) MySQL默认
@Transactional(isolation = Isolation.REPEATABLE_READ)
// 串行化
@Transactional(isolation = Isolation.SERIALIZABLE)

事务失效场景

那么 Spring 是如何通过 @Transactional 为方法开启事务的呢?

底层原理就是通过 动态代理 技术,Spring 会创建一个代理对象,当执行被注解标注的方法时,是通过代理对象来执行的

在代理对象中,会在方法执行的前开启事务,并且当方法执行成功时将事务提交;如果方法抛出异常,代理对象对事务进行回滚

1.事务方法所在的类没有注册为 Spring Bean

既然需要 @Transactional 注解开启事务,那么 Spring 就需要可以扫描到这个注解,也就是 Spring 必须将 @Transactional 标注的方法所在类注册为 Spring 的 Bean,之后才可以扫描到这个注解,并且创建代理对象,如下是 错误示例

没有给 Service 添加 @Service 注解,Spring 没有管理到这个类,自然也就无法开启事务

public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void update(Goods goods) {
      // ...
    }
}

2.非 public 修饰的方法

如果一个方法被定义为 private了,那么同样事务会失效,因为在代理模式中只可以对 公共接口暴露的方法 进行代理拦截并且添加事务管理逻辑,如下是 错误示例

@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private void update(Goods goods) {
      // ...
    }
}

3.同一个类中的方法互相调用

如果是在同一个类中,比如 GoodsServiceImpl 类中有两个方法 A 和 B,B 是事务方法,如果在 A 方法中直接调用 B,会导致事务失效

这是因为,A 直接去调用 B,并没有走到动态代理对象的逻辑,也就是没有被动态代理所拦截添加事务操作,事务自然就失效了,错误示例如下:

@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
    @Override
    private void A(Goods goods) {
      update(goods);
    }
  
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private void update(Goods goods) {
      // ...
    }
}

4.异常被吞掉

如果在事务方法中,爆出异常,但是通过 try-catch 将异常捕捉,导致动态代理并没有感知到事务方法所出现的异常,所以事务也就没有办法回滚,导致事务失效,错误示例如下:

@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private void update(Goods goods) {
        try {
          ...
        } catch (Exception e) {
            // 自定义逻辑吞掉异常
            log.error("捕获异常: {}", e);
        }
    }
}

5.多线程调用不当

对下边这个例子来说,在 A 方法中,通过多线程调用了 sendMsg() 方法,但是执行 A() 的线程和执行 sendMsg() 的线程并不是同一个线程

而 Spring 中的事务是通过 ThreadLocal 保证线程安全的,将事务和当前线程绑定,那么多个线程就会导致事务失效,如果 sendMsg() 方法执行失败了,会导致 A 方法也无法回滚

@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
  
    @Transactional
    private void A(Goods goods) {
     new Thread(() -> {
          sendMsg();
      }).start();
    }
}
@Service
public class MessageServiceImpl{
  
    @Transactional
    private void sendMsg() {
        // ...
    }
}

你可能感兴趣的:(技术文章,Spring事务,Java)