Spring事务和高级特性

5 阅读16分钟

Spring 事务核心与高级特性详解

一、Spring 事务核心(原理+传播行为+隔离级别)

1.1 事务核心原理

1.1.1 事务的本质与Spring事务的定位

事务(Transaction)是数据库操作的最小不可分割单元,核心满足ACID特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。Spring事务并非独立实现事务机制,而是对数据库事务的封装与增强,底层依赖数据库自身的事务支持(如MySQL的InnoDB引擎支持事务,MyISAM不支持),通过AOP(面向切面编程)和动态代理,实现事务的声明式管理,降低开发耦合度。

1.1.2 Spring事务的核心实现机制

Spring事务的核心是 PlatformTransactionManager(事务管理器),它是一个接口,定义了事务的核心操作(提交、回滚、获取事务状态),不同的持久层框架对应不同的实现类:

  • DataSourceTransactionManager:最常用,对应JDBC、MyBatis等基于数据源的持久层框架,管理数据库连接的事务,底层通过Connection.setAutoCommit(false)开启事务,提交时调用Connection.commit(),回滚时调用Connection.rollback()

  • HibernateTransactionManager:对应Hibernate框架,管理Hibernate的Session事务。

  • JtaTransactionManager:用于分布式事务,整合JTA(Java Transaction API),支持多数据源、跨服务的事务管理。

Spring事务的执行流程(以声明式事务为例):

  1. 通过@Transactional注解标记需要事务管理的方法(AOP切面切入点)。

  2. 程序调用该方法时,AOP动态代理拦截方法调用,通过PlatformTransactionManager获取事务状态(TransactionStatus)。

  3. 开启事务(关闭自动提交),执行目标方法逻辑。

  4. 若方法正常执行,调用事务管理器的commit()方法提交事务,恢复自动提交状态。

  5. 若方法抛出未被捕获的异常(默认仅回滚RuntimeExceptionError),调用rollback()方法回滚事务,恢复自动提交状态。

1.1.3 声明式事务与编程式事务对比

类型实现方式优点缺点适用场景
声明式事务@Transactional注解、XML配置代码侵入性低、配置简单、易维护,符合面向切面思想灵活性不足,无法精准控制事务的提交/回滚时机绝大多数业务场景,无需手动控制事务细节
编程式事务TransactionTemplate、手动调用事务管理器API灵活性高,可精准控制事务边界、提交/回滚时机代码侵入性强,需手动编写事务控制逻辑复杂业务场景(如多数据源切换、条件性提交/回滚)

1.2 事务传播行为(Transaction Propagation)

1.2.1 传播行为的定义

事务传播行为定义了当一个带有事务的方法调用另一个方法时,事务如何传递(即被调用方法是否加入当前事务,还是创建新事务)。Spring定义了7种传播行为,均在Propagation枚举类中,核心是解决“嵌套事务”的执行规则问题。

1.2.2 7种传播行为详解(重点掌握前5种)

  • REQUIRED(默认值):如果当前存在事务,则加入当前事务;如果当前没有事务,则创建一个新事务。

    场景:最常用,如Service层方法调用Dao层方法,Dao层方法无需单独事务,复用Service层事务。
    
  • SUPPORTS:如果当前存在事务,则加入当前事务;如果当前没有事务,则以非事务方式执行。

    场景:方法本身不依赖事务,但如果上层有事务则参与(如查询方法,可复用事务提升效率)。
    
  • MANDATORY:如果当前存在事务,则加入当前事务;如果当前没有事务,则抛出异常(IllegalTransactionStateException)。

    场景:方法必须在事务中执行(如核心业务的更新操作,确保有事务保障)。
    
  • REQUIRES_NEW:无论当前是否存在事务,都创建一个新事务,原事务(如果有)会被挂起,直到新事务执行完成。

    场景:方法需要独立事务,不受上层事务影响(如日志记录,即使核心业务回滚,日志也需保留)。
    
  • NOT_SUPPORTED:无论当前是否存在事务,都以非事务方式执行,原事务(如果有)会被挂起。

    场景:方法不需要事务,且避免占用事务资源(如耗时的查询、文件导出,不影响事务一致性)。
    
  • NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

    场景:明确禁止事务(如某些只读方法,防止误操作开启事务)。
    
  • NESTED:如果当前存在事务,则在当前事务内创建一个嵌套事务(子事务);如果当前没有事务,则创建一个新事务。子事务依赖于父事务,父事务回滚,子事务也回滚;子事务回滚,父事务可选择继续执行或回滚。

    注意:与REQUIRES_NEW的区别——NESTED是“嵌套事务”(同一事务内的子单元),REQUIRES_NEW是“独立事务”(完全独立的两个事务);NESTED依赖数据库的保存点(Savepoint),仅支持部分数据库(如MySQL InnoDB、Oracle)。
    

