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