一、什么是事务?
在软件开发领域,大家对事务的概念想必都不陌生,事务简单来说,就是逻辑上的一组操作,这组操作具有一个显著特点:要么全部都得以执行,要么就全部都不执行,就如同一个不可分割的整体。
接下来,咱们结合日常实际开发场景来深入聊聊事务。
在我们所开发的系统里,每个业务方法往往包含了多个原子性的数据库操作。就拿下面这个 savePerson()
方法来说吧:
public void savePerson() {
// 这里首先通过personDao将人员基本信息保存到数据库
personDao.save(person);
// 接着通过personDetailDao将人员详细信息也保存到数据库
personDetailDao.save(personDetail);
}
可以看到,在这个方法中,对人员基本信息和详细信息的保存操作,它们各自都是原子性的数据库操作,而且这两个操作之间是存在依赖关系的哦。也就是说,它们必须要么同时成功完成,要么同时不执行,这样才能保证数据的完整性和一致性。
这里还得特别强调一点,那就是事务能否真正生效,数据库引擎是否支持事务可是至关重要的因素呀。就拿我们常用的 MySQL 数据库来说吧,它默认使用的是支持事务的 InnoDB 引擎。但要是把数据库引擎换成了 MyISAM,那可就麻烦啦,程序在这种情况下可就不再支持事务咯。
说到事务,最经典且常常被拿来举例的就是转账操作啦。想象一下,小明要给小红转账 1000 元呢,这看似简单的一个转账行为,实际上会涉及到两个关键的操作步骤哦:
第一步,得把小明账户的余额减少 1000 元;
第二步,要将小红账户的余额增加 1000 元。
可要是在这两个操作进行的过程当中,突然出现了一些意外情况,比如说银行系统崩溃啦,或者网络出现故障之类的,结果导致小明的余额已经减少了,但是小红的余额却没有增加,这显然是不对的呀,数据就出现不一致啦。而事务的存在呢,就是为了确保这两个关键操作要么都能够成功完成,要么一旦出现问题就都全部失败回滚,从而保证数据始终处于正确的状态。
再来看下面这个 OrdersService
类中的 accountMoney()
方法,它也是一个很好的例子呢:
public class OrdersService {
private AccountDao accountDao;
// 通过这个方法设置AccountDao实例,方便后续数据库操作
public void setOrdersDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT, readOnly = false, timeout = -1)
public void accountMoney() {
// 首先,让小红的账户余额增加1000元
accountDao.addMoney(1000, "xiaohong");
// 这里模拟一个突然出现的异常情况,就好比银行里可能突然停电之类的情况哦
// 如果没有配置合适的事务管理机制的话,就会出现问题啦,比如会造成小红账户多了1000元,而小明账户却没有少钱的情况
int i = 10 / 0;
// 最后,让小王的账户余额减少1000元
accountDao.reduceMoney(1000, "xiaoming");
}
}
在这个方法里,如果没有事务管理的保障,一旦中间出现异常,就很可能导致数据出现不一致的情况。但有了 @Transactional
注解来标记这个方法,就意味着这个方法内的操作会在事务的管控之下进行啦。要是在执行过程中出现异常,事务就会自动按照设定的规则进行回滚操作,从而保证数据的正确性和一致性呢。
二、事务的四大特性了解么?
事务在逻辑上是一组操作,这些操作要么全部执行,要么全部不执行。它主要是针对数据库而言的,比如MySQL。为了保证事务是正确可靠的,在数据库进行写入或者更新操作时,就必须表现出ACID的4个重要特性:
-
原子性(
Atomicity
):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; -
一致性(
Consistency
):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的; -
隔离性(
Isolation
):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; - 持久性(
Durability
):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
三、事务的隔离级别
-
未提交读(Read uncommitted):最低的隔离级别,允许“脏读”(dirty reads),事务可以看到其他事务“尚未提交”的修改。如果另一个事务回滚,那么当前事务读到的数据就是脏数据。
-
提交读(Read committed):一个事务可能会遇到不可重复读(Non-Repeatable Read)的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。
-
可重复读(Repeatable read):一个事务可能会遇到幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就出现了。
-
串行化(Serializable):最严格的隔离级别,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。虽然Serializable隔离级别下的事务具有最高的安全性,但是由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用Serializable隔离级别。
四、Spring支持两种方式事务过滤
1、声明式事务管理
推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于@Transactional
的全注解方式使用最多)。
使用 @Transactional
注解进行事务管理的示例代码如下:
import org.springframework.transaction.annotation.Transactional;
@Transactional(rollbackFor = Exception.class)
public class AccountService {
public void transferMoney(int fromAccountId, int toAccountId, double amount) {
// 从一个账户扣款
accountDao.withdraw(fromAccountId, amount);
// 向另一个账户收款
anotherAccountDao.deposit(toAccountId, amount);
}
}
2、编程式事务管理
-
除了声明式事务管理外,Spring还支持编程式事务管理,即通过编写代码来控制事务的开始、提交和回滚。
-
可以使用
TransactionTemplate
或PlatformTransactionManager
来实现编程式事务管理。 -
示例(使用
TransactionTemplate
):
@Service
public class TransactionalService {
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private UserRepository userRepository;
public void performTransactionalOperation() {
transactionTemplate.execute(status -> {
try {
userRepository.save(new User(/* 参数 */));
// 其他数据库操作
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
return null;
});
}
}
Spring 框架中,事务管理相关最重要的 3 个接口如下:
-
PlatformTransactionManager
:(平台)事务管理器,Spring 事务策略的核心。 -
TransactionDefinition
:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。 -
TransactionStatus
:事务运行状态。
我们可以把 PlatformTransactionManager
接口可以被看作是事务上层的管理者,而 TransactionDefinition
和 TransactionStatus
这两个接口可以看作是事务的描述。
PlatformTransactionManager
会根据 TransactionDefinition
的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 TransactionStatus
接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。
五、Spring 事务管理接口介绍
1、transactionStatus接口
TransactionStatus接口表示了一个具体的事务的状态信息。在进行事务操作时,可以通过TransactionStatus获取当前事务的一些状态信息,如是否已经完成、是否已经回滚等。它主要用于监控或控制事务的执行过程。
TransactionStatus接口包含以下几个主要方法:
-
boolean isCompleted()
:检查事务是否已完成(即已提交或已回滚)。 -
boolean hasRollbackOccurred()
:检查事务是否已回滚。 -
boolean isNewTransaction()
:检查当前事务是否是新事务。 -
boolean isRollbackOnly()
:检查事务是否标记为仅回滚。 -
void setRollbackOnly()
:将事务标记为仅回滚。 -
boolean isActive()
:检查事务是否仍然活跃(即尚未完成)。
public interface TransactionStatus extends SavepointManager {
// 检查事务是否已完成(即已提交或已回滚)
boolean isCompleted();
// 检查事务是否已回滚
boolean hasRollbackOccurred();
// 检查当前事务是否是新事务
boolean isNewTransaction();
// 检查事务是否标记为仅回滚
boolean isRollbackOnly();
// 将事务标记为仅回滚
void setRollbackOnly();
// 检查事务是否仍然活跃(即尚未完成)
boolean isActive();
// 返回当前事务的隔离级别
int getIsolationLevel();
// 返回当前事务是否为只读事务
boolean isReadOnly();
// 其他状态检查方法...
}
2、TransactionDefinition接口
TransactionDefinition接口用于定义事务的一些属性,如隔离级别、传播行为、超时时间等。通过配置这些属性,可以定制事务的行为以满足不同场景下的需求。
TransactionDefinition接口包含以下几个主要属性:
-
隔离级别(Isolation Level):定义了事务方法的操作与其他事务方法的操作之间的隔离程度。Spring提供了多种隔离级别,如DEFAULT(默认)、READ_UNCOMMITTED(读未提交)、READ_COMMITTED(读已提交)、REPEATABLE_READ(可重复读)和SERIALIZABLE(串行化)等。
-
传播行为(Propagation Behavior):定义了事务方法被调用时如何处理现有的事务。Spring提供了多种传播行为,如REQUIRED(必须)、REQUIRES_NEW(要求新事务)、SUPPORTS(支持)、NOT_SUPPORTED(不支持)、MANDATORY(必须存在)、NEVER(永不)和NESTED(嵌套)等。
-
超时时间(Timeout):定义了事务必须在多少秒内完成。如果事务执行时间超过了这个限制,则会自动回滚。
-
只读标志(Read-Only Flag):标记事务是否为只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段来提高性能。
public interface TransactionDefinition {
// 返回事务的传播行为
int getPropagationBehavior();
// 返回事务的隔离级别
int getIsolationLevel();
// 返回事务必须在多少秒内完成
int getTimeout();
// 返回事务的名字
String getName();
// 返回是否优化为只读事务
boolean isReadOnly();
// 事务隔离级别的常量定义
int ISOLATION_DEFAULT = ...; // 使用后端数据库默认的隔离级别
int ISOLATION_READ_UNCOMMITTED = ...; // 最低的隔离级别,允许读取尚未提交的数据变更
int ISOLATION_READ_COMMITTED = ...; // 允许读取并发事务已经提交的数据
int ISOLATION_REPEATABLE_READ = ...; // 对同一字段的多次读取结果都是一致的
int ISOLATION_SERIALIZABLE = ...; // 最高的隔离级别,完全服从ACID的隔离级别
// 事务传播行为的常量定义
int PROPAGATION_REQUIRED = ...; // 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
int PROPAGATION_SUPPORTS = ...; // 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
int PROPAGATION_MANDATORY = ...; // 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
int PROPAGATION_REQUIRES_NEW = ...; // 创建一个新的事务,如果当前存在事务,则把当前事务挂起
int PROPAGATION_NOT_SUPPORTED = ...; // 以非事务方式运行,如果当前存在事务,则把当前事务挂起
// 其他传播行为常量...
}
3、PlatformTransactionManager接口
PlatformTransactionManager是Spring事务管理的核心接口,它定义了事务管理的一些基本操作,如开启事务、提交事务、回滚事务等。这个接口是实际事务管理器(如JDBC事务管理器、JTA事务管理器等)的统一入口,并为上层的业务逻辑屏蔽了不同事务管理器的实现细节。
具体来说,PlatformTransactionManager接口包含以下几个主要方法:
-
TransactionStatus getTransaction(TransactionDefinition definition)
:根据给定的事务定义(TransactionDefinition)来获取一个事务状态(TransactionStatus)对象。这个对象用于表示当前事务的状态,并允许在事务执行过程中进行状态检查和操作。 -
void commit(TransactionStatus status)
:提交事务。如果事务成功执行,则调用此方法将更改持久化到数据库。 -
void rollback(TransactionStatus status)
:回滚事务。如果事务执行过程中出现异常或错误,则调用此方法将更改撤销,恢复到事务开始之前的状态。
public interface PlatformTransactionManager {
// 根据指定的传播行为和定义获取一个事务状态对象
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
六、@Transactional 注解使用详解
1、@Transactional注解的基本使用
@Transactional注解可以应用于接口、接口方法、类以及类方法上。但需要注意以下几点:
-
接口和接口方法:虽然@Transactional注解可以应用于接口和接口方法上,但通常不建议这样做。这是因为只有当使用基于接口的动态代理时,这些注解才会生效。而在实际开发中,基于类的代理(如CGLIB)更为常见。因此,将@Transactional注解应用于具体的实现类上更为稳妥。
-
类级别:当@Transactional注解应用于类上时,该类的所有public方法都将按照相同的事务属性进行处理。这意味着,如果你希望在类的不同方法中使用不同的事务属性,那么你需要将这些方法拆分为不同的类,并为每个类指定不同的事务属性。
-
方法级别:当@Transactional注解应用于方法上时,仅对该方法内的数据库操作生效。这是最常见和推荐的使用方式,因为它允许开发者为不同的业务操作指定不同的事务属性。
2、@Transactional注解的作用范围
@Transactional注解的作用范围主要取决于其应用的位置(类级别或方法级别)以及Spring的AOP(面向切面编程)机制。以下是一些关键点:
-
AOP代理:Spring通过AOP代理来处理带有@Transactional注解的方法。当这些方法被调用时,Spring会自动开启一个事务,并在方法执行完成后提交或回滚事务。需要注意的是,这个代理过程仅对来自外部的方法调用有效。如果类内部的方法调用本类内部的其他带有@Transactional注解的方法,那么这个调用将不会触发事务管理(因为此时调用的是原始对象的方法,而不是代理对象的方法)。
-
public方法:@Transactional注解只能应用于public方法上。如果将其应用于protected、private或默认修饰符的方法上,虽然不会报错,但注解将不会生效。这是因为Spring的AOP代理只能代理public方法。
-
异常处理:默认情况下,@Transactional注解只能回滚运行时异常(RuntimeException及其子类)和错误(Error子类)。如果希望回滚其他类型的异常(如受检异常Checked Exception),则需要通过rollbackFor属性来指定。同时,需要注意的是,如果异常被try-catch块捕获并处理,那么事务将不会回滚(除非在catch块中显式地触发回滚)。
3、@Transactional 的常用配置参数
@Transactional
注解源码如下,里面包含了基本事务属性的配置:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
@Transactional
的常用配置参数总结(只列出了 5 个我平时比较常用的):
属性名 | 说明 |
---|---|
propagation | 事务的传播行为,默认值为 REQUIRED,可选的值在上面介绍过 |
isolation | 事务的隔离级别,默认值采用 DEFAULT,可选的值在上面介绍过 |
timeout | 事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。 |
readOnly | 指定事务是否为只读事务,默认值为 false。 |
rollbackFor | 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。 |
@Transactional 事务注解原理
@Transactional
的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
createAopProxy()
方法 决定了是使用 JDK 还是 Cglib 来做动态代理,源码如下:
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
}
如果一个类或者一个类中的 public 方法上被标注@Transactional
注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解的 public 方法的时候,实际调用的是,TransactionInterceptor
类中的 invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
七、案例
1、引入依赖
首先,确保你的项目中引入了Spring事务管理相关的依赖。如果你使用的是Maven,可以在pom.xml
中添加以下依赖:
<dependencies>
<!-- Spring Context, Core, and other necessary dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.10</version>
</dependency>
<!-- JDBC Driver and DataSource, e.g., H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
</dependencies>
2、配置数据源和事务管理器
在Spring配置文件中(可以是XML文件或Java配置类),配置数据源(DataSource)和事务管理器(PlatformTransactionManager)。
XML配置示例:
<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"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- DataSource configuration -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:testdb"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<!-- PlatformTransactionManager configuration -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- Enable annotation-driven transaction management -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
Java配置示例:
@Configuration
@EnableTransactionManagement
public class AppConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:mem:testdb");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
3、使用@Transactional
注解
在需要事务支持的方法上使用@Transactional
注解。这个注解可以放在类级别(表示类中的所有public方法都受事务管理)或方法级别(仅该方法受事务管理)。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createUser(User user) {
userRepository.save(user);
// 这里可以添加其他数据库操作,它们都会在同一个事务中执行
}
// 其他业务方法...
}
在上面的示例中,createUser
方法被@Transactional
注解标记,这意味着Spring将在这个方法执行前后开启和提交事务。如果方法执行过程中抛出了未捕获的异常,事务将被回滚。
4、事务属性
@Transactional
注解还支持多种属性来定制事务行为,如隔离级别、传播行为、超时时间等。
@Transactional(isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRED,
timeout = 30) // 超时时间为30秒
public void someTransactionalMethod() {
// 方法实现
}
5、异常处理与回滚
默认情况下,Spring会在运行时异常(RuntimeException
及其子类)和错误(Error
及其子类)发生时回滚事务。对于受检异常(checked exceptions
),你需要明确指定哪些异常应该导致事务回滚。
@Transactional(rollbackFor = {CustomException.class, AnotherException.class})
public void someMethodThatMayThrowCheckedExceptions() {
// 方法实现,可能会抛出CustomException或AnotherException
}
6、测试事务
最后,确保你的事务配置正确无误,并编写单元测试来验证事务的行为。你可以使用Spring的测试框架(如@RunWith(SpringRunner.class)
和@SpringBootTest
)来运行测试,并使用@Transactional
注解来模拟事务环境