五、Spring事务原理

五、Spring事务原理

    1、事务四大特性(ACID)

  • 原子性(Atomicity):一个事务中的所有操作,要么都完成,要么都不执行。对于一个事务来说,不可能只执行其中的一部分。

  • 一致性(Consistency):数据库总是从一个一致性的状态转换到另外一个一致性状态,事务前后数据的完整性必须保持一致。。

  • 隔离性(Isolation):一个事务所做的修改在最终提交以前,对其它事务是不可见的,多个事务之间的操作相互不影响。

  • 持久性(Durability):持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

    2、事务隔离级别

        (1)、Read Uncommitted(读取未提交内容):一个事务可以看到其他事务已执行但是未提交的结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少,并且存在脏读问题。

        (2)、Read Committed(读取已提交内容):一个事务只能看到其他事务已执行并已提交的结果(Oracle、SQL Server默认隔离级别)。这种隔离级别支持不可重复读,因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

        (3)、Repeatable Read(可重读):同一事务的多个实例在并发读取数据时,会看到同样的数据行(MySQL的默认事务隔离级别)。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了不可重复读问题,存在幻读问题。

        (4)、Serializable(可串行化):最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

  • 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据就是脏数据

  • 不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。

  • 幻读:A事务在执行过程中,B事务插入了符合A事务查询条件的数据,导致A事务两次读取的数据不一致。

        不可重复读和幻读的区别:

            不可重复读是由update引起的,需要用锁行来避免;幻读是由insert或delete引起的,需要用锁表来避免。

√:可能出现    ×:不会出现

事务隔离级别

脏读

不可重复读

幻读

Read Uncommitted

Read Committed

×

Repeatable Read

×

×

Serializable

×

×

×

        (5)、注:每降低一个事务隔离级别都能提高数据库的并发

            ①、读未提交:其它事务未提交就可以读

            ②、读已提交:其它事务只有提交了才能读

            ③、可重复读:只管自己启动事务时候的状态,不受其它事务的影响(mysql默认)

            ④、事务串行:按照顺序提交事务保证了数据的安全性,但无法实现并发

    3、事务的关键对象

        (1)、PlatformTransactionManager:事务管理器,是用来管理事务的操作,它保存着当前的数据源连接,对外提供对该数据源的事务提交回滚操作接口,同时实现了事务相关操作的方法。一个数据源DataSource需要一个事务管理器。它只包含三个方法:获取事务;回滚事务;提交事务。

            根据底层所使用的不同的持久化API或框架,使用如下:

            ①、DataSourceTransactionManager:适用于使用JDBC和iBatis进行数据持久化操作的情况,在定义时需要提供底层的数据源作为其属性,也就是DataSource。

            ②、HibernateTransactionManager:适用于使用Hibernate进行数据持久化操作的情况,与HibernateTransactionManager对应的是SessionFactory。

            ③、JpaTransactionManager:适用于使用JPA进行数据持久化操作的情况,与JpaTransactionManager对应的是EntityManagerFactory。

        (2)、TransactionDefinition:定义事务的类型,事务包含很多属性,比如是否可读,事务隔离级别,事务传播属性,超时时间等。通过事务的定义,我们根据定义获取特定的事务。DefaultTransactionDefinition:TransactionDefinition的一个实现类。就是对上述属性设置一些默认值,默认的传播特性为PROPAGATION_REQUIRED,隔离级别为ISOLATION_DEFAULT。

