详解Spring事务

详解Spring事务

事务概要

事务(Transaction)是指在编程中一系列操作组成的单个逻辑工作单元,要么全部执行成功,要么全部失败回滚,保证数据的一致性、完整性和安全性

在编程中事务是什么意思?

在编程中,事务是一组操作的集合,这些操作被视为单个、原子性的操作。它们必须全部成功或全部失败,不能部分执行。事务确保了数据的一致性和完整性,以及防止了数据损坏和丢失。事务一般按照ACID原则进行设计,即原子性、一致性、隔离性和持久性。

ACID原则

  • 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
  • 事务隔离(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

Spring事务是什么?

Spring事务是指Spring框架提供的一套对于数据库事务进行管理的技术解决方案。它主要通过对于数据库的事务进行控制,实现对于数据库操作的原子性、一致性、隔离性和持久性的保障。Spring事务通过对于事务的切入(AOP)来实现对于事务的管理,既能够支持编程式实现事务控制,也能够支持声明式实现事务控制。同时Spring事务还具有可扩展性和可定制性,能够适应不同的应用场景和业务需求,是一种常用的事务管理解决方案。

Spring的事务是什么意思?

Spring的事务是一种机制,用于管理和控制数据库操作的一系列操作。它通过确保数据库操作的原子性、一致性、隔离性和持久性来维护数据完整性。Spring事务管理支持声明式事务管理和编程式事务管理。它可以与各种关系型数据库配合使用,并且可以扩展到非关系型数据库。通过使用Spring的事务管理,我们可以确保应用程序中的所有数据库操作都是可以恢复的,在数据库操作出现问题时可以回滚到先前的状态。

Spring事务支持

Spring事务管理支持声明式事务管理编程式事务管理,前者最常见,通常情况下只需要一个 @Transactional 就搞定了(代码侵入性降到了最低),如下例所示:

/**
 * 模拟转账
 */
@Transactional
public void handle() {
 // 转账
 transfer(double money);
 // 减自己的钱
  Reduce(double money);
}

声明式事务和编程式事务有什么区别?

  • 声明式事务需要使用特定的语言或框架来声明事务,在代码中不需要显式的指定事务的开启、提交或回滚操作,而是由框架或容器来自动管理。常用的声明式事务包括Spring中的事务管理器、EJB中的容器管理事务等。
  • 编程式事务需要在代码中显式的开启、提交或回滚事务,并指定事务相应的属性和操作,通常会使用try-catch块和事务管理器的API来管理事务。这种方式相对于声明式事务更加灵活,但也更容易出错和造成代码臃肿。

总的来说,声明式事务更加简洁易用,适合处理简单的事务场景,而编程式事务更加灵活,适合处理复杂的事务场景,但也需要更多的开发工作。声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。

编程式事务管理

编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。比如说,使用 TransactionTemplate 来管理事务:

@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                try {
                    // ....  业务代码
                } catch (Exception e){
                    //回滚
                    transactionStatus.setRollbackOnly();
                }
            }
        });
}

比如说,使用 TransactionManager 来管理事务:

@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
  TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
          try {
               // ....  业务代码
              transactionManager.commit(status);
          } catch (Exception e) {
              transactionManager.rollback(status);
          }
}

就编程式事务管理而言,Spring 更推荐使用 TransactionTemplate。在编程式事务中,必须在每个业务操作中包含额外的事务管理代码,就导致代码看起来非常的臃肿,但对理解 Spring 的事务管理模型非常有帮助。

声明式事务管理

声明式事务将事务管理代码从业务方法中抽离了出来,以声明式的方式来实现事务管理,对于开发者来说,声明式事务显然比编程式事务更易用、更好用。

要想实现事务管理和业务代码的抽离,就必须得用到 Spring 当中的AOP,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。

事务管理模型

Spring 将事务管理的核心抽象为一个事务管理器(TransactionManager),它的源码只有一个简单的接口定义,属于一个标记接口:

public interface TransactionManager {

}

该接口有两个子接口,分别是编程式事务接口 ReactiveTransactionManager声明式事务接口 PlatformTransactionManager

我们来重点说说 PlatformTransactionManager,该接口定义了 3 个接口方法:

interface PlatformTransactionManager extends TransactionManager{
    // 根据事务定义获取事务状态
    TransactionStatus getTransaction(TransactionDefinition definition)throws TransactionException;
    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;
    // 事务回滚
    void rollback(TransactionStatus status) throws TransactionException;
}

通过 PlatformTransactionManager 这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

