# 一、Spring基础
# 1.1 Spring简介
Spring官网:https://docs.spring.io (opens new window)
Spring Framework
中文文档:
- https://github.com/DocsHome/spring-docs (opens new window)
- https://www.jcohy.com/projects/spring-framework (opens new window)
Spring 框架为基于 Java 的现代企业应用程序提供了全面的编程和配置模型。
Spring是一个开源框架,提供一站式解决方案。其目的是为了解决企业应用系统的复杂度,复杂度就是耦合度。
Spring是一个轻量级的**控制反转(IoC)和面向切面(AOP)**的容器框架。
# 1.2 Spring特征
轻量——从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。此外,Spring是非侵入式的:典型地,Spring应用中的对象不依赖于Spring的特定类。
控制反转(IOC)——Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反——不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
依赖注入(DI)——用DI依赖注入来实现IOC控制反转。
面向切面(AOP)——Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
容器——Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建——基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。
框架——Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。
所有Spring的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。
# 二、Spring原理
# 2.1 AOP
AOP就是在Spring中指定的 方法前后完成一些事情。
实现AOP的主要设计模式就是动态代理。
Spring AOP采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类, 对于动态代理技术,Spring AOP提供了对JDK动态代理的支持以及CGLib的支持。
Spring的动态代理有两种:
- 一是JDK的动态代理;
- 另一个是CGLib动态代理。
# JDK动态代理
JDK动态代理只能为接口创建动态代理实例,而不能对类创建动态代理。
需要获得被目标类的接口信息(应用Java的反射技术),生成一个实现了代理接口的动态代理类(字节码),再通过反射机制获得动态代理类的构造函数,利用构造函数生成动态代理类的实例对象,在调用具体方法前调用invokeHandler方法来处理。
JDK动态代理的两个核心接口(类)分别是InvocationHandler和Proxy。注意:只能代理接口。
- 实现
InvocationHandler
接口,重写invoke
方法; - 创建要被代理的接口;
- 通过
Proxy
的newProxyInstance
方法,创建代理对象; - 用代理对象调用其真实对应的方法。
# CGLib动态代理
CGLib动态代理需要依赖asm包,把被代理对象类的class文件加载进来,修改其字节码生成子类。 但是Spring AOP基于注解配置的情况下,需要依赖于AspectJ包的标准注解。
# 2.2 IOC、DI
控制反转IOC
控制反转(IOC),就是将Spring Bean的生命周期交于Spring进行管理。
IOC使得我们只关注与业务本身,而不再关注于对象的创建。之前创建对象是由程序本身创建,现在对象是由spring容器来创建了,程序被动接受创建对象!
IOC 控制反转,用来解决层与层之间的耦合问题,降低系统耦合度。
依赖注入(DI),使用DI来实现IOC。 依赖注入只在容器运行过程中,若需条用另一个对象使用时,无需再代码中创建,而是依赖于Spring容器,由外部容器创建后传递给程序。从而实现对象间的解耦,降低程序的耦合度。
加载过程
首先,由Spring上下文ApplicationContext,将类加载成Bean定义BeanDefinition,有几个步骤:
通过BeanDefinitionReader读取xml中的配置类;
通过BeanDefinitionScanner 扫描配置类中有效的Bean;
通过BeanDefinitionRegistry注册成BeanDefinition。
然后ApplicationContext可以通过BeanFacotryPostProcessor扩展节点对BeanDefinition进行修改或注册。
Bean定义(BeanDefinition)中封装了Bean的一切属性,Bean工厂(BeanFactory)根据BeanDefinition来生产Bean。
# 2.3 Spring缓存
# 2.4 Spring循环依赖
# 三、Spring事务
# 3.1 Spring事务四种实现方式
- 基于
TransactionTemplate
的编程式事务管理实现; - 基于
TransactionProxyFactoryBean
的声明式事务管理; - 基于
AspectJ
的XML声明式事务管理; - 基于
@Transactlonal
注解的声明式事务管理;
# 3.2 Spring编程式事务
通过手动编写代码实现的事务,我们把这种事务叫做编程式事务。
在spring中为了支持编程式事务,专门提供了一个类:TransactionTemplate,在它的execute
方法实现了事务的功能。
@Autowired
private TransactionTemplate transactionTemplate;
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute(transactionStatus -> {
addData1();
updateData2();
return Boolean.TRUE;
});
}
2
3
4
5
6
7
8
9
10
11
12
相较于@Transactional
注解声明式事务,更建议大家使用基于TransactionTemplate的编程式事务。
主要原因如下:
- 避免由于
Spring AOP
问题,导致事务失效的问题。 - 能够更小粒度的控制事务的范围,更直观。
建议在项目中少使用@Transactional注解开启事务。但并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。
# 3.3 Spring声明式事务
Spring事务底层是基于数据库事务和AOP机制的。
实现步骤:
- 首先对于使用了@Transactlonal注解的Bean,Spring会创建一个代理对象作为Bean;
- 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解;
- 如果加了,那么则利用事务管理器创建一个数据库连接;并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的一步;
- 然后执行当前方法,方法中会执行SQL;
- 执行完当前方法后,如果没有出现异常就直接提交事务;
- 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务;
# 3.4 Spring事务的隔离级别
Spring事务的隔离级别对应的就是数据库的隔离级别。
- 未提交读:事务可以读取未提交的数据。
- 读已提交:一个事务只能看见已提交事务所做的修改。
- 可重复读
RR
(默认):保证了在同一个事务中,多次读取同样记录的结果是一致的。 - 串行化:强制事务串行执行,会在读取的每一行数据都加锁。
# 3.5 Spring事务的传播机制
Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的。
Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在新数据库连接上执行SQL。
spring目前支持7种传播特性:
REQUIRED
:如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。SUPPORTS
:如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。MANDATORY
:如果当前上下文中存在事务,否则抛出异常。REQUIRES_NEW
:每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。NOT_SUPPORTED
:如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。NEVER
:如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。NESTED
:如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
# 四、Spring注解
# Bean相关注解
# @Bean
# @Primary
对同一个接口,可能会有几种不同的实现类,而默认只会采取其中一种的情况下 @Primary 的作用就出来了。被注解为@Primary的Bean将作为首选者,否则将抛出异常。
# @Controller
# @Service
# @Repository
# @Autowired
@Autowired默认按类型装配(这个注解是属业spring的)。
默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) ;
@Qualifier
当存在多个实例配合时,如果我们想使用按名称装配,可以将@Autowired 结合注解@Qualifier一起使用。
@Autowired @Qualifie("userService") 两个结合起来可以根据名字和类型注入。
@Autowired默认是根据类型进行自动装配的。如果当Spring上下文中存在不止一个UserDao类型的bean时,就会抛出BeanCreationException异常;如果Spring上下文中不存在UserDao类型的bean,也会抛出BeanCreationException异常。这时,我们可以使用@Qualifier配合@Autowired来解决这些问题。如下:
@Autowired
@Qualifier("userServiceImpl")
public IUserService userService;
2
3
# @Resource
@Resource装配顺序:
- 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
- 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常
- 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常
- 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配
# @Value
# AOP相关注解
@Aspect
@Before
@After
@AfterReturning
@AfterThrowing
@Around
# 配置相关注解
# @PostConstruct
# @Component
@Configuration
@ComponentSpan
@Import
# @Transactional
# 五、Spring源码
# 名词解释
# ApplicationContext
Spring上下文,管理着Spring的整个生命周期。
# BeanDefinitionReader
通过它读取xml配置类
# BeanDefinitionScanner
通过它扫描配置类中有效的Bean
# BeanDefinitionRegistry
由它负责注册BeanDefinition。
# BeanDefinition
Bean定义,是Spinrg的顶层核心接口,封装了生产Bean的一切属性,即存放了Bean的元数据。
BeanDefinition是存储在BeanDefinitionMap中。
# BeanFactory
Bean工厂,是Spring的顶层核心接口,使用了简单工厂模式。它只有一个职责,就是根据BeanDefinition来生产Bean。
# BeanFacotryPostProcessor
扩展点:Bean工厂后置处理器 BeanFacotryPostProcessor
。
用来修改Bean定义BeanDefinition。
例如,集成Mybatis时就是利用该Bean工厂后置处理器来实现。
# BeanDefinitionRegistryPostProcessor
扩展点:Bean定义注册后置处理器 BeanDefinitionRegistryPostProcessor
。
可以用来注册Bean定义BeanDefinition(添加新的BeanDefinition)。
# FacotryBean
# Bean的产生
1、配置类有3种方式:XML配置、注解配置、JavaConfig。
2、加载spring的上下文路径
Xml:
new ClassPathXmlApplicationContext(“xxx.xml”);
1注解:
new AnnotationConfigApplitcationContext(XXXConfig.class);
1
3、获取Bean:getBean()
context.getBean();
# Bean的创建原理
# Bean的生命周期
首先,由Spring上下文ApplicationContext,将类加载成Bean定义BeanDefinition,有几个步骤:
通过BeanDefinitionReader读取xml中的配置类;
通过BeanDefinitionScanner 扫描配置类中有效的Bean;
通过BeanDefinitionRegistry注册成BeanDefinition。
然后ApplicationContext可以通过BeanFacotryPostProcessor扩展节点对BeanDefinition进行修改或注册。
Bean定义(BeanDefinition)中封装了Bean的一切属性,Bean工厂(BeanFactory)根据BeanDefinition来生产Bean。
Bean的创建过程包括实例化、填充属性、初始化。在整个Bean的创建过程中,有9次可以通过BeanPostProcessor扩展到对Bean的创建进行调整。
生产完存储在单例缓存池(一级缓存)Map中。调用getBean,其实就是从Map中获取bean。
# BeanPostProcessor
扩展点:Bean后置处理器 BeanPostProcessor
# 九、FQA
# 如何理解Spring
Spring作为一个容器,可以管理对象的生命周期、对象与对象之间的依赖关系。
Spring是为了解决企业级开发的复杂度问题,其目的就是为代码进行解耦,降低代码间的耦合度,耦合度就是复杂度。
在系统开发过程中分为主业务逻辑和系统级(交叉)业务逻辑。
Spring根据代码的功能特点,将降低耦合度的方式分为了两类:IoC和AOP;
- IoC使得主页午在相互调用过程中,不用再自己维护关系了。即不用再自己创建要使用的对象了,而是交由Spring统一管理;
- AOP使得系统级服务得到了最大的复用,由Spring容器统一完成织入。
# @Mapper和@Repository
# @Mapper
- @Mapper 一般我们用在DAO接口上,使用 @Mapper,最终 Mybatis 会有一个拦截器,会自动的把 @Mapper 注解的接口生成动态代理类。
- @Mapper与@MapperScan 不能同时使用。
# 共同点
两者都是用在DAO接口上,用于声明一个Bean。
# 区别
- @Mapper不需要配置扫描地址;而@Repository多了一个配置扫描地址的步骤,即**@Repository** 需要于 @MapperScan结合使用。
- @Mapper是
Mybatis
的注解,和Spring没有关系;@Repository 是Spring
的注解。
# @Autowired和@Resource
# 区别
一般@Autowired和@Qualifier一起用,@Resource单独用。当然没有冲突的话@Autowired也可以单独用。
- @Autowired默认按类型注入(type),@Qualifier("cusInfoService")一般作为@Autowired()的修饰用
- @Resource(name="cusInfoService")//默认按名字注入(name),可以通过name和type属性进行选择性注入;
# @Autowired、@Qualifier
@Autowired默认按类型装配(这个注解是属业spring的)。
默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) ;
如果我们想使用名称装配可以结合@Qualifier注解进行使用。
@Autowired @Qualifie("userService") 两个结合起来可以根据名字和类型注入。
@Autowired默认是根据类型进行自动装配的。如果当Spring上下文中存在不止一个UserDao类型的bean时,就会抛出BeanCreationException异常;如果Spring上下文中不存在UserDao类型的bean,也会抛出BeanCreationException异常。这时,我们可以使用@Qualifier配合@Autowired来解决这些问题。如下:
@Autowired @Qualifier("userServiceImpl") public IUserService userService;
1
2
3
# @Resource
@Resource装配顺序:
- 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
- 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常
- 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常
- 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配
# @Transactional失效场景
一、事务不生效【七种】
- 方法访问权限问题 (只有public方法会生效);
- 方法用final修饰,不会生效;
- 同一个类中的方法直接内部调用,会导致事务失效;
- 类本身未被Spring管理;
- 多线程调用;
- 表(存储引擎))不支持事务;
- 项目未开启事务;
二、事务不回滚【五种】
- 错误的传播特性;
- 自己手动捕获了异常;
# 1、方法访问权限非public
众所周知,java的访问权限主要有四种:private、default、protected、public;它们的权限从左到右,依次变大。
但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题 (只有public方法会生效)。
例如:
@Service
public class UserService {
@Transactional
private void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
2
3
4
5
6
7
8
9
解读:
我们可以看到add
方法的访问权限被定义成了private,这样会导致事务失效,Spring要求被代理方法必须得是public的。
说白了,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute
方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是public
,而是private、default或protected的话,Spring则不会提供事务功能。
# 2、方法用final修饰
有时候,某个方法不想被子类重写,这时可以将该方法定义成final
的。普通方法这样定义是没问题的。但如果将事务方法定义成final,事务不会生效。
例如:
@Service
public class UserService {
@Transactional
public final void add(UserModel userModel){
saveData(userModel);
updateData(userModel);
}
}
2
3
4
5
6
7
8
9
解读:
如果你看过Spring事务的源码,可能会知道Spring事务底层使用了AOP,也就是通过JDK动态代理或者CGlib,帮我们生成了代理类,在代理类中实现的事务功能。
- 而如果某个方法用
final
修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。 - 注意:如果某个方法是
static
的,同样无法通过动态代理,变成事务方法。
# 3、同类方法内部调用
有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法。同一个类中的方法直接内部调用,会导致事务失效。
例如:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}
@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。
由此可见,在同一个类中的方法直接内部调用,会导致事务失效。
那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?
解决方案一:
只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。
@Servcie
public class ServiceA {
@Autowired
prvate ServiceB serviceB;
public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
解决方案二:
在该Service类中使用AOPProxy获取代理对象来实现。
@Servcie
public class ServiceA {
public void save(User user) {
queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 4、(类本身) 未被spring管理
在我们平时开发过程中,有个细节很容易被忽略。即使用Spring事务的前提是:对象要被Spring管理,需要创建Bean实例。
通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现Bean实例化和依赖注入的功能。当然创建Bean实例的方法还有很多,不一一说了。
如下所示, 开发了一个Service类,但忘了加@Service注解。
// @Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
2
3
4
5
6
7
8
没有加@Service注解,那么该类不会交给Spring管理,所以它的add方法也不会生成事务。
# 5、多线程调用
在实际项目开发中,多线程的使用场景还是挺多的。如果Spring事务用在多线程场景中,会有问题吗?
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
从上面的例子中,我们可以看到事务方法add
中,调用了事务方法doOtherThing
,但是事务方法doOtherThing是在另外一个线程中调用的。
这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing
方法中抛了异常,add
方法也回滚是不可能的。
如果看过Spring事务源码的朋友,可能会知道Spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
2
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。
如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
# 6、表不支持事务
有些老项目中,可能还在用MyISAM。在创建表的时候,只需要把ENGINE
参数设置成MyISAM
。
MyISAM有个很致命的问题是:不支持事务。
# 7、未开启事务
有时候,事务没有生效的根本原因是没有开启事务。
- 如果你使用的是Springboot项目,那么你很幸运。因为Springboot通过DataSourceTransactionManagerAutoConfiguration类,已经默默的帮你开启了事务。你所要做的事情很简单,只需要配置
spring.datasource
相关参数即可。 - 但如果你使用的还是传统的Spring项目,则需要在
applicationContext.xml
文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
具体配置如下信息:
<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 用切点把事务切进去 -->
<aop:config>
<aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 8、错误的传播特性
在使用@Transactional
注解时,是可以指定propagation
参数的。
该参数的作用是指定事务的传播特性,Spring目前支持7种传播特性,如果我们在手动设置propagation
参数的时候,把传播特性设置错了,比如定义成了Propagation.NEVER
。
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
2
3
4
5
6
7
8
这种类型的传播特性不支持事务,如果有事务则会抛异常。
目前只有这三种传播特性才会创建新事务:REQUIRED
,REQUIRES_NEW
,NESTED
。
# 9、自己吞了异常
事务不会回滚,最常见的问题是:开发者在代码中手动try…catch
了异常。比如:
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
这种情况下Spring事务当然不会回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。
如果想要Spring事务能够正常回滚,必须抛出它能够处理的异常(
RuntimeException
(运行时异常)和Error
(错误))。如果没有抛异常,则Spring认为程序是正常的。或者手动进行事务回滚。
@Slf4j @Service public class UserService { @Transactional public void add(UserModel userModel) { try { saveData(userModel); updateData(userModel); } catch (Exception e) { //手工回滚异常 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); log.error(e.getMessage(), e); } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 10、手动抛了别的异常
即使开发者手动捕获后向外再抛出别的异常,但如果抛的异常不正确,Spring事务也不会回滚。
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) throws Exception {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面的这种情况,开发人员自己捕获了异常,又手动抛出了异常Exception
,事务同样不会回滚。
- 因为Spring事务,默认情况下只会回滚
RuntimeException
(运行时异常)和Error
(错误); - 对于普通的
Exception
(非运行时异常),它不会回滚,比如常见的IOExeption
和SQLException
。
# 11、自定义了回滚异常
在使用@Transactional
注解声明事务时,有时我们想自定义回滚的异常,Spring也是支持的。可以通过设置rollbackFor
参数,来完成这个功能。但如果这个参数的值设置错了,就会引出一些莫名其妙的问题。
例如:
@Slf4j
@Service
public class UserService {
@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel userModel) throws Exception {
saveData(userModel);
updateData(userModel);
}
}
2
3
4
5
6
7
8
9
10
如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException
、DuplicateKeyException
等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException
,所以事务也不会回滚。
即使rollbackFor
有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。
因为如果使用默认值,一旦程序抛出了Exception
,事务不会回滚,这会出现很大的BUG。所以建议一般情况下,将该参数设置成:Exception或Throwable。
# 12、嵌套事务回滚多了
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
roleService.doOtherThing();
}
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doOtherThing() {
// 发生异常
System.out.println("保存role表数据");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing
方法时,如果出现了异常,只回滚doOtherThing
方法里的内容,不回滚 userMapper.insertUser
里的内容,即回滚保存点。但事实是,insertUser也回滚了。
因为doOtherThing
方法出现了异常,没有手动捕获,会继续往上抛,到外层add
方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。
怎么样才能只回滚保存点呢?
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
try {
roleService.doOtherThing();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。
# Spring事务的大事务问题
关于大事务可参考:大事务问题 (opens new window)
在使用Spring事务时,有个让人非常头疼的问题,就是大事务问题。
# 大事务现象
现象一:在事务中存在大量查询操作
通常情况下,我们会在方法上@Transactional注解,填加事务功能。例如:
@Service
public class UserService {
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
query1();
query2();
query3();
roleService.save(userModel);
update(userModel);
}
}
@Service
public class RoleService {
@Autowired
private RoleService roleService;
@Transactional
public void save(UserModel userModel) throws Exception {
query4();
query5();
query6();
saveData(userModel);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
现在的这种写法,@Transactional注解被加到方法上,有个缺点就是整个方法都包含在事务当中了,会导致所有的query方法也被包含在同一个事务当中。
粒度太大,不好控制事务范围。如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。
现象二:事务中存在远程调用
我们在接口中调用其他系统的接口是不能避免的,由于网络不稳定,这种远程调的响应时间可能比较长,如果远程调用的代码放在某个事物中,这个事物就可能是大事务。
当然,远程调用不仅仅是指调用接口,还有包括:发MQ消息,或者连接redis、mongodb保存数据等。
远程调用的代码可能耗时较长,切记一定要放在事务之外。
远程调用的代码不放在事务中如何保证数据一致性呢?这就需要建立:重试+补偿机制,达到数据最终一致性了。
现象三:事务中处理大量数据
如果一个事务中需要处理的数据太多,也会造成大事务问题。比如为了操作方便,你可能会一次批量更新1000条数据,这样会导致大量数据锁等待,特别在高并发的系统中问题尤为明显。
解决办法是分页处理,1000条数据,分50页,一次只处理20条数据,这样可以大大减少大事务的出现。
# 大事务引发的问题
- 死锁
- 锁等待
- 回滚时间长
- 接口超时
- 数据库主从延迟
- 并发情况下数据库连接池被打满
# 大事务优化
优化方案一:缩小事务粒度
如果出现大事务,可以将查询(select
)方法放到事务外,也是比较常用的做法,因为一般情况下这类方法是不需要事务的。
上面的这个例子中,在UserService类中,其实只有这两行才需要事务:
roleService.save(userModel);
update(userModel);
2
在RoleService类中,只有这一行需要事务:
saveData(userModel);
优化方案二:使用编程式事务
利用TransactionTemplate 编程式事务的方法。
@Autowired
private TransactionTemplate transactionTemplate;
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
2
3
4
5
6
7
8
9
10
11
12
优化方案三:异步处理业务
还有一点也非常重要,是不是事务中的所有方法都需要同步执行?我们都知道,方法同步执行需要等待方法返回,如果一个事务中同步执行的方法太多了,势必会造成等待时间过长,出现大事务问题。
分析业务,将不需要及时返回处理的,进行异步处理。
但需注意数据的一致性。解决方法是建立重试+补偿机制,达到数据最终一致性。
# Spring中有哪些扩展接口
- Bean工厂后置处理器BeanFacotryPostProcessor
- Bean定义注册后置处理器BeanDefinitionRegistryPostProcessor
- Bean后置处理器BeanPostProcessor
# BeanFactory与ApplicationContext的关系
BeanFactory是Bean的工厂,Spring的顶层核心接口。没有BeanFactory就没有Bean的存在。BeanFactory只负责按照BeanDefinition来生产Bean。
ApplicationContext是Spring上下文,管理着Spring的整个生命周期。ApplicationContext继承自BeanFactory。