Spring基础专题——第八章(事务)

目标,去年一年比较懒吧,所以今年我希望我的知识可以分享给正在奋斗中的互联网开发人员,以及未来想往架构师上走的道友们我们一起进步,从一个互联网职场小白到一个沪漂湿人,一路让我知道分享是一件多么重要的事情,总之不对的地方,多多指出,我们一起徜徉代码的海洋!
 
我这里做每个章节去说的前提,不是一定很标准的套用一些官方名词,目的是为了让大家可以理解更加清楚,如果形容的不恰当,可以留言出来,万分感激

回顾下上节说的内容,上节说到了连接对象来控制事务的提交,回滚,那么本节我们彻底研究下事务是什么,以及事务的本质?

1、什么是事务

保证业务操作完整性的一种数据库机制!!我们称为事务,注意,事务是数据库的机制,是由数据库来保证的,java代码仅仅是对这个机制的调用。

事务的四个特点:

  • A

事务的原子性(Atomicity)

  • C

事务的一致性(Consistency)

  • I

事务的隔离性(Isolation)

  • D

事务的持久性(Durability)

我先不解释这些具体的意思,后面我们慢慢理解,然后我再定义

 

2、如何控制事务

回顾下我们用JDBC的时候:用Connection.setAutoCommit(false);表示开启事务, Connection.commit();表示提交事务,Connection.rollback();表示回滚事务

对事务的控制,全部依附于连接对象Connection。

回顾下我们用Mybatis的时候:Mybatis会自动开启事务,Sqlsession.commit();表示提交事务,SqlSession.rollback();表示回滚事务

所以你发现对事务的控制,一定只有这三个方案,不管是什么框架!

我们知道,Mybatis底层,实际上是封装了Connection,这个不知道的话,可以先默认知道,后续我加Mybatis的教程说明下;

所以SqlSession底层也封装了Connection对象,SqlSession.commit()底层还是用的是Connection.commit();

结论:控制事务的底层,都是通过Connection对象来完成的

 

3、Spring如何控制事务

事务我们知道,是非业务核心功能,所以事务属于额外功能,既然是额外功能,就是可有可无,有,我就把你加上,没有,我就不加你

那么前几节我们知道额外功能的创建我们是通过AOP代理完成的,所以Spring是通过AOP完成事务的开发

既然是aop,开发步骤我们一样是4步:

  • 原始对象

就是我们所开发的一个一个Service,UserService

public class XXXUserServiceImpl{   private xxxDAO xxxDAO   set get
 
   1. 原始对象 --》原始方法 --》核心功能(业务处理+Dao调用)
   2. 需要调用Dao,就要依赖Dao,就需要通过成员变量,让Spring把Dao注入到Service的成员变量
}

 

  • 额外功能

前面我们对额外功能有两种处理方式,一个通过MethodInterceptor,实现里面的invoke方法,然后先放原始方法执行,通过invoke(MethodInvocation invocation)的invocation.proceed();执行原始方法

public Object invoke(MethodInvocation invocation) {
        try {
            Connection.setAutoCommit(false);
            Object ret = invocation.proceed();
            Connection.commit();
        } catch (Exception e) {
            Connection.rollback();
        }
        return ret;
    }
}

在方法执行前,设置开启事务,执行后,提交事务,只要出现异常,就回滚,所以用try catch抛出!!

另一种通过@Aspect注解的方式添加额外功能,不记得的往前翻翻aop章节!

 

第一种方法,是人为来书写,那么既然每个人都可以写,那么Spring就帮你把这个事情做了,你不需要再写了,给你封装了一个类叫做org.springframework.jdbc.datasource.DataSourceTransactionManager,这类的核心功能,就是控

制事务这个额外功能,封装的就是我们上面写的invoke这些代码,所以后续再应用Spring开发事务功能,这些代码你就不需要再写了!!!

有个细节要注意下,上面这段代码,控制事务的这些东西,是由Connection来帮你完成的,所以要想这段代码起作用,是不是就需要连接对象来支持??

所以DataSourceTransactionManager需要Connection对象,需要,就等于依赖,就要注入给DataSourceTransactionManager!还有就是我们要在创建连接的过程中,为了提高连接的效率,我们引入了连接池,希望通过连接池来帮我

我们管理连接,所以等效于DataSourceTransactionManager需要注入连接池!!

 

  • 切入点

Spring为我们提供了一个注解@Transactional,可以指定事务这个额外功能,到底加在哪些业务方法上

加入的位置有2个:类、方法