参数 TransactionDefinition 和 @Transactional 注解是对应的,比如说 @Transactional 注解中定义的事务传播行为、隔离级别、事务超时时间、事务是否只读等属性,在 TransactionDefinition 都可以找得到。

返回类型 TransactionStatus 主要用来存储当前事务的一些状态和数据,比如说事务资源(connection)、回滚状态等。

TransactionDefinition代码如下:

public interface TransactionDefinition {

 // 事务的传播行为
 default int getPropagationBehavior() {
  return PROPAGATION_REQUIRED;
 }

 // 事务的隔离级别
 default int getIsolationLevel() {
  return ISOLATION_DEFAULT;
 }

  // 事务超时时间
  default int getTimeout() {
  return TIMEOUT_DEFAULT;
 }

  // 事务是否只读
  default boolean isReadOnly() {
  return false;
 }
}

Transactional注解代码如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
 Propagation propagation() default Propagation.REQUIRED;
 Isolation isolation() default Isolation.DEFAULT;
  int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
  boolean readOnly() default false;
}
  • @Transactional 注解中的 propagation 对应 TransactionDefinition 中的 getPropagationBehavior,默认值为 Propagation.REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED)。
  • @Transactional 注解中的 isolation 对应 TransactionDefinition 中的 getIsolationLevel,默认值为 DEFAULT(TransactionDefinition.ISOLATION_DEFAULT)。
  • @Transactional 注解中的 timeout 对应 TransactionDefinition 中的 getTimeout,默认值为TransactionDefinition.TIMEOUT_DEFAULT。
  • @Transactional 注解中的 readOnly 对应 TransactionDefinition 中的 isReadOnly,默认值为 false。

事务隔离

事务隔离是指在数据库事务并发执行的过程中,为了保证数据的一致性、稳定性和可靠性,将多个事务相互隔离,保证它们不会互相干扰。

事务隔离分为 4 种不同的级别:

隔离级别 说明
未提交读(Read uncommitted) 最低的隔离级别,允许“脏读”(dirty reads),事务可以看到其他事务“尚未提交”的修改。如果另一个事务回滚,那么当前事务读到的数据就是脏数据。
提交读(Read committed) 一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。
可重复读(Repeatable read) 一个事务可能会遇到幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。
串行化(Serializable) **最严格的隔离级别,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。**虽然 Serializable 隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用 Serializable 隔离级别。

事务传播行为

当事务方法被另外一个事务方法调用时,必须指定事务应该如何传播,例如,方法可能继续在当前事务中执行,也可以开启一个新的事务,在自己的事务中执行。
声明式事务的传播行为可以通过 @Transactional 注解中的 propagation 属性来定义,比如说:

@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {
}

TransactionDefinition 一共定义了 7 种事务传播行为:

PROPAGATION_REQUIRED

这是@Transactional 默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。

如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚。

PROPAGATION_REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。

也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会开启自己的事务,且开启的事务与外部的事务相互独立,互不干扰。

当类A中的 a 方法用默认 Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停a方法的事务 ,总结就是a不影响b,b影响a

PROPAGATION_NESTED(嵌套的)

表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与REQUIRED一样

当类A中的 a 方法用默认 Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.NESTED模式,然后在a方法里调用b方法操作数据库,然而b方法抛出异常后,a方法是不用回滚 ,总结就是b不影响a,a影响b。

PROPAGATION_SUPPORTS

如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

PROPAGATION_NOT_SUPPORTED(不支持事务)

表示该方法不应该运行在事务中,如果存在当前事务,在该方法运行期间,当前事务将被挂起。

PROPAGATION_MANDATORY(强制性的)

表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常。

PROPAGATION_NEVER(不会运行在有事务的环境)

以非事务方式运行,如果当前存在事务,则抛出异常。

Spring事务的传播是什么意思?举个例子说明一下

Spring事务的传播(Propagation)指的是当多个事务方法相互调用时,如何确定事务的边界和处理方式的规则。

举个例子,假设有一个服务类A,里面有两个方法:methodAmethodBmethodAmethodB 都有可能涉及到数据库的操作,并需要被事务管理。在没有指定事务传播的情况下,当 methodA 中开启一个事务并执行一些操作后,若 methodB 被调用,它应该如何处理事务?

  • REQUIRED:默认传播方式为 REQUIRED,指的是 methodB 会加入 methodA 的事务中,如果 methodA 没有开启事务,那么 methodB 会创建一个新的事务,并在执行完后提交。
  • REQUIRES_NEW:如果将事务传播方式设置为 REQUIRES_NEW,那么 methodB 将会开启一个新的事务,和 methodA 的事务没有关系。
  • SUPPORTS:如果将事务传播方式设置为 SUPPORTS,那么 methodB 不会开启新的事务,而是检查是否已有事务存在,如果有就加入,没有就以非事务方式执行。
  • NEVER:如果将事务传播方式设置为 NEVER,那么 methodB 不允许在事务中运行。如果 methodA 已经开启了事务,那么 methodB 将会失败。