1.2.3 传播行为实战注意事项

  1. 同一类中方法调用:由于Spring AOP是动态代理,同一类中带@Transactional的方法调用另一方法时,传播行为不生效(代理对象未被调用),需通过自注入(@Autowired自身)或ApplicationContext获取代理对象。

    1. 异常影响:传播行为的执行结果会受异常抛出范围影响,如REQUIRES_NEW的子事务抛出异常,仅回滚子事务,父事务不受影响(需手动捕获子事务异常)。

1.3 事务隔离级别(Transaction Isolation)

1.3.1 隔离级别的定义

事务隔离级别用于解决并发事务带来的问题(脏读、不可重复读、幻读),隔离级别越高,并发安全性越强,但性能越低。Spring的隔离级别底层依赖数据库的隔离级别,Spring仅提供统一的配置入口,不单独实现隔离逻辑。

1.3.2 并发事务的3个核心问题

  • 脏读:事务A读取了事务B未提交的数据,随后事务B回滚,事务A读取的数据是“无效”的(脏数据)。

  • 不可重复读:事务A多次读取同一数据,期间事务B修改并提交了该数据,导致事务A多次读取的结果不一致(侧重“修改”)。

  • 幻读:事务A查询符合条件的数据集,期间事务B插入/删除了符合条件的数据,导致事务A再次查询时,结果集数量发生变化(侧重“新增/删除”)。

1.3.3 Spring的5种隔离级别(对应数据库4种+默认)

  • DEFAULT(默认值):继承数据库的默认隔离级别(MySQL默认REPEATABLE READ,Oracle默认READ COMMITTED)。

  • READ_UNCOMMITTED:最低隔离级别,允许读取未提交的数据,会出现脏读、不可重复读、幻读。

    场景:极少使用,仅用于对数据一致性要求极低、追求极致性能的场景(如统计临时数据)。
    
  • READ_COMMITTED:允许读取已提交的数据,避免脏读,但会出现不可重复读、幻读。

    场景:大多数业务场景(如电商订单、用户管理),平衡一致性和性能,是Oracle的默认隔离级别。
    
  • REPEATABLE READ:保证同一事务内多次读取同一数据的结果一致,避免脏读、不可重复读,但会出现幻读。

    说明:MySQL InnoDB通过“MVCC(多版本并发控制)”机制,在REPEATABLE READ级别下避免了幻读(逻辑上的避免,非绝对),是MySQL的默认隔离级别。
    
  • SERIALIZABLE:最高隔离级别,将事务串行执行(相当于单线程),避免所有并发问题,但性能极差。

    场景:数据一致性要求极高的场景(如金融转账、账务核算),极少用于高并发系统。
    

1.3.4 隔离级别与并发问题对应关系

隔离级别脏读不可重复读幻读
READ_UNCOMMITTED存在存在存在
READ_COMMITTED不存在存在存在
REPEATABLE READ不存在不存在MySQL InnoDB不存在,其他数据库存在
SERIALIZABLE不存在不存在不存在

1.3.5 实战注意事项

  1. 隔离级别配置:通过@Transactional(isolation = Isolation.XXX)配置,优先级高于数据库默认隔离级别。

    1. 性能权衡:高并发系统优先选择READ_COMMITTED,避免使用SERIALIZABLE;数据一致性要求高的场景,可结合乐观锁、悲观锁辅助。

    2. MySQL与Oracle差异:Oracle不支持READ_UNCOMMITTED和REPEATABLE READ,默认是READ_COMMITTED,若配置REPEATABLE READ,会自动降级为READ_COMMITTED。

二、Spring 高级特性(作用域+注入问题+性能优化)

2.1 Spring Bean作用域(Scope)

2.1.1 作用域的定义

Spring Bean的作用域定义了Bean实例的创建时机、生命周期和可见范围,Spring通过作用域管理Bean的实例,避免不必要的实例创建,提升性能,同时满足不同业务场景的需求。默认情况下,Spring Bean的作用域是singleton(单例)