加类上,表示整个类的所有方法都会加入事务

加方法上,表示该方法会加入事务

 

  • 组装切面

切面=切点+额外功能

Spring组装通过一个标签来完成切面,就是

<tx:annotation-driven transaction-manager=""></tx:annotation-driven>

这个transaction-manager是用于获取上面的额外功能,而这个标签会扫描@Transactional这个注解,所以切入点有了,就可以进行组装。

所以这4步骤我就说完了,看完了肯定少不了编码给你们看!记得点赞三连

 

4、Spring控制事务编码

1、搭建开发环境

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-tx</artifactId>
   <version>5.1.14.RELEASE</version>
</dependency>

2、编码

原始对象UserServiceImpl

public interface UserService {
    void register(Users users);
}
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void register(Users users) {
        userDao.save(users);
    }
}

此时在UserServiceImpl这个属性UserDao是null的,我们需要通过配置文件为其进行赋值。

因为userDao是用户自定义类型,所以要用ref来指向由工厂帮我们创建的userDao这个代理对象,上节说过,如果你的dao接口是UserDao,则首字母小写,如果是UserDAO,则是userDAO~~

<!--    连接池-->
    <bean id="dataSource"
          class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver">
        </property>
        <property name="url" value="jdbc:mysql://localhost:3306/users?useSSL=false"></property>
        <property name="username" value="root"></property>
        <property name="password" value="527713"></property>
    </bean>
    <!--创建SqlsessionFactoryBean-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
        <!--        执行实体对应的包-->
        <property name="typeAliasesPackage" value="com.chenxin.spring5.mybatis.entity"></property>
        <property name="mapperLocations">
            <list>
                <value>classpath*:*Mapper.xml</value>
            </list>
        </property>
    </bean>

    <bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
        <!--        设置dao所在的包路径-->
        <property name="basePackage" value="com.chenxin.spring5.mybatis.dao"></property>
    </bean>

    <bean id="userService" class="com.chenxin.spring5.tx.service.UserServiceImpl">
        <property name="userDao" ref="userDao"></property>
    </bean>

额外功能的编写,上面说了Spring提供了DataSourceTransactionManager来处理,其中需要注入连接池,有了连接池,就有了连接,就可以控制事务了

    <!--    额外功能-->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

做个切入点

在类上进行@Transactional注解,等于把这个UserServiceImpl这个类作为切入点

@Transactional
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void register(Users users) {
        userDao.save(users);
    }
}

最后来个切面

整合切点+额外功能

<tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>

所以整体的applicationContext.xml

   <!--    连接池-->
    <bean id="dataSource"
          class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver">
        </property>
        <property name="url" value="jdbc:mysql://localhost:3306/users?useSSL=false"></property>
        <property name="username" value="root"></property>
        <property name="password" value="527713"></property>
    </bean>
    <!--创建SqlsessionFactoryBean-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
        <!--        执行实体对应的包-->
        <property name="typeAliasesPackage" value="com.chenxin.spring5.mybatis.entity"></property>
        <property name="mapperLocations">
            <list>
                <value>classpath*:*Mapper.xml</value>
            </list>
        </property>
    </bean>

    <bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
        <!--        设置dao所在的包路径-->
        <property name="basePackage" value="com.chenxin.spring5.mybatis.dao"></property>
    </bean>

    <bean id="userService" class="com.chenxin.spring5.tx.service.UserServiceImpl">
        <property name="userDao" ref="userDao"></property>
    </bean>

    <!--    额外功能-->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>


    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>

测试下

    @Test
    public void test10(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        com.chenxin.spring5.tx.service.UserService userService = (com.chenxin.spring5.tx.service.UserService) ctx.getBean("userService");

        Users users = new Users();
        users.setName("chenxin");
        users.setPassword("11111222333");
        userService.register(users);
    }

输出

