事务这个词在学习 MySQL 和多线程并发编程的时候,想必大家或多或少接触过。那么什么是事务呢?
事务是指一组操作作为一个不可分割的执行单元,要么全部成功执行,要么全部失败回滚。在数据库中,事务可以保证数据的一致性、完整性和稳定性,同时避免了数据的异常和不一致情况。常见的事务包括插入、更新、删除等数据库操作。事务的核心要素是ACID特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
比如,常见的转账操作,以小明给小红转账100元为例,分为如下两个操作:
如果没有事务,第一步操作执行成功,而第二步执行失败,就会导致小明账户平白无故的扣款而小红账户没有收到款项的问题。因此,事务的存在是必要的,这一组操作要么全部执行成功,要么一起失败~
在 MySQL 中,事务有三个重要的操作,分别为:开启事务、提交事务、回滚事务,对应的操作命令如下:
-- 开启事务
start transaction;
-- 业务执行
...
-- 提交事务
commit;
-- 回滚事务
rollback;
与 MySQL 操作事务类似,Spring 手动操作事务也需要三个重要的操作:开启事务(获取事务)、提交事务、回滚事务。
SpringBoot 内置了两个对象:
DataSourceTransactionManager
⽤来获取事务(开启事务)、提交或回滚事务的;TransactionDefinition
是事务的属性,在获取事务的时候需要将TransactionDefinition
传递进去从⽽获得⼀个事务 TransactionStatus
;实现代码如下:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
// 事务管理器
@Autowired
private DataSourceTransactionManager transactionManager;
// 定义事务属性
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/add")
public int add(UserInfo userInfo) {
// 非空校验
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
// 1. 开始事务
TransactionStatus transactionStatus =
transactionManager.getTransaction(transactionDefinition);
int result = userService.add(userInfo);
System.out.println("添加: " + result);
// // 2. 回滚事务
// transactionManager.rollback(transactionStatus);
// 3. 提交事务
transactionManager.commit(transactionStatus);
return result;
}
}
从上述代码可以看出,虽然可以实现事务,但是操作很繁琐。因此,我们 常常使用另一种更简单的方式:基于注解的声明式事务。
相比手动操作事务来说,声明式事务非常简单,只需要在需要的方法上添加 @Transactional
注解,无需手动开启事务和提交事务。
示例代码如下:
@Transactional // 声明式事务(自动提交)
@RequestMapping("/insert")
public Integer insert(UserInfo userInfo) {
// 非空校验
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int result = userService.add(userInfo);
return result;
}
对于 @Transactional
的几点说明:
附:@Transactional
的常见参数:
参数 | 说明 |
---|---|
propagation | 定义了事务方法被嵌套调用时,事务如何传播到被调用的方法。常见取值包括: |
- REQUIRED (默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 |
|
- REQUIRES_NEW :每次调用方法时都会创建一个新的事务,如果存在当前事务,则将其挂起。 |
|
- SUPPORTS :如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。 |
|
- NOT_SUPPORTED :以非事务方式执行操作,如果当前存在事务,则将其挂起。 |
|
- MANDATORY :如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 |
|
- NEVER :以非事务方式执行操作,如果当前存在事务,则抛出异常。 |
|
- NESTED :如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建一个新的事务。 |
|
isolation | 定义了事务并发执行时,事务之间的隔离程度。常见取值包括: |
- DEFAULT (默认):使用数据库默认的隔离级别。 |
|
- READ_UNCOMMITTED :最低的隔离级别,事务可以读取未提交的数据。 |
|
- READ_COMMITTED :事务只能读取已提交的数据。 |
|
- REPEATABLE_READ :事务在整个过程中保持一致的读取视图,防止脏读和不可重复读。 |
|
- SERIALIZABLE :最高的隔离级别,事务串行执行,避免脏读、不可重复读和幻读。 |
|
timeout | 定义了事务执行的最长时间,单位为秒。默认值为-1,表示没有超时限制。 |
readOnly | 如果设置为true ,表示事务只读,不会修改数据库的数据。默认值为false 。 |
rollbackFor | 触发事务回滚的异常类数组。当方法抛出指定的异常时,事务将回滚。 |
noRollbackFor | 不触发事务回滚的异常类数组。当方法抛出指定的异常时,事务将不会回滚。 |
rollbackForClassName | 触发事务回滚的异常类名数组。当方法抛出指定的异常时,事务将回滚。 |
noRollbackForClassName | 不触发事务回滚的异常类名数组。当方法抛出指定的异常时,事务将不会回滚。 |
value | 用于指定事务管理器的名称。如果应用程序中存在多个事务管理器,可以使用该参数指定要使用的事务管理器的名称。默认情况下,事务将使用默认的事务管理器。 |
transactionManager | 用于指定事务管理器的引用。可以直接将一个事务管理器对象传递给该参数,以指定要使用的事务管理器。默认情况下,事务将使用默认的事务管理器。 |
对于上述表格中的事务隔离级别需要重点掌握,具体后面详细说。
需要特别注意的是,如果方法中的异常被 try-catch
异常捕获处理后,则不会再进行事务的回滚。
当然,我们可以通过 throw
将异常抛出,使得事务能够正常自动回滚。但是这样子做,try-catch
还有意义吗?
因此,对于这种情况,更偏向于使用另一种优雅的方式,进行手动回滚事务来解决~
如何在声明式事务中进行手动回滚事务?
使用代码进行手动回滚事务:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
具体来看,@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。@Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。实现细节的执行流程如图所示:
事务有4 ⼤特性(ACID),原⼦性、⼀致性、隔离性和持久性:
其中,对于隔离性有隔离级别可以设置。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串⾏化(Serializable)。
归根到底,事务隔离级别的设置是为了防止其它事务影响当前事务的一种策略。
在MySQL中,默认是可重复读(repeatable read)级别。以下是MySQL常见的事务隔离级别以及它们对脏读、不可重复读和幻读问题的解决情况的表格:
事务隔离级别 | 脏读(Dirty Read) | 不可重复读(Non-repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
读未提交(Read Uncommitted) | 可能发生 | 可能发生 | 可能发生 |
读已提交(Read Committed) | 避免 | 可能发生 | 可能发生 |
可重复读(Repeatable Read) | 避免 | 避免 | 可能发生 |
串行化(Serializable) | 避免 | 避免 | 避免 |
对于脏读、不可重复读和幻读的解释:
脏读(Dirty Read):指一个事务读取了另一个事务未提交的数据。如果一个事务可以读取未提交的数据,则会发生脏读。
不可重复读(Non-repeatable Read):指在同一个事务中,多次读取同一行数据时,得到的结果不一致。这是因为在读取期间,另一个事务修改了该行数据。
幻读(Phantom Read):指在同一个事务中,多次查询同一个范围的数据时,得到的结果集不一致。这是因为在查询期间,另一个事务插入或删除了符合查询条件的数据。
每个隔离级别对这些问题的解决情况如下:
读未提交(Read Uncommitted):允许脏读、不可重复读和幻读。一个事务可以读取另一个事务未提交的数据。
读已提交(Read Committed):避免脏读。一个事务只能读取已提交的数据。但是,可能发生不可重复读和幻读,因为在同一个事务中,另一个事务可能会修改数据。
可重复读(Repeatable Read):避免脏读和不可重复读。在同一个事务中,多次读取同一行数据时,得到的结果是一致的。但是,可能发生幻读,因为在同一个事务中,另一个事务可能会插入或删除数据。
串行化(Serializable):避免脏读、不可重复读和幻读。事务串行执行,保证了数据的一致性和完整性。
但隔离级别的提升会增加并发性能的开销,因为更高的隔离级别通常需要使用锁来实现。
在数据库中,可以使用如下语句来查询全局事务隔离级别和当前连接的事务隔离级别:
select @@global.tx_isolation,@@tx_isolation;
在Spring框架中,事务的隔离级别可以使用@Transactional
注解来设置。@Transactional
注解可以应用在方法级别或类级别上,用于声明一个事务性方法或类。
Spring 框架支持以下五个事务隔离级别:
DEFAULT
(默认):使用底层数据库的默认隔离级别。对于大多数数据库来说,通常是READ_COMMITTED
。
READ_UNCOMMITTED
:读未提交。允许脏读、不可重复读和幻读。这是最低的隔离级别,一个事务可以读取另一个事务未提交的数据。
READ_COMMITTED
:读已提交。避免脏读。一个事务只能读取已提交的数据。但是,可能发生不可重复读和幻读,因为在同一个事务中,另一个事务可能会修改数据。
REPEATABLE_READ
:可重复读。避免脏读和不可重复读。在同一个事务中,多次读取同一行数据时,得到的结果是一致的。但是,可能发生幻读,因为在同一个事务中,另一个事务可能会插入或删除数据。
SERIALIZABLE
:串行化。避免脏读、不可重复读和幻读。事务串行执行,保证了数据的一致性和完整性,但是性能太低。
可以在@Transactional
注解上使用isolation
属性来设置事务的隔离级别。例如:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void myTransactionalMethod() {
// 事务性操作
}
需要注意的是,事务的隔离级别还受数据库本身支持的隔离级别的限制。如果数据库不支持某个特定的隔离级别,那么Spring框架将尽力使用最接近的隔离级别。
事务的传播机制是用来定义事务在传播过程中的行为模式的一种机制。 Spring 事务传播机制定义了多个包含了事务的方法,相互调用时,事务是如何在这些方法进行传递的。
对比事务的隔离级别来看,如果说事务的隔离级别是保证多个并发事务执行的可控性的(稳定性),则 事务的传播机制就是保证一个事务在多个调用方法间的可控性的(稳定性)。
事务的传播机制解决的是一个事务在多个节点(方法)中传递的问题:
比如,方法 A 正常执行,完成了事务。但是,方法 B 发生了错误。那么,方法 A 进行的事务操作是否要回滚呢?这就是事务的传播机制需要解决的问题~
在Spring框架中,事务传播机制用于定义在多个事务性方法相互调用时,事务如何传播和交互的规则。Spring框架提供了七种不同的事务传播行为:
REQUIRED
(需要有):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是最常用的传播行为。
SUPPORTS
(可以有):如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
MANDATORY
(强制有):如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
REQUIRES_NEW
:创建一个新的事务,并挂起当前事务(如果存在)。新创建的事务与当前事务完全独立。
NOT_SUPPORTED
:以非事务方式执行,并且挂起当前事务(如果存在)。
NEVER
:以非事务方式执行,如果当前存在事务,则抛出异常。
NESTED
:如果当前存在事务,则在嵌套事务中执行。嵌套事务是独立于当前事务的子事务,它可以独立地进行提交或回滚。如果当前没有事务,则创建一个新的事务。
这些事务传播行为可以通过@Transactional
注解的propagation
属性进行设置。例如:
@Transactional(propagation = Propagation.REQUIRED)
public void myTransactionalMethod() {
// 事务性操作
}
需要注意的是,事务传播行为仅在方法之间的调用时才会生效,对于同一个方法内部的事务性操作,传播行为不会起作用。
本文被 JavaEE编程之路 收录点击订阅专栏 , 持续更新中。
以上便是本文的全部内容啦!创作不易,如果你有任何问题,欢迎私信,感谢您的支持!