2.1.2 6种核心作用域(Spring 5+)

  • singleton(单例,默认):整个Spring容器中,Bean仅创建一个实例,全局共享,生命周期与容器一致(容器启动时创建,容器关闭时销毁)。

    实现原理:Spring通过“饿汉式”(默认)或“懒汉式”(配置lazy-init="true")创建单例Bean,底层通过ConcurrentHashMap缓存Bean实例,确保线程安全。
    
    场景:无状态Bean(如Service、Dao、工具类),无成员变量或成员变量为常量,可复用。
    
    注意:单例Bean不是线程安全的,若存在可修改的成员变量,需通过锁(synchronized)或ThreadLocal保证线程安全。
    
  • prototype(原型):每次获取Bean(通过getBean()或依赖注入)时,都会创建一个新的实例,Spring不管理原型Bean的生命周期(创建后交给调用者管理,容器关闭时不主动销毁)。 场景:有状态Bean(如实体类、请求级别的Bean),每个请求/操作需要独立的实例。

    注意:原型Bean不能与单例Bean配合使用(单例Bean初始化时注入原型Bean,仅注入一次,后续复用该实例,失去原型意义),需通过ObjectFactory或Provider动态获取。
    
  • request(请求域):仅适用于Web应用(Spring MVC),每个HTTP请求创建一个Bean实例,请求结束后Bean被销毁。 场景:请求级别的数据存储(如请求上下文、用户临时信息),需配合Spring MVC的WebApplicationContext使用。

  • session(会话域):仅适用于Web应用,每个HTTP Session创建一个Bean实例,会话过期后Bean被销毁。

    场景:会话级别的数据存储(如用户登录信息、购物车)。
    
  • application(应用域):仅适用于Web应用,整个Web应用(ServletContext)中仅创建一个Bean实例,生命周期与ServletContext一致。

    区别于singleton:singleton是Spring容器级别的单例,application是Web应用级别的单例(若多个Spring容器共享一个ServletContext,application Bean全局唯一)。
    
  • websocket(WebSocket域):仅适用于WebSocket应用,每个WebSocket会话创建一个Bean实例,会话关闭后Bean被销毁。

    场景:WebSocket通信中的会话级Bean(如用户会话处理器)。
    

