1.什么是事务
数据库事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么一起成功,要么一起失败,是一个不可分割的工作单元。
涉及到事务的场景非常多,一个 service 中往往需要调用不同的 dao 层方法,这些方法要么同时成功要么同时失败,我们需要在 service 层确保这一点。
事务的四大特性(ACID)
-
原子性(Atomicity): 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
-
一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
-
隔离性(Isolation): 数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read Uncommitted)、提交读(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
-
持久性(Durability): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
2.Spring中的事务
Spring 中对事务的支持提供了三大核心接口,我们先来了解下。
- PlatformTransactionManager
- TransactionDefinition
- TransactionStatus
2.1PlatformTransactionManager
PlatformTransactionManager 中定义了基本的事务操作方法,这些事务操作方法的具体实现都是由不同的子类来实现的。相当于 PlatformTransactionManager 是定义了一套标准,我们不需要掌握具体实现类的用法,只需要掌握好 PlatformTransactionManager 的用法即可。
public interface PlatformTransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition);
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
PlatformTransactionManager 中主要有如下三个方法:
1.getTransaction()
getTransaction() 是根据传入的 TransactionDefinition 获取一个事务对象,TransactionDefinition 中定义了一些事务的基本规则,例如传播性、隔离级别等。
2.commit()
commit() 方法用来提交事务。
3.rollback()
rollback() 方法用来回滚事务。
2.2TransactionDefinition
TransactionDefinition 用来描述事务的具体规则,也称作事务的属性。
事务主要有五种属性:
- 隔离性
- 传播性
- 回滚规则
- 超时时间
- 是否只读
TransactionDefinition 接口 中还有5个对应的方法:
- getIsolationLevel(),获取事务的隔离级别
- getName(),获取事务的名称
- getPropagationBehavior(),获取事务的传播性
- getTimeout(),获取事务的超时时间
- isReadOnly(),获取事务是否是只读事务
如果开发者使用了编程式事务的话,直接使用 DefaultTransactionDefinition 即可。
2.3TransactionStatus
TransactionStatus表示一个事物的状态。接口提供了控制事务执行和查询事务状态的方法。比如当前调用栈中之前已经存在了一个事物,那么就是通过该接口来判断的,TransactionStatus接口可以让事务管理器控制事务的执行,比如检查事务是否为一个新事务,或者是否只读,TransactionStatus还可以初始化回滚操作。
接口继承了SavepointManager接口,因此封装了事物中回滚点的相关操作
SavepointManager接口:
public interface SavepointManager {
//1.创建回滚点
Object createSavepoint() throws TransactionException;
//2.回滚到回滚点
void rollbackToSavepoint(Object savepoint) throws TransactionException;
//3.释放回滚点
void releaseSavepoint(Object savepoint) throws TransactionException;
}
TransactionStatus接口:
public interface TransactionStatus extends SavepointManager, Flushable {
//是否是一个新的事物
boolean isNewTransaction();
//判断是否有回滚点
boolean hasSavepoint();
//将一个事务标识为不可提交的。在调用完setRollbackOnly()后只能被回滚
//在大多数情况下,事务管理器会检测到这一点,在它发现事务要提交时会立刻结束事务。
//调用完setRollbackOnly()后,数数据库可以继续执行select,但不允许执行update语句,因为事务只可以进行读取操作,任何修改都不会被提交。
void setRollbackOnly();
boolean isRollbackOnly();
@Override
void flush();
//判断事物是否已经完成
boolean isCompleted();
}
3.编程式事务
编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。通过 PlatformTransactionManager 或者 TransactionTemplate 可以实现编程式事务。如果是在 Spring Boot 项目中,这两个对象 Spring Boot 会自动提供,我们直接使用即可。
使用 TransactionTemplate 来管理事务:
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}
使用 TransactionManager 来管理事务:
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
编程式事务由于代码入侵太严重了,因为在实际开发中使用的很少,我们在项目中更多的是使用声明式事务。
4.声明式事务(@Transactional)
声明式事务将事务管理代码从业务方法中抽离了出来,利用到了 Spring 当中的AOP,以声明式的方式来实现事务管理,对于开发者来说,声明式事务显然比编程式事务更易用、更好用。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。
5. 事务属性
5.1 隔离性(isolation)
高性能MySQL读书笔记第一章——MySQL架构_汤姆&Tom的博客-CSDN博客
5.2 传播性(propagation)
事务传播行为是为了解决业务层方法之间互相调用的事务问题,当一个事务方法被另一个事务方法调用时,事务该以何种状态存在?例如新方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行,等等,这些规则就涉及到事务的传播性。
关于事务的传播性,Spring 主要定义了如下几种:
传播性 | 描述 |
---|---|
REQUIRED(默认) | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 |
SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 |
MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 |
REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起 |
NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起 |
NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常 |
NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED |
这里只讲解部分传播性,最重要的还是默认的 REQUIRED
REQUIRED
这也是 @Transactional 默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。更确切地意思是:
- 如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。
- 如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚。
REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会开启自己的事务,且开启的事务与外部的事务相互独立,互不干扰。
- 当类A中的 a 方法用默认 Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务
总结就是a不影响b,b影响a(如果catch了异常则不影响a)
NESTED
如果当前存在事务,就在当前事务内执行;否则开启自己的事务
- 当类A中的 a 方法用默认 Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.NESTED模式,然后在 在a 方法里调用 b方法操作数据库,然而 b方法抛出异常后,a方法是不的回滚
总结就是 a影响b,b可能影响 a (如果不处理异常)
5.3 回滚规则(rollbackFor)
默认情况下,事务只有遇到 运行期异常(RuntimeException 的子类)以及 Error 时才会回滚,在遇到 检查型(Checked Exception)即 IOException 异常时不会回滚。
@Transactional(rollbackFor = IOException.class)
5.4 是否只读(readOnly)
只读事务一般设置在查询方法上,但不是所有的查询方法都需要只读事务,要看具体情况。
一般来说,如果这个业务方法只有一个查询 SQL,那么就没必要添加事务,强行添加最终效果适得其反。
但是如果一个业务方法中有多个查询 SQL,情况就不一样了:多个查询 SQL,默认情况下,每个查询 SQL 都会开启一个独立的事务,这样,如果有并发操作修改了数据,那么多个查询 SQL 就会查到不一样的数据。此时,如果我们开启事务,并设置为只读事务,那么多个查询 SQL 将被置于同一个事务中,多条相同的 SQL 在该事务中执行将会获取到相同的查询结果。
@Transactional(readOnly = true)
5.5 超时时间(timeout)
超时时间是说一个事务允许执行的最长时间(单位为秒),如果超过该时间限制但事务还没有完成,则自动回滚事务。
@Transactional(timeout = 10)
6.Spring事务的失效场景
1.事务只能应用到 public 方法上才会有效。
2.事务需要从外部调用,Spring 自调事务(同一个类中)用会失效。即在同一个类里边,A 方法没有事务,B 方法有事务,A 方法调用 B 方法,则 B 方法的事务会失效,这点尤其要注意,因为代理模式只拦截通过代理传入的外部方法调用,所以自调用事务是不生效的。
如下面,test方法调用 transfer 方法,出现异常后事务是不会回滚的。
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void transfer(){
jdbcTemplate.update("update user set money = ? where username = ?;",1,"zhangsan");
int i=1/0;
}
public void test(){
//事务失效
transfer();
}
}
因为事务需要从外部调用,从外部注入UserService,直接调用transfer()方法 才可以。如下:
@RestController
public class Controller {
@Autowired
private UserService userService;
@GetMapping("/test")
public void test(){
userService.transfer();
}
}
3.建议事务注解 @Transactional 一般添加在实现类上,而不要定义在接口上。如果加在接口类或接口方法上时,只有配置基于接口的代理这个注解才会生效。