Spring

frameSpring

# 一、Spring基础

# 1.1 Spring简介

Spring官网:https://docs.spring.io (opens new window)

Spring Framework 中文文档:

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的动态代理有两种:

  1. 一是JDK的动态代理;
  2. 另一个是CGLib动态代理。

# JDK动态代理

JDK动态代理只能为接口创建动态代理实例,而不能对类创建动态代理。

需要获得被目标类的接口信息(应用Java的反射技术),生成一个实现了代理接口的动态代理类(字节码),再通过反射机制获得动态代理类的构造函数,利用构造函数生成动态代理类的实例对象,在调用具体方法前调用invokeHandler方法来处理。

JDK动态代理的两个核心接口(类)分别是InvocationHandlerProxy。注意:只能代理接口。

  1. 实现InvocationHandler接口,重写invoke方法;
  2. 创建要被代理的接口;
  3. 通过ProxynewProxyInstance方法,创建代理对象;
  4. 用代理对象调用其真实对应的方法。

# 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事务四种实现方式

  1. 基于TransactionTemplate编程式事务管理实现;
  2. 基于TransactionProxyFactoryBean声明式事务管理;
  3. 基于AspectJXML声明式事务管理;
  4. 基于@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;
        });
   }
1
2
3
4
5
6
7
8
9
10
11
12

相较于@Transactional注解声明式事务,更建议大家使用基于TransactionTemplate的编程式事务。

主要原因如下:

  • 避免由于Spring AOP问题,导致事务失效的问题。
  • 能够更小粒度的控制事务的范围,更直观。

建议在项目中少使用@Transactional注解开启事务。但并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

# 3.3 Spring声明式事务

Spring事务底层是基于数据库事务AOP机制的。

实现步骤:

  1. 首先对于使用了@Transactlonal注解的Bean,Spring会创建一个代理对象作为Bean;
  2. 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解;
  3. 如果加了,那么则利用事务管理器创建一个数据库连接;并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的一步;
  4. 然后执行当前方法,方法中会执行SQL;
  5. 执行完当前方法后,如果没有出现异常就直接提交事务;
  6. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务;

# 3.4 Spring事务的隔离级别

Spring事务的隔离级别对应的就是数据库的隔离级别

  1. 未提交读:事务可以读取未提交的数据。
  2. 读已提交:一个事务只能看见已提交事务所做的修改。
  3. 可重复读 RR默认):保证了在同一个事务中,多次读取同样记录的结果是一致的。
  4. 串行化:强制事务串行执行,会在读取的每一行数据都加锁

# 3.5 Spring事务的传播机制

Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的。

Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在新数据库连接上执行SQL。

spring目前支持7种传播特性:

  1. REQUIRED :如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
  2. SUPPORTS :如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
  3. MANDATORY :如果当前上下文中存在事务,否则抛出异常。
  4. REQUIRES_NEW :每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
  5. NOT_SUPPORTED :如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
  6. NEVER :如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
  7. 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;
1
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、获取BeangetBean()

context.getBean();
1

# 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结合使用。
  • @MapperMybatis的注解,和Spring没有关系;@RepositorySpring的注解。

# @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失效场景

一、事务不生效【七种】

  1. 方法访问权限问题 (只有public方法会生效);
  2. 方法用final修饰,不会生效;
  3. 同一个类中的方法直接内部调用,会导致事务失效;
  4. 类本身未被Spring管理;
  5. 多线程调用;
  6. 表(存储引擎))不支持事务;
  7. 项目未开启事务;

二、事务不回滚【五种】

  1. 错误的传播特性;
  2. 自己手动捕获了异常;

# 1、方法访问权限非public

众所周知,java的访问权限主要有四种:private、default、protected、public;它们的权限从左到右,依次变大。

但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题 (只有public方法会生效)。

例如:

@Service
public class UserService {
    
    @Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}
1
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);
    }
}
1
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();
    }
}
1
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();
    }
 }
1
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();
    }
 }
1
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);
    }    
}
1
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表数据");
    }
}
1
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");
1
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> 
1
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);
    }
}
1
2
3
4
5
6
7
8

这种类型的传播特性不支持事务,如果有事务则会抛异常。

目前只有这三种传播特性才会创建新事务REQUIREDREQUIRES_NEWNESTED

# 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);
        }
    }
}
1
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);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上面的这种情况,开发人员自己捕获了异常,又手动抛出了异常Exception,事务同样不会回滚。

  • 因为Spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误);
  • 对于普通的Exception(非运行时异常),它不会回滚,比如常见的IOExeptionSQLException

# 11、自定义了回滚异常

在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,Spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。但如果这个参数的值设置错了,就会引出一些莫名其妙的问题。

例如:

@Slf4j
@Service
public class UserService {
    
    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}
1
2
3
4
5
6
7
8
9
10

如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlExceptionDuplicateKeyException等异常。而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表数据");
    }
}
1
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);
        }
    }
}
1
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);
    }
}
1
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);
1
2

在RoleService类中,只有这一行需要事务:

saveData(userModel);
1

优化方案二:使用编程式事务

利用TransactionTemplate 编程式事务的方法。

   @Autowired
   private TransactionTemplate transactionTemplate;
   
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }
1
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。