2021-04-05 22:29:26 DEBUG DefaultListableBeanFactory:213 - Creating shared instance of singleton bean 'org.springframework.transaction.interceptor.TransactionInterceptor#0'
2021-04-05 22:29:26 DEBUG DataSourceTransactionManager:372 - Creating new transaction with name [com.chenxin.spring5.tx.service.UserServiceImpl.register]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2021-04-05 22:29:26 INFO  DruidDataSource:1003 - {dataSource-1} inited
2021-04-05 22:29:26 DEBUG DataSourceTransactionManager:265 - Acquired Connection [com.mysql.jdbc.JDBC4Connection@6e005dc9] for JDBC transaction
2021-04-05 22:29:26 DEBUG DataSourceTransactionManager:282 - Switching JDBC Connection [com.mysql.jdbc.JDBC4Connection@6e005dc9] to manual commit
2021-04-05 22:29:26 DEBUG SqlSessionUtils:49 - Creating a new SqlSession
2021-04-05 22:29:26 DEBUG SqlSessionUtils:49 - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50de186c]
2021-04-05 22:29:26 DEBUG SpringManagedTransaction:49 - JDBC Connection [com.mysql.jdbc.JDBC4Connection@6e005dc9] will be managed by Spring
2021-04-05 22:29:26 DEBUG save:159 - ==>  Preparing: insert into t_user(name, password) values (?, ?) 
2021-04-05 22:29:26 DEBUG save:159 - ==> Parameters: chenxin(String), 11111222333(String)
2021-04-05 22:29:26 DEBUG save:159 - <==    Updates: 1
2021-04-05 22:29:26 DEBUG SqlSessionUtils:49 - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50de186c]
2021-04-05 22:29:26 DEBUG SqlSessionUtils:49 - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50de186c]
2021-04-05 22:29:26 DEBUG SqlSessionUtils:49 - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50de186c]
2021-04-05 22:29:26 DEBUG SqlSessionUtils:49 - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50de186c]
2021-04-05 22:29:26 DEBUG DataSourceTransactionManager:743 - Initiating transaction commit
2021-04-05 22:29:26 DEBUG DataSourceTransactionManager:327 - Committing JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@6e005dc9]
2021-04-05 22:29:26 DEBUG DataSourceTransactionManager:385 - Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@6e005dc9] after transaction

很明显,已经成功!

那么在完成Spring事务开发的编码后,我们分析几个细节

我们为UserService的register增加了事务,也保存到了数据库中,而我们之前没有用UserService的时候,只用UserDao.save方法,也是可以保存到数据库的,所以通过简单的保存,是不能很好的验证register方法加上了事务

是在忽悠你们吗?

当然不是,为了证明一定是在register上加的事务,我给你验证下

我在register上加上一行代码

public void register(Users users) {
        userDao.save(users);
        throw  new RuntimeException("eee!");
    }

此时这两行代码构成了一个整体,就应该符合事务的原子性,也就是这两行代码,要么一起成功,要么一起失败。如果失败了,说明是在register上加了事务,否则就没加,该执行还是执行,大不了抛个异常罢了。

运行看下

很明显有了Rolling back事务的日志,去数据库看看,一定是没有的,因为被异常回滚了!!

第二个细节是什么呢?

在配置文件中,

<tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>

标签中有个额外属性

proxy-target-class="true"

默认是false,记不记得我们讲aop代理的时候,这个属性是控制切换JDK和Cglib代理的方式?

false表示采用默认的基于接口的JDK代理,反之则为Cglib代理。

所以整个Spring的事务处理,就是用到我们之前讲的Aop代理。

 

5、Spring中的事务属性

1、什么是事务属性

属性是描述物体特性的一系列描述值

那么这个事务的特性是什么呢?五大特性

  • 隔离属性
  • 传播属性
  • 只读属性
  • 超时属性
  • 异常属性

2、如何添加事务属性

Spring的@Transactional注解就已经帮你做到这些

isolation:隔离属性

propagation:传播属性

timeout:超时属性

readOnly:只读属性

rollbackFor和noRollbackFor:异常属性

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

这里先混个眼熟,后面在详解

3、事务属性详解

  • 隔离属性的概念(isolation)

他描述了事务解决并发问题的特征

什么是并发?

并发指多个事务(用户)在同一时间,访问操作了相同的数据。

这里我要详细说明下,这个同一时间,并不是严格意义上的吻合且不差分毫,而是有0.0000几秒的前后之分,微小前,微小后,在人的角度来说,可能感受不到1s一下时间的差异,站在计算机的角度,还是有这样的差异的

并发会产生哪些问题?

脏读、不可重复读、幻读

出现并发问题,怎么解决?

通过隔离属性来解决的,隔离属性中设置不同的值,解决并发过程中的问题!!

 

来看看事务并发产生的问题,这些问题和现象,分别为我们的程序,带来了哪些困扰?

1、脏读

概念:一个事务,读取了另一个事务中没有提交的数据,会在本事务中产生数据不一致的问题

画个图:

假设,chenxin今天发了1000块钱的工资,chenxin的媳妇儿在下午15点000几秒的时候,去取钱,取了300块钱