除此之外,Spring 还定义了其他几种事务传播方式,如 NOT_SUPPORTEDMANDATORYNESTED 等。不同的事务传播方式有不同的应用场景,合理设置可以提高代码的可维护性和性能。

事务的超时时间

事务超时timeout,也就是指一个事务所允许执行的最长时间,如果在超时时间内还没有完成的话,就自动回滚。
假如事务的执行时间格外的长,由于事务涉及到对数据库的锁定,就会导致长时间运行的事务占用数据库资源。

事务的只读属性

事务的只读属性readOnly, 如果一个事务只是对数据库执行读操作,那么该数据库就可以利用事务的只读属性,采取优化措施,适用于多条数据库查询操作中。

为什么一个查询操作还要启用事务支持呢?

这是因为 MySql(innodb)默认对每一个连接都启用了 autocommit 模式,在该模式下,每一个发送到 MySql 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务。
那如果我们给方法加上了 @Transactional 注解,那这个方法中所有的 SQL 都会放在一个事务里。否则,每条 SQL 都会单独开启一个事务,中间被其他事务修改了数据,都会实时读取到。
有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条 SQL 查询后,被其他用户改变了数据,那么下一个 SQL 查询可能就会出现不一致的状态。

事务的回滚策略

**回滚策略rollbackFor,用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。**默认情况下,事务只在出现运行时异常(Runtime Exception)时回滚,以及 Error,出现检查异常(checked exception,需要主动捕获处理或者向上抛出)时不回滚。

如果想要回滚特定的异常类型的话,可以这样设置:

@Transactional(rollbackFor= MyException.class)

Spring事务失效

Spring事务失效指在使用Spring进行事务管理时,事务没有被正确地提交或回滚所致的问题。可能是因为代码中未正确地配置事务管理器或者事务注解的使用不正确所导致的。如果事务失效,则数据库操作可能未能正确地更新或回滚,可能会造成不可预知的结果,从而影响系统的稳定性和一致性。

1 访问权限问题:

Java的访问权限有4种:privatedefaultprotectedpublic,它们的权限从左到右,以此变大。如果在开发中,将事务方法定义了错误的访问权限,则事务功能会失效。

@Service
public class EmpService {
 
    @Transactional
    private void add(UserModel userModel){
        saveData(userModel);
    }
}

如上:add方法的权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须是public的。

在Spring源码中,如果目标方法不是public,则TransactionAttribute返回null,不支持事务。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dAy16QoI-1691073054985)(C:\Users\HUAWEI\AppData\Roaming\Typora\typora-user-images\image-20230329153942709.png)]


2 方法用final修饰

方法被final修饰时,也会导致事务失效,如下的add方法使用了final修饰,造成事务失效

@Service
public class EmpService {
 
    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
    }
}

因为Spring事务底层是用了AOP,用了JDK的动态代理或者CGLB的动态代理,会帮我们生成代理类,在代理类中实现事务功能。

如果某个方法被final修饰了,那么在代理类中,就无法重新该方法,而添加事务功能。

注意:如果某个方法被static修饰,同样也无法通过动态代理,变成事务方法。


3 直接调用内部方法

@Service
public class EmpService {
 
    @Transactional
    public void add(UserModel userModel){
        saveData(userModel);
        updateSataus(userModel);
    }
 
 
    @Transactional
    public void updateSataus(UserModel userModel){
        doSomething();
    }
}

在事务方法add可知,它直接调用了updateStatus方法,方法拥有事务的能力是因为Spring AOP中生成了代理对象,但是直接调用updateStatus方法不会直接生成事务。但是可以直接将该类直接注入进来,比如:

@Service
public class EmpService {
 
    private EmpService empService;
 
    @Transactional
    public void add(UserModel userModel){
        saveData(userModel);
        empService.updateSataus(userModel);
    }
 
 
    @Transactional(rollbackFor = Exception.class)
    public void updateSataus(UserModel userModel){
        doSomething();
    }
}

这样事务就生效了,也不会穿生循环依赖的问题。


4 未被Spring管理

如下所示:EmpService类没有交给Spring进行管理(没添加@Service等注解),导致事务失效

