一、事务的基本性质
- 原子性:一系列操作整体不可拆分,要么同时成功,要么同时失败。(下订单、减库存、减积分)
- 一致性:数据在事务的前后,业务整体一致。(存取钱的总数量)
- 隔离性:事务之间互相隔离。
- 持久性:一旦事务成功,数据一定会落盘在数据库。
二、事务的隔离级别
事务的隔离级别(Transaction Isolation Levels)定义了在并发执行的事务中,一个事务对其他事务的操作的可见性和影响。SQL标准定义了四种常见的隔离级别,按隔离程度从低到高排序,分别是:
-
读未提交(Read Uncommitted):
- 在此隔离级别下,一个事务可以读取另一个事务尚未提交的修改。这种级别可能导致脏读(Dirty Read),即读取到一个事务未提交的、不一致的数据。
- 脏读:一个事务读取到另一个事务正在修改但未提交的数据,若第二个事务回滚,则第一个事务读取到的数据不准确。
-
读已提交(Read Committed):
- Oracle和SQL Server的默认隔离级别。
- 该隔离级别保证一个事务只能读取到已经提交的数据。也就是说,一个事务只能看到其他事务已经提交的结果。
- 但它仍然存在不可重复读(Non-repeatable Read)的情况。即,在同一事务中,多次读取同一数据时,数据可能会在事务过程中被其他事务修改。
-
可重复读(Repeatable Read):
- 在此隔离级别下,事务中读取的数据在整个事务期间都是一致的,即同一查询多次执行时结果不会变化,避免了不可重复读的问题。
- 但此级别仍可能出现幻读(Phantom Read)。幻读指的是在同一个事务中,第一次查询某个范围的数据后,第二次查询同一范围时,可能会看到其他事务插入的新的数据行。
- MYSQL的默认隔离级别。MySQL数据库的InnoDB引擎可以通过next-key locks机制来避免幻读。
-
串行化(Serializable):
- 在该隔离级别下事务都是串行顺序执行的,MySQL数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。
示例:
@Transactional(isolation = Isolation.READ_COMMITTED)- Isolation.READ_UNCOMMITTED:允许脏读。- Isolation.READ_COMMITTED:禁止脏读,允许不可重复读。- Isolation.REPEATABLE_READ:禁止脏读和不可重复读,允许幻读。- Isolation.SERIALIZABLE:禁止所有并发问题(脏读、不可重复读、幻读)。
三、事务的传播行为
传播行为 | 说明 | 适用场景 |
---|---|---|
PROPAGATION.REQUIRED | 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务。该设置是最常用的设置。 | 最常用,适用于大多数业务方法。 |
PROPAGATION.SUPPORTS | 支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。 | 某些方法可以在事务中执行,也可以在非事务模式下执行,通常用于查询操作。 |
PROPAGATION.MANDATORY | 支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。 | 确保方法必须在事务中执行,若外部没有事务,则会抛出异常。 |
PROPAGATION.REQUIRES_NEW | 创建新事务,无论当前存不存在事务,都创建新事务。 | 需要独立事务执行的操作,如日志记录或外部系统调用等。 |
PROPAGATION.NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 | 不希望当前方法在事务中执行的操作,通常用于日志、非数据库操作等场景。 |
PROPAGATION.NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 | 必须确保方法不在事务中执行。 |
PROPAGATION.NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION.REQUIRED 类似的操作。 | 支持事务嵌套,适用于需要回滚部分操作但不影响外部事务的场景。 |
事务传播行为示例:
@Transactionalpublic void methodA() {methodB();methodC(); }@Transactional(propagation = Propagation.REQUIRED)public void methodB() {}@Transactional(propagation = Propagation.REQUIRES_NEW)public void methodC() {}
事务执行顺序:
假设我们执行 methodA()
:
-
调用
methodA
:methodA
被调用时,由于@Transactional
默认使用Propagation.REQUIRED
,它会启动一个事务(假设当前没有事务的话)。- 这时,
methodA
进入事务上下文。
-
调用
methodB
:- 在
methodA
中调用methodB
时,由于methodB
的传播行为是Propagation.REQUIRED
,如果已经存在事务(即methodA
创建的事务),methodB
会加入这个现有事务。 - 因此,
methodB
和methodA
会共享同一个事务。
- 在
-
调用
methodC
:- 在
methodA
中调用methodC
时,由于methodC
的传播行为是Propagation.REQUIRES_NEW
,它会始终开启一个新的事务。 methodC
的事务会挂起methodA
中的事务,methodC
会在自己的事务中执行。methodC
执行完后,methodA
的事务会恢复。methodC
和methodA / methodB
的事务是独立的,methodC
完成后,methodA
会继续执行。
- 在
四、本类方法调用事务失效
同一个对象内事务方法互相调用默认失败,因为绕过了代理对象。相当于b、c的事务不会生效,和a共用一个事务。
示例:
@Transactional(timeout = 30)public void a(){b();c();}@Transactional(propagation = Propagation.REQUIRED,timeout = 2)public void b(){}@Transactional(propagation = Propagation.REQUIRES_NEW,timeout = 20)public void c(){}
解决:
- 引入aop依赖,使用里面的aspectj做动态代理
<!--引入aop依赖,使用里面的aspectj做动态代理--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
- 开启aspectj动态代理功能,并对外暴露代理对象。以后所有动态代理都是用aspectj创建(即便没有接口也可以创建动态代理)
@EnableAspectJAutoProxy(exposeProxy= true)
- 使用动态代理调用
@Transactional(timeout = 30)public void a(){OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();orderService.b();orderService.c();}