Spring 核心 —— 数据访问与事务管理
1. 核心理论:Spring 数据访问的演进
在传统的 Java 应用中,直接使用 JDBC (Java Database Connectivity, Java 数据库连接) 进行数据库操作非常繁琐,需要手动管理连接、Statement、ResultSet,并且要处理大量的异常和资源关闭。Spring 提供了强大的数据访问抽象层,极大地简化了这一过程。
- Spring JDBC 抽象: Spring 对 JDBC 进行了封装,提供了
JdbcTemplate等工具类,消除了冗余的 JDBC 代码,让开发者只需关注 SQL 语句和参数。 - ORM 框架集成: Spring 可以无缝集成各种 ORM (Object-Relational Mapping, 对象关系映射) 框架,如 Hibernate, MyBatis 等,通过 IoC/DI 统一管理它们的生命周期和配置。
2. 深度剖析:事务管理
事务(Transaction) 是数据库管理系统(DBMS)执行过程中的一个逻辑单位,它由一个或多个数据库操作组成,这些操作共同完成一个不可分割的任务。事务的目的是为了确保数据的完整性和一致性。
2.0 生活比喻:银行转账操作
最经典的例子就是银行转账:从账户A扣款100元,并向账户B存款100元。这两个操作必须被视为一个整体,要么全部成功,要么全部失败。
事务的 ACID 特性
ACID是衡量一个事务是否可靠的四个核心特性。它们分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
2.1 原子性 (Atomicity)
- 定义:事务被视为一个不可分割的最小工作单元,事务中的所有操作要么全部成功提交 (commit),要么全部失败回滚 (rollback)。
- 实现原理:主要由数据库的
Undo Log(回滚日志)来保证。Undo Log记录的是数据被修改之前的旧值。当事务需要回滚时,数据库可以根据Undo Log将数据恢复到事务开始前的状态。
2.2 一致性 (Consistency)
- 定义:事务的执行必须使数据库从一个一致性状态转变到另一个一致性状态。数据库的完整性约束没有被破坏。
- 核心地位:一致性是事务的最终目的。原子性、隔离性和持久性都是为了保证一致性而存在的手段。
2.3 隔离性 (Isolation)
- 定义:一个事务的执行不能被其他并发执行的事务干扰。
- 实现原理:主要通过锁机制 (Locking) 和多版本并发控制 (MVCC, Multi-Version Concurrency Control) 来实现。为了在隔离性和性能之间取得平衡,SQL标准定义了四种隔离级别。
2.3.1 并发问题定义
- 脏读 (Dirty Read):一个事务读取到了另一个事务尚未提交的数据。
- 不可重复读 (Non-Repeatable Read):在一个事务内,两次读取同一行数据,但得到了不同的结果。
- 幻读 (Phantom Read):在一个事务内,两次执行相同的范围查询,但第二次查询返回了第一次查询中没有的“幻影”行。
2.3.2 隔离级别详解
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 (Read Uncommitted) | 可能 | 可能 | 可能 |
| 读已提交 (Read Committed) | 避免 | 可能 | 可能 |
| 可重复读 (Repeatable Read) | 避免 | 避免 | 可能 |
| 串行化 (Serializable) | 避免 | 避免 | 避免 |
- 读已提交 是大多数数据库(如Oracle, SQL Server)的默认级别。
- 可重复读 是MySQL InnoDB引擎的默认级别。值得注意的是,InnoDB通过其独特的 MVCC 和 Next-Key Locking 机制,在很大程度上也避免了幻读。
2.4 持久性 (Durability)
- 定义:一旦事务被成功提交,它对数据库中数据的改变就是永久性的。
- 实现原理:主要由数据库的
Redo Log(重做日志)和Write-Ahead Logging (WAL)预写日志策略来保证。在数据写入磁盘前,变更操作会先被写入持久化的Redo Log。当系统崩溃后,可以通过Redo Log来恢复数据。
3. ACID 实现原理总结
- A (原子性):通过
Undo Log(回滚日志) 实现。 - C (一致性):是最终目的,由应用层和数据库共同保证,A、I、D是其基础。
- I (隔离性):通过 锁 (Locking) 和 MVCC (多版本并发控制) 实现。
- D (持久性):通过
Redo Log(重做日志) 实现。
4. 事务传播行为 (Propagation Behavior)
解决了什么问题? 解决了当一个已存在事务的方法调用另一个需要事务的方法时,这两个事务应该如何协作的问题。
Spring 定义了七种传播行为,它们共同决定了事务方法在相互调用时,事务是如何创建、加入、挂起或以非事务方式运行的。
支持当前事务:
REQUIRED(默认, 必需的):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是最常用的设置,保证了方法总是在一个事务内执行。SUPPORTS(支持):如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。表示“我能支持事务,但不是必须的”。MANDATORY(强制):强制要求必须在一个已有的事务中运行。如果当前没有事务,则会抛出异常。
不支持当前事务:
REQUIRES_NEW(需要新事务):总是创建一个新的事务,并挂起当前事务(如果存在)。新事务与外部事务完全独立,拥有自己的提交/回滚。常用于需要独立提交或回滚的非核心逻辑,如记录日志。NOT_SUPPORTED(不支持):以非事务方式执行操作,如果当前存在事务,则挂起当前事务。NEVER(从不):总是以非事务方式运行。如果当前存在事务,则会抛出异常。
嵌套事务:
NESTED(嵌套):如果当前存在事务,则在嵌套事务内执行。嵌套事务是外部事务的一个子事务,它有自己的保存点 (Savepoint)。如果嵌套事务回滚,它只会回滚到自己的保存点,而不会影响外部事务。如果外部事务回滚,嵌套事务也会一起回滚。如果当前没有事务,则行为等同于REQUIRED。
5. Spring 事务管理方式
Spring 提供了两种主要的事务管理方式:编程式事务和声明式事务。
5.1 编程式事务 (Programmatic Transaction Management)
- 特点:通过在代码中显式调用事务管理 API 来控制事务的开启、提交和回滚。
- 实现方式:
PlatformTransactionManager: 直接使用 Spring 提供的事务管理器接口。你需要手动获取事务、提交事务、回滚事务。TransactionTemplate: Spring 提供的模板类,它封装了事务的样板代码,使得编程式事务管理更加简洁。
- 优点:极致的灵活性。可以在代码的任何地方精确地控制事务的边界。
- 缺点:代码侵入性强。事务管理代码与业务逻辑代码耦合在一起,增加了代码的复杂性和维护成本。
- 使用场景:通常只在需要进行非常细粒度事务控制的特殊场景下使用。
5.2 声明式事务 (Declarative Transaction Management) - 推荐
- 特点:将事务管理逻辑与业务逻辑代码完全解耦,通过配置或注解来“声明”事务的属性。
- 底层原理:基于 AOP (面向切面编程) 实现。当一个方法被
@Transactional标记时,Spring 会为这个 Bean 创建一个代理对象。当通过代理对象调用这个方法时,代理会在方法执行前后插入事务管理逻辑(开启事务、提交/回滚)。 - 优点:代码侵入性低,业务代码可以专注于业务逻辑。配置集中,易于维护。
- 实现方式:最常用的方式是使用
@Transactional注解,但也可以通过 XML 配置。
@Transactional 注解方式
@Transactional 注解是声明式事务的核心,它有多个重要属性:
propagation: 设置事务的传播行为,默认值:Propagation.REQUIREDPropagation.REQUIRED的行为:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是最常用的传播行为。- 其他常用行为:
- Propagation.REQUIRES_NEW:总是创建一个全新的、独立的事务。如果外部已有事务,会将其挂起。
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
- Propagation.NOT_SUPPORTED:总是以非事务方式执行。如果外部已有事务,会将其挂起。
isolation: 设置事务的隔离级别,常用级别:Isolation.READ_COMMITTED, Isolation.REPEATABLE_READ等。readOnly: 将事务设置为只读。这是一个优化,数据库可以针对只读事务进行性能优化。timeout: 设置事务的超时时间(秒)。rollbackFor/noRollbackFor: 精确控制哪些异常会触发事务回滚,哪些不会。
XML 配置方式
在现代 Spring 开发中,@Transactional 注解是更主流的方式。但在一些老项目或需要集中管理事务规则的场景下,XML 配置也仍然可见。
主要步骤:
- 定义数据源 (DataSource) 和 事务管理器 (PlatformTransactionManager)。
- 配置事务通知 (
<tx:advice>):定义事务的切面逻辑(如传播行为、回滚规则)。 - 配置 AOP (
<aop:config>):将事务通知应用到特定的方法上(即定义切入点)。
详细的 XML 配置示例:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:tx="http://www.springframework.org/schema/tx"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 1. 数据源和事务管理器配置 (与注解方式相同) --><bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"><!-- ... 属性配置 ... --></bean><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><!-- 2. 配置事务通知 (Transaction Advice) --><tx:advice id="txAdvice" transaction-manager="transactionManager"><tx:attributes><!-- 对所有以 'save', 'insert', 'update', 'delete' 开头的方法,应用 REQUIRED 传播行为 --><tx:method name="save*" propagation="REQUIRED"/><tx:method name="insert*" propagation="REQUIRED"/><tx:method name="update*" propagation="REQUIRED"/><tx:method name="delete*" propagation="REQUIRED"/><!-- 对所有以 'get', 'find', 'query' 开头的方法,设置为只读事务 --><tx:method name="get*" read-only="true"/><tx:method name="find*" read-only="true"/><tx:method name="query*" read-only="true"/><!-- 其他方法使用默认事务设置 --><tx:method name="*"/></tx:attributes></tx:advice><!-- 3. 配置 AOP 切面,将事务通知和切入点关联起来 --><aop:config><!-- 定义切入点:com.study.spring.transaction.service 包及其子包下所有类的所有方法 --><aop:pointcut id="serviceMethods" expression="execution(* com.study.spring.transaction.service..*.*(..))"/><!-- 将事务通知应用到切入点 --><aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/></aop:config><!-- 4. 定义需要事务管理的业务 Bean --><bean id="userService" class="com.study.spring.transaction.service.UserService"/><!-- ... 其他 Bean 定义 ... --></beans>
5.3 @Transactional 失效的常见“坑” (高频面试题)
-
应用在非
public方法上:- Spring AOP 默认使用的是 JDK 动态代理或 CGLIB 代理。代理对象在方法执行前插入事务逻辑,如果方法不是
public的,代理无法拦截,导致事务失效。
- Spring AOP 默认使用的是 JDK 动态代理或 CGLIB 代理。代理对象在方法执行前插入事务逻辑,如果方法不是
-
内部调用失效 (Self-invocation):
- 在同一个类中,一个没有
@Transactional注解的方法调用另一个有@Transactional注解的方法时,事务会失效。 - 原因:内部调用是直接通过
this引用调用的,它指向的是原始对象,而不是 Spring AOP 创建的代理对象,因此无法触发事务切面。@Service public class MyService {public void methodA() {// this.methodB(); // 事务失效,因为不是通过代理对象调用// 正确做法:通过 Spring 注入自己来调用,或者将 methodB 抽取到另一个 Service// myService.methodB();}@Transactionalpublic void methodB() { /* ... */ } }
- 在同一个类中,一个没有
-
异常类型与回滚策略不匹配:
@Transactional默认只在捕获到运行时异常 (RuntimeException) 和Error时才回滚事务。- 对于受检异常 (Checked Exception),默认是不会回滚的。如果你希望在捕获到受检异常时也回滚,需要明确指定
rollbackFor属性。@Transactional(rollbackFor = IOException.class) public void doSomething() throws IOException { /* ... */ }
-
异常被
catch块“吃掉”:- 如果
@Transactional方法内部的try-catch块捕获了异常(尤其是RuntimeException),但没有在catch块中将其重新抛出,那么 Spring 的事务管理器就无法感知到异常的发生,事务也就不会回滚。
- 如果
-
数据库引擎不支持事务:
- 如果底层数据库表的存储引擎(如 MySQL 的 MyISAM)不支持事务,那么 Spring 的事务注解也无法生效。必须确保使用支持事务的存储引擎(如 InnoDB)。
-
配置错误:
- 忘记在 Spring 配置类上添加
@EnableTransactionManagement注解来开启声明式事务管理。
- 忘记在 Spring 配置类上添加
6. 源码学习 (简要):@Transactional 的实现
@Transactional 注解的实现是基于 Spring AOP 的。当一个方法被 @Transactional 标记时,Spring 会为这个 Bean 创建一个代理对象。当通过代理对象调用这个方法时,代理会拦截方法调用,并在方法执行前后插入事务管理逻辑。
TransactionInterceptor(事务拦截器): 这是 Spring 事务管理的核心拦截器。它负责在目标方法执行前开启事务,在方法执行成功后提交事务,在方法抛出异常时回滚事务。PlatformTransactionManager(平台事务管理器): 事务管理器接口,Spring 提供了多种实现,如DataSourceTransactionManager(用于 JDBC/MyBatis)、HibernateTransactionManager(用于 Hibernate) 等。