此时账户上剩下700,对于update操作是update(1000-300=700),但是这个时候媳妇儿还没做完,还没来得及进行事务的提交;此时chenxin在0.000几秒后也访问到这条数据,这个时候就有了这个并发了

此时chenxin做了一件事,也是去取钱,但是chenxin读到了媳妇儿没有提交的数据,也就是700,于是取了200,把账户更新成了update(700-200=500),chenxin做这个事情的时候,他媳妇儿没有闲着,觉得这对chenxin太不仁慈了

良心发现了,不想取钱了,于是进行回滚rollback,等同于媳妇儿300块钱没有取!

所以此时,站在媳妇儿的角度来说,我没取钱,卡里应该还剩下1000块钱没动,而对于chenxin来说,认为卡里目前应该剩下500块钱。

闲聊的时候发现,媳妇儿问,今天发工资了啊,发了多少?1000啊,我可取了200。那现在账户还有多少钱?500块啊。你取200,发1000,还剩下500?你说剩余的300去哪了???给谁了??

所以,产生这个问题的核心是:chenxin读取了媳妇儿未提交的数据!!人家有可能反悔,可能回滚,就影响到了chenxin的后续操作,因为拿到了错误的数据,进行后面的处理。

这就是所谓的——————脏读!!!

 

上面我们提到,事务并发的问题,需要通过隔离属性来解决,那么在编程的过程中写在哪里呢——@Transactional,里面给isolation值,就可以解决这个脏读的问题了

@Transactional(isolation=Isolation.READ_COMMITED)    ——》  隔离级别为,提交读,这个很好记忆,因为脏读产生的原因是未提交读,所以,你设置隔离级别为提交读就可以了!!后续我只能读取你提交的数据,就不会产生脏读的情况了 

 

2、不可重复读

概念:一个事务中,多次读取相同的数据,但是读取的结果不一样,会在本事务中产生数据不一致的问题。

画个图:还是一样的图

媳妇儿此时,开启了一个事务,先查询chenxin的账户1000块,但是这个时候,chenxin来了,做了个操作,将自己的账户从1000改为了800,取了200块钱去干事了,update后,进行了commit;chenxin就溜了

媳妇儿这会去做其他事情了,其他事情处理完了,再回来查chenxin的账户,发现读到的是800块了。这个过程,就是不可重复读,在媳妇儿的一个事务里,读了两次数据,两次却不一样。那么业务流程中,就会有问题,到底是基于1000去处理,还是基于800

去处理后续的业务流程?

这个称为——不可重复读!!

注意两个细节,1、这个800块不是脏数据,因为chenxin做完update后,直接提交了,就不可能再回滚,影响我后续的操作,所以不是脏数据

2、对于媳妇儿来说,这是一个事务内两次查询不一样的结果,如果是今天查询1000,明天查询800,这个就是正常啊,很有可能今天到明天之内有人取了钱,但是一定不是在一个事务,你没见过一个事务要一天后提交的吧???

那我要是查5次呢,每次查就少200,查6次岂不是就成了欠银行的了?所以这个是有问题的。

 

解决方案,依旧是设置隔离属性@Transactional(isolation=Isolation.REPETABLE_READ),意思就是隔离属性设置为可重复读,底层是为数据库这条数据,加上一把行锁!!!

本质是一把行锁;作为媳妇儿来说,如果设置了当前事务是可重复读,就会把这条数据加上一把行锁,那么chenxin在去做其他操作的时候,只有等待,等待锁释放了,那么chenxin继续自己的业务逻辑;只要媳妇儿自己不做任何修改,就可以保证在

媳妇儿的一个事务里,多次读取都是相同的数据,永远获得的余额是1000!

3、幻读

概念:一个事务中,多次对整表进行查询统计,但是结果不一样,会在本事务中产生数据不一致的问题

还是这个图:

有一个事务t1,做了个全表统计select count(*) = 10800元,此时t1做其他事情去了,这个时候事务t2来了,做了个插入数据的操作,id=4,name=hah,money=2000,因为t2事务执行很快,把这个事务给提交了!!

然后t1做了后续的处理后,又进行了一次统计,发现现在成了12800了,站在这个t1这个事务,或者用户来说,存在数据不一致的情况了;

造成了t1的幻影读!!就好像id=4这条数据,一开始查的时候没有,后来查的时候发现有了,所以称为幻读!

 

解决方案:依旧是设置隔离属性@Transactional(isolation=Isolation.SERIALIZABLE),这个是和序列化那个关键字是一样的,只不过是大写。本质是对应数据库的表锁!!