public interface TransactionDefinition {
    /**
      * 事务隔离级别
     */
    //数据库默认的事务隔离级别
    int ISOLATION_DEFAULT = -1;
    //读未提交
    int ISOLATION_READ_UNCOMMITTED = 1;
    //读已提交
    int ISOLATION_READ_COMMITTED = 2;
    //可重复读    
    int ISOLATION_REPEATABLE_READ = 4;
    //串行执行
    int ISOLATION_SERIALIZABLE = 8;
    /**
      * 事务传播属性
     */
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    int TIMEOUT_DEFAULT = -1;
    ...
}

            ①、事务隔离级别

                a、isolation_default:是PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。Mysql在InnoDB引擎下默认ISOLATION_REPEATABLE_READ;Oracle默认ISOLATION_READ_COMMITTED。

                b、isolation_read_uncommitted:事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。

                c、isolation_read_committed:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻读。

                d、isolation_repeatable_read:该事务隔离级别可以防止脏读,不可重复读。但是可能出现幻读。

                e、isolation_serializable:是代价最高但最可靠的事务隔离级别。事务被处理为顺序执行。

            ②、事务传播属性

                示例前提:外层事务ServiceA的MethodA()调用内层ServiceB的MethodB()

                a、propagation_required:如果存在一个事务,则支持当前事务,如果当前没有事务,就新建一个事务。这通常是一个事务定义的默认设置,并且通常限定了事务同步范围,是Spring默认的事务的传播。

                    示例:如果ServiceB.methodB()的事务级别定义为PROPAGATION_REQUIRED,那么执行ServiceA.methodA()的时候spring已经起了事务,这时调用ServiceB.methodB(),ServiceB.methodB()看到自己已经运行在ServiceA.methodA()的事务内部,就不再起新的事务。假如ServiceB.methodB()运行的时候发现自己没有在事务中,他就会为自己分配一个事务。这样,在ServiceA.methodA()或者在ServiceB.methodB()内的任何地方出现异常,事务都会被回滚。

                b、propagation_requires_new:总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。新建的事务将和被挂起的事务没有任何关系,是两个独立的事务,外层事务失败回滚之后,不能回滚内层事务执行的结果,内层事务失败抛出异常,外层事务捕获,也可以不处理回滚操作。

                    示例:如果ServiceA.methodA()的事务级别定义为PROPAGATION_REQUIRED,ServiceB.methodB()的事务级别为PROPAGATION_REQUIRES_NEW。那么当执行到ServiceB.methodB()的时候,ServiceA.methodA()所在的事务就会挂起,ServiceB.methodB()会起一个新的事务,等待ServiceB.methodB()的事务完成以后,它才继续执行。

                    PROPAGATION_REQUIRES_NEW与PROPAGATION_REQUIRED的事务区别:

                        两者区别在于事务的回滚程度,因为ServiceB.methodB()是新起一个事务,那么就是存在两个不同的事务。如果ServiceB.methodB()已经提交,那么ServiceA.methodA()失败回滚,ServiceB.methodB()不会回滚。如果ServiceB.methodB()失败回滚,如果他抛出的异常被ServiceA.methodA()捕获,ServiceA.methodA()事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。

                c、propagation_supports:如果存在一个事务,则支持当前事务,如果当前没有事务,就以非事务方式执行。注意对于具有事务同步的事务管理器,PROPAGATION_SUPPORTS与完全没有事务稍有不同,因为它定义了同步可能适用的事务范围。

                    示例:如果ServiceB.methodB()的事务级别为PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。

                d、propagation_not_supported:总是以非事务地执行,如果当前存在事务,则挂起任何存在的事务

                    示例:如果ServiceB.methodB()的事务级别为PROPAGATION_NOT_SUPPORTED,执行ServiceA.methodA()方法,当执行到ServiceB.methodB()的时候,如果ServiceA.methodA()有事务,则会挂起ServiceA.methodA()的事务,当执行完ServiceB.methodB()方法的时候,ServiceA.methodA()方法继续以事务的方式执行。

                    示例:如果ServiceB.methodB()的事务级别为PROPAGATION_MANDATORY,那么当执行ServiceA.methodA()执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则ServiceB.methodB()沿用该事务,如果没有,则会抛出异常。

                e、propagation_mandatory:如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。MANDATORY是强制的,命令的意思,放在这里就是强制需要一个事务。

                f、propagation_never:总是非事务地执行,如果存在一个活动事务,则抛出异常

                    示例:如果ServiceB.methodB()的事务级别为PROPAGATION_NEVER,执行ServiceA.methodA()方法,当执行到ServiceB.methodB()的时候,如果有事务,则抛出异常,如果没有则以非事务方式执行。

                d、propagation_nested:如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。

                    示例:如果ServiceB.methodB()的事务级别为PROPAGATION_REQUIRED,执行ServiceA.methodA()方法,当执行到ServiceB.methodB()的时候,如果ServiceA.methodA()方法有事务,则会创建一个依赖于当前事务的嵌套事务,如果ServiceB.methodB()执行失败,只会回滚ServiceB.methodB(),不会回滚ServiceA.methodA(),只有当ServiceA.methodA()执行完成后才会提交ServiceB.methodB()的事务,因为嵌套事务不能单独提交;如果ServiceA.methodA()回滚了,则会导致内部嵌套事务的回滚。如果ServiceA.methodA()方法没有事务,就会新建一个事务。

        (3)、TransactionStatus:一个事务运行的状态,事务管理器通过状态可以知道事务的状态信息,然后进行事务的控制。事务是否完成,是否是新的事务,是不是只能回滚等。

    4、多线程事务

        问题:多线程底层连接数据库的时候,使用的线程变量(TheadLocal)。所以,开多少线程理论上就会建立多少个连接,每个线程有自己的连接,事务肯定不是同一个了。

        (1)、解决办法一:强制手动把每个线程的事务状态放到一个同步集合里面。然后如果有单个异常,循环回滚每个线程。