2.1.3 作用域实战配置与注意事项

  1. 配置方式:

    • 注解方式:@Scope("singleton")、@Scope("prototype"),可结合@Lazy注解实现单例Bean懒加载。

    • XML方式:。

    1. 单例与原型的冲突解决:

    当单例Bean需要依赖原型Bean时,使用ObjectFactory<T>@Autowired Provider<T>动态获取,每次调用getObject()都会获取新的原型实例。

    示例: @Autowired private Provider<User> userProvider; public void test() { User user1 = userProvider.get(); User user2 = userProvider.get(); // user1 != user2 }

2.2 Spring 依赖注入(DI)核心问题与解决方案

2.2.1 依赖注入的核心原理

依赖注入(Dependency Injection)是Spring IOC(控制反转)的核心实现,将Bean的依赖关系由Spring容器主动注入,而非Bean自身创建,降低Bean之间的耦合度。核心注入方式有3种:构造器注入、setter注入、字段注入。

2.2.2 3种注入方式对比与最佳实践

注入方式实现方式优点缺点最佳实践
构造器注入@Autowired标注构造器,或无注解(Spring 4.3+,单构造器默认注入)强制依赖注入,确保Bean初始化时依赖齐全;不可变(final修饰字段);线程安全依赖过多时,构造器参数冗长推荐使用,尤其是依赖较少(1-3个)的Bean;适合注入不可变依赖
setter注入@Autowired标注setter方法支持可选依赖(可设置默认值);灵活性高,可动态修改依赖Bean初始化后可能依赖未注入;不可变字段无法注入适合可选依赖、动态依赖场景
字段注入@Autowired直接标注字段(无需setter、构造器)代码简洁,开发效率高耦合度高;无法注入final字段;单元测试困难(无法mock注入);不支持构造器注入的强制依赖校验不推荐使用(除非简单demo),生产环境尽量避免

2.2.3 依赖注入的核心问题及解决方案

问题1:循环依赖(Circular Dependency)

定义:两个或多个Bean互相依赖(如A依赖B,B依赖A),导致Spring容器无法完成初始化,抛出BeanCurrentlyInCreationException异常。

Spring支持的循环依赖场景:

- 单例Bean + 构造器注入:不支持(无法解决,必须避免)。

- 单例Bean + setter注入/字段注入:支持,底层通过“三级缓存”解决。

三级缓存原理(核心):

1. 一级缓存(singletonObjects):存储完全初始化完成的单例Bean。

2. 二级缓存(earlySingletonObjects):存储提前暴露的、未完全初始化的单例Bean(仅实例化,未注入依赖)。

3. 三级缓存(singletonFactories):存储Bean的工厂对象,用于生成提前暴露的Bean实例。

流程:当A依赖BB依赖A时,Spring先实例化A,将A的工厂对象放入三级缓存,再去实例化BB实例化后,需要注入A,从三级缓存获取A的实例(提前暴露),注入B并完成B的初始化,将B放入一级缓存;再回到A的初始化,注入B,完成A的初始化,将A放入一级缓存,删除三级、二级缓存中的A

解决方案:

- 避免构造器注入的循环依赖,改用setter注入。

- 拆分Bean的依赖关系,将共同依赖抽取为独立Bean。

- 使用@Lazy注解,延迟加载其中一个Bean,打破循环依赖。
问题2:依赖注入失败(NoSuchBeanDefinitionException)

常见原因及解决方案:

1. Bean未被Spring容器扫描:确保Bean所在包被@ComponentScan扫描(如@SpringBootApplication的扫描范围),或通过@Bean、XML配置注册Bean。

2. 注入类型不匹配:注入时指定的类型(如接口)没有对应的实现类,或存在多个实现类(需用@Qualifier指定Bean名称)。

3. 单例Bean依赖原型Bean:如前文所述,使用ObjectFactory或Provider动态获取。

4. Bean的作用域不匹配:如request域Bean被单例Bean注入(单例Bean生命周期长于request域,导致注入失败),需动态获取。
问题3:多个实现类的注入冲突

当一个接口有多个实现类,使用@Autowired注入时,Spring无法确定注入哪个实例,抛出NoUniqueBeanDefinitionException异常。

解决方案:

- @Qualifier("beanName"):指定要注入的Bean名称(与@Bean的name属性或类名首字母小写一致)。

- @Primary:在其中一个实现类上标注,指定默认注入的Bean。

- 手动通过@Bean配置,明确注入的实例。

2.3 Spring 性能优化实战

Spring性能优化的核心思路:减少Bean的创建和销毁成本、减少容器扫描开销、优化依赖注入效率、避免不必要的资源占用,结合业务场景针对性优化。

2.3.1 Bean相关优化

  • 合理选择Bean作用域:无状态Bean优先使用singleton(单例),避免频繁创建原型Bean;有状态Bean使用prototype,避免单例线程安全问题。

  • 单例Bean懒加载:通过@Lazy注解或XML配置lazy-init="true",让单例Bean在第一次被使用时创建,而非容器启动时创建,减少容器启动时间(适合大型项目,Bean数量多的场景)。 注意:懒加载的Bean无法在容器启动时校验依赖问题,需在测试阶段重点验证。

  • 减少Bean的数量:合并功能相似的Bean,避免冗余Bean;工具类可通过静态方法实现,无需注入Spring容器。

  • 避免不必要的Bean初始化逻辑:Bean的@PostConstruct方法中避免执行耗时操作(如数据库查询、远程调用),可将耗时操作异步执行或延迟执行。

2.3.2 容器扫描与配置优化

  • 精准配置@ComponentScan扫描范围:避免扫描无关包(如第三方依赖包、非Spring Bean包),减少容器扫描时间。

    示例:@ComponentScan(basePackages = "com.xxx.service, com.xxx.dao"),仅扫描业务层和数据层。
    
  • 避免使用@ComponentScan("com.xxx")(全包扫描):全包扫描会遍历包下所有类,耗时较长,尤其在大型项目中。

  • 优先使用JavaConfig配置,替代XML配置:JavaConfig(@Configuration + @Bean)的解析效率高于XML配置,且类型安全,便于维护。

  • 关闭不必要的自动配置(Spring Boot):Spring Boot的自动配置会加载大量默认Bean,若不需要某些功能(如DataSourceAutoConfiguration),可通过@SpringBootApplication(exclude = XXX.class)关闭,减少容器负担。

2.3.3 依赖注入与AOP优化

  • 优先使用构造器注入:构造器注入的效率高于setter注入和字段注入,且能避免循环依赖问题,提升Bean初始化效率。

  • 减少AOP切面的范围:AOP的动态代理会带来一定性能开销,避免使用@Aspect注解的切面扫描全包,通过execution表达式精准匹配需要增强的方法(如execution(* com.xxx.service..(..)))。

  • 避免频繁获取Bean实例:多次获取同一Bean时,缓存Bean实例,避免频繁调用applicationContext.getBean()(该方法会触发Bean的查找和初始化,耗时较长)。

2.3.4 其他性能优化点

  • 使用Spring的缓存机制:通过@Cacheable、@CacheEvict等注解,缓存频繁访问的数据(如查询结果),减少数据库查询或远程调用的开销。

  • 异步处理耗时操作:通过@Async注解,将耗时操作(如文件上传、邮件发送)异步执行,避免阻塞主线程,提升系统响应速度。

  • 优化数据库相关Bean:DataSource、SqlSessionFactory等核心Bean,配置合理的连接池大小(如HikariCP的maximumPoolSize),避免连接池不足或浪费。

  • 避免Bean的循环依赖:虽然Spring支持单例Bean的循环依赖,但循环依赖会增加容器初始化的复杂度和耗时,尽量在设计阶段避免。

2.4 高级特性实战总结

  1. 作用域:根据Bean的状态选择合适的作用域,解决单例与原型的冲突,避免线程安全问题。

    1. 注入问题:优先使用构造器注入,规避循环依赖和注入冲突,确保Bean初始化的稳定性。

    2. 性能优化:从Bean、容器、AOP、业务逻辑多维度优化,平衡系统性能和开发效率,重点关注容器启动速度和Bean初始化效率。