对于t1来说,访问到了这个数据后,会把这个表给锁住,那么表都锁住了,t2你来的时候插入,更新,删除的操作都不可以进行了。t2只能等锁释放掉

 

总结:

1、从并发安全来说,表锁安全性   >    行锁安全性   >   无锁安全性,对应的隔离级别是:Serializable   >   Repeatable_read  >   Read_commited

2、从运行效率来说,表锁安全性   <    行锁安全性   <   无锁安全性,对应的隔离级别是:Serializable   <   Repeatable_read  <   Read_commited

那在未来开发中,我们应该用哪种来控制呢?后续我们拿出一个章节专门讲这个,这里先留个悬念哈哈!!

 

4、数据库对隔离属性的支持

我花了一张图

发现没,Oracle对于可重复读这个隔离属性,是不支持的。那么Oracle如何解决不可重复读呢?

这个小事Oracle肯定可以做的,只是他没有采用隔离属性去做,而是采用多版本比对的方式,来解决不可重复读!!

 

5、默认隔离属性

如果我们把@Transactional后面的隔离属性都去掉,默认采用的是什么隔离属性呢?

验证一波

这里会有一个隔离属性是Isolation_default,实际上当你不写的时候,Spring给的一个默认隔离属性,这个默认隔离属性,是会调用不同数据库所设置的默认隔离属性。

如果是Mysql,默认是可重复读——Repeatable_read

验证一波Mysql环境

如果是Oracle,默认是提交读——Read_commited

我这里不截图了,没环境,sql也麻烦,如果有兴趣可以去搜下,实际上查找的是动态视图。

SELECT s.sid, s.serial#,
CASE BITAND(t.flag, POWER(2, 28))
WHEN 0 THEN 'READ COMMITTED'
ELSE 'SERIALIZABLE'
END AS isolation_level
FROM v$transaction t
JOIN v$session s ON t.addr = s.taddr
AND s.sid = sys_context('USERENV', 'SID');

 

所以这个,默认隔离级别和你的数据库有关系!!

 

隔离属性在实战中的建议:

推荐使用Spring的默认值,好处是连接不同的数据库,可以使用不同数据库的默认隔离属性;

我再想说一句大实话,其实在实际开发中,这种并发的场景,本身就很低,别看B站很多视频讲什么高并发,高可用,实战中遇到的可能性非常低的,前提一定是要有海量的用户!!!

你我两个人同时点这个这个链接,到服务器那边也一定会有一个前后之分的,为了一个很难遇到的场景,加各种各样的锁,从而影响我们系统效率,这肯定是不能接受的!!!

所以没必要指定这么高的隔离属性,你能看到的,默认的其实就够了,如果你在大厂专门做高并发的,你应该也会赞同我这段话!

如果真遇到这个并发的问题,优先推荐的方案也不是通过隔离属性来解决,而是通过乐观锁来处理,乐观锁是应用锁,不会影响我们效率,而隔离属性是物理锁,,实际在就在给物理文件加锁,对效率影响太大了。

Hibernate(JPA)我们可以通过Verison方式去做处理,而Mybatis就稍微复杂点,通过拦截器自定义去开发版本对比,有兴趣可以上网了解下。

 

6、传播属性

概念:描述了事务解决嵌套问题的特征。

什么叫做事务嵌套?

比如我事务A,包含着一个事务B,这样就称为嵌套事务,什么场景呢?

比如Service调用Service,UserService中的register方法,更新用户后,会调用OrderService的查看该用户的订单信息,这个业务场景是很常见的,而对OrderService的查看订单信息,假如说,加上了事务,而UserService的register也加上了事务,

这就形成了事务嵌套。

还有一个业务场景,比如TA事务,嵌套了TB和TC事务,TA开启了事务后,执行业务逻辑,调用了TB事务,TB事务执行完了,提交了TB事务,然后继续执行TC事务,这个时候,TC事务发生了异常,那么TC事务应该需要回滚rollback

TC事务属于TA事务的一个部分,那么TC出现了异常,TA也应该需要回滚,此时,TA可以回滚,TC可以回滚,但是!!!TB已经提交了,无法回滚了。

在事务嵌套过程中因为一个大事务,嵌套了很多小事务,这些小事务会彼此影响,互相干扰,不能保证大的事务的原子性了。

 

7、传播属性的值及其用法


对于Required,有A,B,C三个业务类

然后我设置个调用关系,A调用B和C;事务产生了嵌套,如果C出了问题,所有事务没办法一起回滚保证原子性,要么一起成功,要么一起失败