@Resource
private PlatformTransactionManager transactionManager;

private void mainFun(List<DoctorAdviceDto> doctorAdviceDtoList){
    List<Future<String>> futureList = new ArrayList<>();
    AtomicBoolean rollbackFlag = new AtomicBoolean(false);
    CountDownLatch subThreadLatch = new CountDownLatch(1);
    CountDownLatch mainThreadLatch = new CountDownLatch(doctorAdviceDtoList.size());
    doctorAdviceDtoList.stream().forEach(doctorAdviceDto -> {
        Future<String> future = threadPoolUtils.submit(() -> {
            DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
            defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
            TransactionStatus status = transactionManager.getTransaction(defaultTransactionDefinition);
            try {
                //子线程开始处理自有业务
                //...
                //业务处理结束
            }catch (Exception e){
                //子线程异常,rollbackFlag修改值为true
                rollbackFlag.set(true);
                //subThreadLatch为0,解除各个子线程阻塞
                subThreadLatch.countDown();
                return e.getMessage();
            }finally {
                //子线程运行,mainThreadLatch执行countDown减1,将控制权交给主线程
                mainThreadLatch.countDown();
                //子线程阻塞,等待其他子线程执行完
                subThreadLatch.await(10, TimeUnit.SECONDS);
                if (rollbackFlag.get()) {
                    transactionManager.rollback(status);
                } else {
                    transactionManager.commit(status);
                }
            }
            return null;
        });
        futureList.add(future);
    });
    if (!rollbackFlag.get()) {
        try {
            //主线程阻塞,如果mainThreadLatch为0或await超过10秒,继续往下运行
            mainThreadLatch.await(10, TimeUnit.SECONDS);
            subThreadLatch.countDown();
        } catch (Exception e) {
            throw new ServiceException(e.getMessage());
        }
        //异常处理开始
        //...
        //异常处理结束
    }
}

        (2)、解决办法二:(测试好像不可行)

            场景:A类使用多线程来完成一个添加业务,所以这个类里面会有run()方法,然后再run()方法里面执行添加add()方法,完成业务实现。

            ①、不进行事务管理的时候,可以将添加add()方法放在A类里面,然后直接进行调用就可以。

            ②、当进行事务管理时候,如果将add()方法放在A类里面,事务Spring是不帮忙管理的,所以需要将add()方法放到B类里面,然后将B类注入到A类里面,这样我们就相当于将B类交给Spring管理类,这样Spring就会帮处理事务了,就可以使用@Transaction注解控制事务了,B类的add()方法也需要添加@Transaction注解。

    5、事务实现原理

        Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring是无法提供事务功能的。Spring在启动的时候会去解析生成相关的Bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理类中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

    6、@Transactional注解的相关问题

        ServiceA中有method1()方法、method2()方法、method3()方法;

        ServiceB中有method4()方法、method5()方法;

        (1)、Spring默认情况下会对unchecked异常进行事务回滚,对checked异常则不回滚。Java里面将派生于Error或者RuntimeException(如NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException)的异常称为unchecked异常(编译器不要求强制处置的异常)。其他继承自java.lang.Exception的异常统称为Checked Exception,如IOException、TimeoutException等(编译器要求必须处置的异常)。

            如果需要遇见Exception异常也回滚,需要进行标注。

@Transactional(rollbackFor=Exception.class)

        (2)、@Transactional注解只能应用到public的方法上,如果应用在protected、private或者package方法上,也不会报错,不过事务设置不会起作用。

        (3)、如果ServiceA中有method1()方法中调用了method2()方法和method3()方法,@Transactional注解只在method2()方法和method3()方法上标注,则事务不生效,需要标注在method1()方法上。

            因为Spring的事务是基于动态代理实现的,同一个类内这样调用的话,只有ServiceA的第一次调用会根据动态代理生成ProxyClass,之后类内调用是不带任何切面信息的方法本身,因为第一次调用没有标注@Transactional注解,所以不会调用Spring生成的代理对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值