public class EmpService {
    @Transactional
    public void add(UserModel userModel){
        saveData(userModel);
        updateSataus(userModel);
    }
}

5 多线程调用

由以下代码可知:在add事务方法里面,调用了updateStatus事务方法,但是updateStatus事务方法是在另外一个线程中调用的。这样就导致了两个方法不在同一个线程中,获取到了数据库连接不一样,从而是两个不同的事务,如果updateStatus方法中抛出了异常,add方法是不会回滚的

@Service
public class EmpService {
 
    @Autowired
    private OrderService orderService;
 
 
    @Transactional
    public void add(UserModel userModel){
 
        new Thread(()->{
            orderService.updateSataus();
        }).start();
    }
 
}
 
@Service
public class OrderService{
 
    @Transactional
    public void updateSataus(){
        System.out.println("======================");
    }
 
}

Spring的事务是通过数据库的连接来实现的当前线程中保存了一个map,key是数据源,value是数据库连接。同一个事务,指同一个数据库连接,只有拥有同一个事务连接才能保证同时提交和回滚。如果是不同的线程,拿到的数据库连接肯定是不同的。


6 表不支持事务

如果表的引擎是myisam,那么它是不支持事务的,要想支持事务,改成innodb引擎


7 事务没有开启

如果是Spring Boot项目,那么是事务默认是开启的,但如果是Spring项目,需要xml配置


8 事务的传播特性

如果事务的传播特性设置错了,事务也会失效

如下:Propagation.NEVER这种类型的传播特性不支持事务,如果有事务会抛出异常。

@Service
public class EmpService {
 	//Propagation.NEVER传播特性不支持事务
    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel){
        saveData(userModel);
        updateSataus(userModel);
    }
 
}

目前只有这三种传播特性才会创建新事物:REQUIREDREQUIRES_NEWNESTED


9 自己吞了异常

事务不会回滚,最常见的问题是:开发者在代码中手动try…catch了异常

@Service
public class EmpService {
    @Transactional
    public void add(UserModel userModel){
        try {//开发者手动捕获了异常,导致事务失效
            saveData(userModel);
            updateSataus(userModel);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这种情况下,因为开发者自己捕获了异常、又没有手动抛出


10 手动抛了别的异常

如果抛的异常不正确,事务也不会回滚

@Service
public class EmpService {
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
            saveData(userModel);
            updateSataus(userModel);
        } catch (Exception e) {
            throw new Exception(e);
        }
    }
}

因为Spring事务,默认情况下只会回滚RuntimeException(运行时异常)Error(错误),对于普通的非运行时异常,它不会回滚。


11 自定义回滚异常

如果在使用@Transactional注解声明事务时,有时想自定义回滚异常,Spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。如下:

@Service
public class EmpService {
    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) {
            saveData(userModel);
            updateSataus(userModel);
    }
}

但是如果在程序执行过程中,出现了sql异常,但是sql异常并不属于我们定义的BusinessException异常,所以事务也不会回滚

Spring事务的实现原理

Spring事务的实现原理基于AOP(面向切面编程)和代理模式。具体来说,在使用@Transactional注解时,Spring会通过AOP动态代理机制,为目标对象生成一个代理对象,对目标方法进行拦截,以实现对事务的控制。

当方法上使用@Transactional注解时,Spring会将该方法标记为事务方法,生成一个连接点(JoinPoint),并根据配置文件中的事务管理器(Transaction Manager)创建并绑定一个事务Context对象。在方法执行过程中,如果发生异常,则Spring会回滚事务。如果方法执行成功,则Spring会提交事务。

在同时存在多个@Transactional注解的情况下,Spring会根据注解的属性设置来确定事务的传播行为(Propagation)隔离级别(Isolation)。例如REQUIRED传播属性表示将当前方法加入到已经存在的事务中,如果不存在事务,则开启新事务;SERIALIZABLE隔离级别表示事务期间需要对数据进行串行化操作。

其实事务操作是AOP的一个核心体现,当一个方法添加@Transactional注解之后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当使用这个代理对象的方法的时候,如果有事务处理,那么会先把事务的自动提交给关系,然后去执行具体的业务逻辑,如果执行逻辑没有出现异常,那么代理逻辑就会直接提交如果出现任何异常情况,那么直接进行回滚操作,当然用户可以控制对哪些异常进行回滚操作

总的来说,Spring事务的实现原理是基于代理模式和AOP动态代理机制,并且可以根据不同的注解属性来确定事务的传播行为和隔离级别。

你可能感兴趣的:(后端,SpringBoot,Spring,spring,数据库,后端)