所以这里的Required可以解决这个问题。

详细补充下,Required可以 保证这个事务的完整性,我上一张图对比下:

首先作为A方法来说,外部不存在事务,那么会创建新的事务,形成了事务A,所以我需要在A方法上加传播属性Required

对于方法B和C来说,我外部是存在事务A的,所以我在B,C方法上加上传播属性,此时B和C就会融合到A事务中,自己的事务就不要了,以A事务为主形成一个整体,这样就整体保证了原子性,你看看,此时B或者C任何一个出现问题,是不是都等于A事务出了问题,那么A,B和C就可以保证整体成功,或者整体失败!!!

实际生产中,我们会为增删改这样的方法加上传播事务,甭管自己的内部使用,还是独立使用,都可以保证了有事务的存在!!保证了原子性

看第二个Supports,如果A方法外部不存在事务,加上这个传播属性,则A方法不开启事务;

如果A方法外部存在事务,则加上这个传播属性,表示和外部事务融合,以外部事务为主;

场景多用于查询,如果A本身是查询功能,没有外部事务,加上Supports注解,A就不开启事务;而如果A外部有事务,反正我自己不需要事务,我就听外部事务的!

默认的传播属性是什么呢?

记不记得上面那个图

前面有一个Propagation_Required,这就是传播属性的默认值,而这个传播属性用于增删改方法上,所以这个默认的你写不写都行。

推荐日后开发的使用方式:

1、增删改:直接使用默认的Required

2、查询:显示指定默认值Supports

剩下不常用的,我在上面的图里写了,这里就不多做解释了,我用的也非常少,几乎没用过!!!在未来的及其微小发生的概率上,0.001%的概率会用到,记住含义,记得有这么个隔离属性可以解决这个问题就行

 

8、只读属性

针对于只进行查询操作的业务方法,可以加入只读属性,提高运行效率。

这个出现的原因,前面我们说了加上事务,为了保证并发安全,就要加各种各样的锁,一定会影响效率,当我们确定这个方法是查询操作,为方法加上了只读属性,就不会加上各种各样的锁!

可以这么使用:

如果不加,readOnly默认值是false

9、超时属性

还是这个图

如果t1事务去给id=1这个记录加了一把行锁,那么t2事务需要进行等待t1释放锁,那么这个等待时间后进行后续操作,我们称为超时属性。

timeout的单位是秒,timeout=2表示等待2s,超过2s后,就抛出异常了,我们让register这个方法睡3s。

@Transactional(timeout = 2)
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void register(Users users) {
        try {
            Thread.sleep(TimeUnit.SECONDS.toMillis(3));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        userDao.save(users);
//        throw  new RuntimeException("eee!");
    }
}
    @Test
    public void test10(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        com.chenxin.spring5.tx.service.UserService userService = (com.chenxin.spring5.tx.service.UserService) ctx.getBean("userService");

        Users users = new Users();
        users.setName("chenxin");
        users.setPassword("11111222333");
        userService.register(users);
    }

结果超时异常:

如果不显示写默认值,Spring默认的属性值是-1,表示最终这个值由对应的数据库指定。假如Oracle是3s,Mysql是4s,这个我是打比方,实际开发中,很少去指定这个值!

一般默认值就够了。

10、异常属性

Spring事务处理中

默认:对于 RuntimeException 及其⼦类 采⽤的是回滚的策略
 
默认:对于 Exception 及其⼦类 采⽤的是提交的策略
 

 

这个对比就很nice了
 

 那么我们可以不可以让RuntimeException采用提交操作?让Exception采用回滚操作?

设置rollbackFor即可!!

想让RuntimeException不提交,直接回滚,就这么设置 rollbackFor = {java.lang.Exception,xxx,xxx}
想让Exception提交,不回滚,这么设置 noRollbackFor = {java.lang.RuntimeException,xxx,xx}
 
 
实战中,我们推荐用默认的设置,真正场景里,我们很少很少用到这个异常属性!!基本上有异常,还是会直接回滚,所以建议用RuntimeException及其子类。

总结:

1. 隔离属性 默认值

2. 传播属性 Required(默认值) 增删改 Supports 查询操作

3. 只读属性 readOnly false 增删改 true 查询操作

4. 超时属性 默认值 -1

5. 异常属性 默认值

增删改操作 @Transactional

查询操作    @Transactional(propagation=Propagation.SUPPORTS,readOnly=true)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风清扬逍遥子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值