摘要:面试时常常被面试官问到这个问题。Spring框架中的核心技术:控制反转/依赖注入/面向切面编程/Spring的声明式事务/以及Spring生态相关产品的简介,本文是Spring第六讲:Spring事务原理及应用
1、 请描述一下Spring的事务(事务的实现方式+事务底层原理)
1.1、编程式事务与声明式事务
1.1.1、编程式事务:是指在代码中手动管理事务的提交、回滚等操作,代码侵入性比较强
如下示例:
public void update(Integer id, String name, Integer age, Integer marks, Integer year) {
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
String SQL1 = "update Student set age = ?,name = ? where id = ?";
jdbcTemplateObject.update(SQL1, age,name, id);
System.out.println("Updated Record with ID = " + id);
String SQL2 = "update Marks set marks = ?,year = ? where sid = ?";
jdbcTemplateObject.update(SQL2, marks,year, id);
System.out.println("Updated Record with SID = " + id);
transactionManager.commit(status);
} catch (DataAccessException e) {
System.out.println("Error in creating record, rolling back");
transactionManager.rollback(status);
throw e;
}
return;
}
1.1.2、声明式事务:基于AOP面向切面的
它将具体业务与事务处理部分解耦,代码侵入性很低,所以在实际开发中声明式事务用的比较多。声明式事务也有两种实现方式
基于TX和AOP的xml配置文件方式,已经过时
步骤:
1、配置事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="sessionFactory" ref="sessionFactory">
<bean/>
2、配置注解事务
<tx:annotation-driven transaction-manager="transactionManager"/>
<!--配置advice通知 ->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED" timeout="" read-only="false" no-rollback-for="" rollback-for=""/>
<tx:method name="*" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="interceptorPointCuts"
expression="execution(* cn.gov.zcy.service.*.manager.*.*(..))” id="pointcut"/>
<aop:advisor advice-ref="txAdvice"
pointcut-ref="interceptorPointCuts"/>
</aop:config>
execution 语法结构 execution([权限修饰符][返回类型][类全路径]方法名称) execution(com.bluesky.spring.dao..*(…)) 所有方法进行增强
基于@Transactional 注解
- @Transactional 可以注解在接口、接口方法、类和类方法上。当作用于类上时,该类的一切 public 方法将都具有该类型的事务属性。
spring事务配置 TransactionManager----datasourcetransactionManager // jdbc配置 ----hibernatetransactionManager // hibernate配置
代理机制----bean和代理 --每个bean有一个代理 –所有bean共享一个代理基类 ----使用拦截器 ----使用tx标签配置的拦截器 ----全注解配置
基于TransactionInterceptor的声明式事务管理:
- 两个次要的属性:
- transactionManager,用来指定一个事务治理器,并将具体事务相关的操作请托给它;
- Properties 类型的transactionAttributes 属性,该属性的每一个键值对中,键指定的是方法名,方法名可以行使通配符,而值就是表现呼应方法的所运用的事务属性。
代码示例如下
@Transactional
public class AccountServiceImpl implements AccountService{}
- 结论:事务的操作本来应该是由数据库来进行控制,但是为了方便用户进行业务逻辑的操作,Spring对事务功能进行了扩展实现,一般我们很少使用编程式事务,更多的是通过添加 @Transactional 注解来进行实现,当添加此注解后事务的自动功能就会关闭,由Spring框架来帮忙进行控制。
1.2、@Transactional注解介绍
1.2.1、@Transactional注解可以作用于哪些地方?
- @Transactional 可以作用在接口、类、类方法。
- 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息。
- 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
- 作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效
代码示例如下:
@Transactional
@RestController
@RequestMapping
publicclass MybatisPlusController {
@Autowired
private CityInfoDictMapper cityInfoDictMapper;
@Transactional(rollbackFor = Exception.class)
@GetMapping("/test")
public String test() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setParentCityId(2);
cityInfoDict.setCityName("2");
cityInfoDict.setCityLevel("2");
cityInfoDict.setCityCode("2");
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert + "";
}
}
1.2.2、@Transactional注解有哪些属性?
Spring 对事务控制的支持统一在 TransactionDefinition 类中描述,该类有以下几个重要的接口方法:
-
int getPropagationBehavior():事务的传播行为:就是多个事务方法相互调用时,事务如何在这些方法间传播
- REQUIRED 支持当前事务,不存在就新建一个 默认(默认的隔离级别与各个数据库一致)
- required new 如果有当前事务,挂起当前事务,创建一个新的事务
- nested 如果有当前事务,嵌套事务执行
- 假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这3个服务类的3个方法通过Spring的事务传播机制都工作在同一个事务中
-
int getIsolationLevel():事务的隔离级别 -
int getTimeout():事务的过期时间 -
boolean isReadOnly():事务的读写特性。 -
传播行为是解决什么问题呢?
- 多个事务方法相互调用时,事务是如何在这些方法之间进行传播的。
- 例如 A类有个方法a,B类有个方法b,如果在a方法里面调用b方法,那么应当执行什么样的事务策略呢,看配置的propagation属性
-
propagation属性(共7种)
- 定义:propagation 代表事务的传播行为,默认值为 Propagation.REQUIRED,其他的属性信息如下:
- ①Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。
- 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务
- 默认
- ②Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
- ③Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
- ④Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。
- 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务
- ⑤Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
- ⑥Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。
- ⑦Propagation.NESTED :和 Propagation.REQUIRED 效果一样。
-
isolation 属性
- isolation:事务的隔离级别,默认值为 Isolation.DEFAULT。
- TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,
- Mysql 默认采用的 REPEATABLE_READ隔离级别
- Oracle 默认采用的 READ_COMMITTED隔离级别.
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读.
- TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
- TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
- 但是这将严重影响程序的性能。通常情况下也不会用到该级别,没有使用场景
-
timeout 属性
- timeout:事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
-
readOnly 属性
- readOnly:指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
-
rollbackFor 属性
- rollbackFor:用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。
-
noRollbackFor属性
- noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。
1.2.3、事务执行流程图
1.3、Spring事务的底层实现原理
1.3.1、事务中的主类
1、TransactionProxyFactoryBean
2、TransactionAttributeSourceAdvisor
- Spring事务切入点通知器
- 定义cutPoint,和对应的Interceptor
3、TransactionInterceptor
- 事务拦截器
- 调用 TransactionAspectSupport.invokeWithinTransaction方法开始执行切面事务
4、TransactionAspectSupport
- 切面事务逻辑主类,声明事务+反射调用全流程
- 1、组装创建事务前置条件信息
- 2、创建事务
- 3、反射调用真正业务逻辑
- 4、事务的提交和回滚
5、AbstractPlatformTransactionManager
- 抽象事务管理器,声明事务中操作模板,封装通用部分,屏蔽TransactionStatus的流转细节,由具体的事务管理器(类似DataSourceTransactionManager或HibernateTransactionManager等)来实现具体逻辑,如 doGetTransaction,doBegin,doCommit,doRollback,isExistingTracsaction等逻辑实现
6、TransactionStatus
- 事务状态操作接口类
- 由具体的实现类类实现状态操作接口(如DefaultTransactionStatus)
7、TransactionInfo
- 事务信息
- 描述一个事务内所涵盖的所有信息,如下
- 1、事务管理器
- 2、事务属性(隔离级别、传播行为等)
- 3、事务状态
- 4、事务切点
1.3.2、使用AOP实现原理
- 当一个方法添加 @Transactional 注解之后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当使用这个代理对象的方法时,如果有事务处理,那么会先把事务的自动提交给关闭,然后去执行具体的业务逻辑,如果执行逻辑没有出现异常,那么代理逻辑就会直接提交,如果出现任何异常情况,那么直接进行回滚操作,当然,用户可以控制对那些异常进行回滚操作。
AOP原理:
- AOP在进行解析的时候,最终生成一个Advisor对象,这个Advisor对象中封装了切面织入所需要的所有信息,其中就包括Aspect:他是跨不同java类层面的横切性逻辑,实现形式上,可以是XML文件中配置的普通类,也可在类代码中用“@Aspect”注解声明,运行时spring框架创建Advisor来指代他:(源码中对应BeanFactoryTransactionAttributeSourceAdvisor)
Advisor最重要的两个部分:PointCut和Advice属性。
- 切入的时机pointCut:判断目标bean是否需要织入当前事务逻辑;(为了使切点复用,利用@PointCut专门定义拦截规则)代码中对应TransactionAttributeSourcePointcut
- 切入的动作Advice:封装了需要织入的切面逻辑 ;代码中对应TransactionInterceptor
Join Point:它是Aspect可以切入的特定点,在Spring里面只有方法可以作为Join Point(是可利用的机会,具体由pointcut指定) Advice:它定义了切面中能够采取的动作。如果去看Spring源码,就会发现Advice、Join Point并没有定义在Spring自己的命名空间里,这是因为他们是源自AOP联盟,可以看作是Java工程师在AOP层面沟通的通用规范
- 通常将拦截器类型的Advice叫作Around,在代码中可以使用“@Around”来标记,或者在配置中使用“ < aop:around>”
分别解释这三个类:
- BeanFactoryTransactionAttributeSourceAdvisor:封装了实现事务所需的所有属性,包括pointCut,advice,transactionManager以及一些其他在Transactional注解中声明的属性;
- TransactionAttributeSourcePointcut:判断目标bean是否需要织入当前事务逻辑,依据就是当前方法或类声明上有没有使用@Transactional注解;
- TransactionInterceptor : 封装了需要织入的切面逻辑 ,Spring事务是借助数据库事务来实现对目标方法的环绕的。
总结:Spring支持AspectJ的注解式切面编程 1、使用@Aspect声明是一个切面; 2、使用@After、@Before、@Around定义建言(advice),可直接将拦截规则(切点)作为参数; 3、其中@After、@Before、@Around参数的拦截规则为切点(PointCut),为了使切点复用,可使用@PointCut专门定义拦截规则; 4、其中符合条件的每一个被拦截处为连接点(JointPoint)
示例: 1、添加Spring AOP支持及AsectJ依赖
- aop/aspectjrt/aspectjweaver
2、编写拦截规则的注解(编写注解)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Action{
String name();
}
3、编写使用注解的被拦截类(使用注解)
@Service
public class DemoAnnotationService{
@Action(name="拦截式拦截的add操作")
public void add(){}
}
4、编写切面
@Aspect
@Component
public class LogAspect{
@PointCut("@annotation(com.wisely.aop.Action)")
public void annotationPointCut(){};
@After("anntationPointCut")
public void after(JointPoint jointPoint){
MethodSignature signature=(MethodSignature)jointPoint.getSignature();
Method method =getSignature.getMethod();
Action action = method.getAnnotation(Action.class);
sout("Q"+action.name());
}
}
5、配置类
- 使用@EnableAspectJAutoProxy注解开启Spring对AspectJ的支持
6、运行
1.3.3、事务实现原理-整体流程
1.3.4、事务代理对象、拦截器建立过程
1.3.5、事务拦截及处理过程
1.3.6、createTransactionIfNecessary 方法处理过程
1.3.7、DataSourceTransactionManager.doBegin() 方法处理过程
1.3.8、Mybatis 查询db过程
1.3.9、Spring + Mybatis 事务中查询db过程
1.3.10、多事务管理器
Demo示例如下:
@Configuration
public class TransactionManagerConfiguration {
@Bean(name = "drdsTransactionManager")
public PlatformTransactionManager drdsTransactionManager(DynamicDataSource dynamicDataSource){
DataSource targetDataSourceByUserDefinedKey = dynamicDataSource.getTargetDataSourceByUserDefinedKey("drds");
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(targetDataSourceByUserDefinedKey);
return dataSourceTransactionManager;
}
@Bean(name = "drdsSqlSessionFactory")
public SqlSessionFactory drdsSqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
DataSource targetDataSourceByUserDefinedKey = dynamicDataSource.getTargetDataSourceByUserDefinedKey("drds");
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(targetDataSourceByUserDefinedKey);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/itemMapper.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage("cn.gov.zcy.service.item.domain");
return sqlSessionFactoryBean.getObject();
}
@Bean(name = "drdsSqlSessionTemplate")
public SqlSessionTemplate drdsSqlSessionTemplate(@Qualifier("drdsSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
return sqlSessionTemplate;
}
}
1.3.11、分布式事务
见这篇文章:MySQL第七讲:数据库事务及MVCC机制/分布式事务实战
2、 六种 @Transactional 注解失效场景 面试必备
使用@Transactional注解时需要注意许多的细节,不然你会发现@Transactional总是莫名其妙的就失效了
- 1、@Transactional 应用在非 public 修饰的方法上
如果Transactional注解应用在非public 修饰的方法上,Transactional将会失效 之所以会失效是因为在Spring AOP 代理时,如上图所示 TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
-
此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。 注意:protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。 -
2、@Transactional 注解属性 propagation 设置错误 这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。
- TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
-
3、@Transactional 注解属性 rollbackFor 设置错误 rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
- 若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。Spring 源码如下:
private int getDepth(Class<?> exceptionClass, int depth) {
if (exceptionClass.getName().contains(this.exceptionName)) {
return depth;
}
if (exceptionClass == Throwable.class) {
return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}
- 4、同一个类中方法调用,导致@Transactional失效(开发中遇到过这种场景)
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
@GetMapping("/test")
private Integer A() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
this.insertB();
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert;
}
@Transactional()
public Integer insertB() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("3");
cityInfoDict.setParentCityId(3);
return cityInfoDictMapper.insert(cityInfoDict);
}
解决方案
- 在事务内部调用事务时,对调用的方法采用
ApplicationContextUtil 来管理,使得 Spring容器能管理事务。
AgGoodsManager agGoodsManager = (AgGoodsManager) ApplicationContextUtil.getBean("agGoodsManager");
- 5、异常被你的 catch“吃了”导致@Transactional失效
这种情况是最常见的一种 @Transactional 注解失效场景
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);
insert = cityInfoDictMapper.insert(cityInfoDict);
b.insertB();
} catch (Exception e) {
e.printStackTrace();
}
}
如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?
答案:不能! 会抛出异常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
-
因为当ServiceB中抛出了一个异常以后,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。 -
spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。 -
在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit造成数据不一致,所以有些时候 try catch反倒会画蛇添足。 -
6、数据库引擎不支持事务 这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。
参考资料
吃得苦中苦,方为人上人
|