✅P285_商城业务-分布式事务-本地事务隔离级别-传播行为等复习

gong_yz大约 7 分钟谷粒商城

一、事务的基本性质

数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation)和持久性(Durabilily),简称就是 ACID;

  • 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败
  • 一致性:数据在事务的前后,业务整体一致。
    • 转账。A:1000;B:1000; 转 200 事务成功; A:800 B:1200
  • 隔离性:事务之间互相隔离。
  • 持久性:一旦事务成功,数据一定会落盘在数据库。

在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据表,一旦有异常,我们可以很容易的整体回滚;

  • Business:我们具体的业务代码
  • Storage:库存业务代码;扣库存
  • Order:订单业务代码;保存订单
  • Account:账号业务代码;减账户余额

比如买东西业务,扣库存,下订单,账户扣款,是一个整体;必须同时成功或者失败

一个事务开始,代表以下的所有操作都在同一个连接里面;

二、事务的隔离级别

2.1 READ UNCOMMITTED(读未提交)

该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。

2.2 READ COMMITTED(读提交)

一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle 和 SQL Server 的默认隔离级别。

2.3 REPEATABLE READ(可重复读)

该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL的 InnoDB 引擎可以通过 next-key locks 机制(参考下文"行锁的算法"一节)来避免幻读。

2.4 SERIALIZABLE(序列化)

在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。

三、事务的传播行为

  1. PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
  2. PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
  3. PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
  4. PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
  5. PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  6. PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  7. PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。

四、案例演示

案例一:

方法B()和方法A()共用一个事务,方法C则创建一个新事务,若出现异常则方法B()和方法A()会回滚,方法C()则不会

案例二:

方法B()设置了事务的超时时间,但是方法B()和方法A()共用方法A()的事务,因此,以方法A设置的超时时间为准。


五、SpringBoot事务

事务失效的原因:绕过了代理

5.1 未启用事务

@EnableTransactionManagement 注解用来启用spring事务自动管理事务的功能,这个注解不要忘记写

5.2 方法不是public类型

@Transaction 可以用在类上、接口上、public方法上,如果将@Trasaction用在了非public方法上,事务将无效

5.3 数据源未配置事务管理器

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

5.4 自身调用问题

Spring是通过Aop的方式,对需要Spring管理事务的bean生成了代理对象,然后通过代理对象拦截了目标方法的执行,在方法前后添加了事务的功能,所以必须通过代理对象调用目标方法的时候,事务才会起效。

看下面代码,大家思考一个问题:当外部直接调用m1的时候,m2方法的事务会生效么?

@Component
public class UserService {
    public void m1(){
        this.m2();
    }
    
    @Transactional
    public void m2(){
        //执行db操作
    }
}

显然不会生效,因为m1中通过this的方式调用了m2方法,而this并不是代理对象,this.m2()不会被事务拦截器,所以事务是无效的,如果外部直接调用通过UserService这个bean来调用m2方法,事务是有效的,上面代码可以做一下调整,如下,m1在UserService中注入了自己,此时会产生更为严重的问题:循环依赖

@Component
public class UserService {
    @Autowired //@1
    private UserService userService;
 
    public void m1() {
        this.userService.m2();
    }
 
    @Transactional
    public void m2() {
        //执行db操作
    }
}

5.5 异常类型错误

Spring事务回滚的机制:对业务方法进行try catch,当捕获到有指定的异常时,spring自动对事务进行回滚,那么问题来了,哪些异常spring会回滚事务呢?

并不是任何异常情况下,Spring都会回滚事务,默认情况下,RuntimeException和Error的情况下,Spring事务才会回滚。

也可以自定义回滚的异常类型(需继承RuntimeException):@Transactional(rollbackFor = {异常类型列表})

5.6 异常被吞噬

当业务方法抛出异常,Spring感知到异常的时候,才会做事务回滚的操作,若方法内部将异常给吞了,那么事务无法感知到异常了,事务就不会回滚了。

如下代码,事务操作2发生了异常,但是被捕获了,此时事务并不会被回滚

@Transactional
public void m1(){
    事务操作1
    try{
        事务操作2,内部抛出了异常
    }catch(Exception e){
        
    }
}

5.7 业务和spring事务代码必须在一个线程中

Spring事务实现中使用了ThreadLocal,ThreadLocal大家应该知道吧,可以实现同一个线程中数据共享,必须是同一个线程的时候,数据才可以共享,这就要求业务代码必须和Spring事务的源码执行过程必须在一个线程中,才会受Spring事务的控制,比如下面代码,方法内部的子线程内部执行的事务操作将不受m1方法上Spring事务的控制,这个大家一定要注意

@Transactional
public void m1() {
    new Thread() {
        一系列事务操作
    }.start();
}

5.8 解决方案

本地事务失效的原因:同一个对象内事务方法互相调用默认失效,原因绕过了代理对象,事务使用代理对象来控制

解决:使用代理对象来调用事务方法

方法B()和方法C()的事务属性设置会失效,原因是绕过了代理,SpringBoot的事务是通过AOP代理实现的

解决事务失效:

  1. 引入aspectj依赖
 <!-- 导入aspectj依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 开启aspectj动态代理功能
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class CfmallOrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(CfmallOrderApplication.class, args);
    }
}
  1. 本类互调用对象
@Transactional(timeout = 30)
public void A() {
    OrderServiceImpl service = (OrderServiceImpl) AopContext.currentProxy();
    service.B();
    service.C();
    int i = 10 / 0;
}

@Transactional(propagation = Propagation.REQUIRED, timeout = 20)
public void B() {

}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void C() {

}