事务
数据库的事务是一种机制、一个操作序列,包含了一组数据库操作命令。事务把所有的命令作为一个整体一起向系统提交或撤销操作请求,即这一组数据库命令要么都执行,要么都不执行,因此事务是一个不可分割的工作逻辑单元。
事务四大特性
为了保证事务是正确可靠的,在数据库进行写入或者更新操作时,就必须得表现出 ACID 的 4 个重要特性:
- 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
- 事务隔离(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
- 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
事务隔离级别
其中,事务隔离又分为 4 种不同的级别(其中oracle默认是读已提交,MySql默认是可重复读),包括:
- 读未提交,最低的隔离级别,允许“脏读”,事务可以看到其他事务“尚未提交”的修改。如果另一个事务回滚,那么当前事务读到的数据就是脏数据。(脏读、不可重复读、幻读)
- 读已提交,一个事务可能会遇到不可重复读的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。(不可重复读、幻读)
- 可重复读,一个事务可能会遇到幻读的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。(幻读)
- 可串行化,最严格的隔离级别,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。虽然 Serializable 隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用 Serializable 隔离级别。
spring事务
Spring 支持两种事务方式,分别是编程式事务和声明式事务。
编程式事务
编程式事务是指将事务管理代码嵌入到业务代码中,来控制事务的提交和回滚。一般有两种方式。
1、PlatformTransactionManager实现
@Autowired
private PlatformTransactionManager transactionManager;
@Override
public void bcTtransactionByManage() {
TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Account account = new Account("F15B1b57-c8dc-475e-F5A9-6d8D8B271Be9", "廖娜", 1000);
accountMapper.updateById(account);
int i = 10 / 0;
transactionManager.commit(transactionStatus);
} catch (Exception e) {
e.printStackTrace();
log.info("数据执行异常,进行事务回滚");
transactionManager.rollback(transactionStatus);
}
}
2、TransactionTemplate实现
@Autowired
private TransactionTemplate transactionTemplate;
@Override
public Account bcTtransaction1() {
// 为事务设置属性
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
return transactionTemplate.execute((new TransactionCallback<Account>(){
Account account = null;
// @Override
// public Account doInTransaction(TransactionStatus status) {
// try {
// account = new Account("F15B1b57-c8dc-475e-F5A9-6d8D8B271Be9", "廖娜", 2000);
// accountMapper.updateById(account);
// int i = 10 / 0;
// } catch (Exception e) {
// // 根据自己指定的Exception去回滚事务
// e.printStackTrace();
// status.setRollbackOnly();
// }
// return account;
// }
// 直接复用模板
@Override
public Account doInTransaction(TransactionStatus status) {
account = new Account("F15B1b57-c8dc-475e-F5A9-6d8D8B271Be9", "廖娜", 1000);
accountMapper.updateById(account);
int i = 10 / 0;
return account;
}
}));
}
声明式事务
将事务管理代码从业务代码中抽离出来,用注解@Transactional的方式来实现事务管理。首先需要事用@EnableTransactionManagement开启事务管理功能。
要实现事务管理和业务代码的抽离,就必须得用到 Spring 当中最关键最核心的技术之一,AOP,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。
事务管理模型
Spring 将事务管理的核心抽象为一个事务管理器(TransactionManager),它的源码只有一个简单的接口定义,属于一个标记接口:
public interface TransactionManager {
}
该接口有两个子接口,分别是反应式事务接口 ReactiveTransactionManager 和声明式事务接口 PlatformTransactionManager。重点说PlatformTransactionManager,该接口定义了 3 个接口方法:
interface PlatformTransactionManager extends TransactionManager{
// 根据事务定义获取事务状态
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 事务回滚
void rollback(TransactionStatus status) throws TransactionException;
}
通过 PlatformTransactionManager 这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,由各个平台自己实现。
参数 TransactionDefinition 和 @Transactional 注解是对应的,比如说 @Transactional 注解中定义的事务传播行为、隔离级别、事务超时时间、事务是否只读等属性,在 TransactionDefinition 都可以找得到。
返回类型 TransactionStatus 主要用来存储当前事务的一些状态和数据,比如说事务资源(connection)、回滚状态等。
public interface TransactionDefinition {
// 事务的传播行为
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}
// 事务的隔离级别
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}
// 事务超时时间
default int getTimeout() {
return TIMEOUT_DEFAULT;
}
// 事务是否只读
default boolean isReadOnly() {
return false;
}
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
}
事务传播行为
当事务方法被另外一个事务方法调用时,必须指定事务应该如何传播(多个方法),例如,方法可能继续在当前事务中执行,也可以开启一个新的事务,在自己的事务中执行。
声明式事务的传播行为可以通过 @Transactional 注解中的 propagation 属性来定义,比如说:
@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {
}
1. PROPAGATION_REQUIRED
这也是 @Transactional 默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。更确切地意思是:
1、 如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。
2、如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚。
Class A {
@Transactional
public void aMethod {
//do something
B b = new B();
b.bMethod();
}
}
Class B {
@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
public void bMethod {
//do something
}
}
2. PROPAGATION_REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会开启自己的事务,且开启的事务与外部的事务相互独立,互不干扰。
Class A {
@Autowired
private B b;
@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
b.bMethod();
}
}
Class B {
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void bMethod {
//do something
}
}
如果 aMethod()发生异常回滚,bMethod()不会跟着回滚,因为 bMethod()开启了独立的事务。但是,如果 bMethod()抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod()同样也会回滚。
3. PROPAGATION_NESTED
如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。 嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
4. PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
5. PROPAGATION_SUPPORTS
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
6. PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
7. PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
事务的超时时间
事务超时,也就是指事务中sql语句所允许执行的最长时间(注意:不是业务代码的执行时间),如果在指定时间内没有完成的话,就自动回滚。
Spring事务超时 = 事务开始时到最后一个Statement创建时时间 + 最后一个Statement的执行时超时时间(即其queryTimeout)。
@Transactional(timeout = 3)
public void timeOutTest(Account account) throws InterruptedException {
accountService.generateAccount(account);
// 中间设置耗时操作
TimeUnit.SECONDS.sleep(5);
account.setId(UUID.randomUUID().toString());
account.setUsername(account.getUsername().concat("_A"));
accountService.generateAccount(account);
}
事务的只读属性
在将事务设置成只读后,当前只读事务就不能进行写的操作,否则报错。
有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条 SQL 查询后,被其他用户改变了数据,那么下一个 SQL 查询可能就会出现不一致的状态。
若一个事务里先后发出了多条select语句。 @Transactional注解表明被申明方法是一个整体事务。在mysql的默认隔离级别下(可重复读),多次查询结果不会改变,所以能保证读一致性;相反若不加 @Transactional注解,则多条select都是独立的事务在前条select之后,后条select之前,数据被其他事务改变,则该次整体的查询将会出现读数据不一致的现象。此时需要添加@Transactional注解
事务的回滚策略
默认情况下,事务只在出现运行时异常(Runtime Exception)时回滚,以及 Error,出现检查异常(checked exception,需要主动捕获处理或者向上抛出)时不回滚。
如果你想要回滚特定的异常类型的话,可以这样设置:
@Transactional(rollbackFor= Exception.class)
事务失效场景
-
private、default、protected、static、final修饰的方法
-
基于AOP思想,TransactionInterceptor类中对事务方法进行增强,所以所修饰的方法需要被代理
-
方法内部调用
@Override
public void doUpdateInMethed() {
this.doUpdateInMethedImpl();
}
@Transactional
public void doUpdateInMethedImpl(){
Account account = new Account("F15B1b57-c8dc-475e-F5A9-6d8D8B271Be9", "廖娜", 1000);
accountMapper.updateById(account);
int i = 10 / 0;
}
doUpdateInMethed无事务,Impl有事务,doUpdateInMethed调用Impl的时候由于Impl方法没有被代理对象调用,所以此时B方法没有被事务包含。
如果是外层方法加上@Transactional注解,那么它方法体里的所有方法会被事务包含。
解决办法:
- 新建一个service,将事务方法抽取到新的service中
- 在service中注入自己
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountService accountService;
@Override
public void updateAccountA() {
accountService.updateAccountB();
}
}
- 多线程调用
@Override
@Transactional
public void threadTest(Account account) throws InterruptedException {
System.out.println(account.toString());
accountMapper.insert(account);
TimeUnit.SECONDS.sleep(2);
new Thread(() ->{
account.setUsername(account.getUsername() + "_A");
// 多线程内部抛出异常
accountService.generateAccountThrow(account);
},"A").start();
}
@Override
@Transactional
public int generateAccountThrow(Account account) {
int insert = accountMapper.insert(account);
int i = 10 / 0;
return insert;
}
事务方法threadTest调用了方法generateAccountThrow,但generateAccountThrow在另一个线程中,获取到的数据库连接不一样,所以处在不同的事务中。A线程抛出异常后,accountMapper.insert(account)操作不会回滚。
- 数据库或者当前表不支持事务
- 未开启事务
springboot中需要使用@EnableTransactionManagement注解开启事务,来注册相关类。 - 设置错误的传播特性
@Override
@Transactional
public void userpPropagationByError(Account account){
accountMapper.insert(account);
Account account1 = accountService.newTransaction(account.getId());
log.info("查询结果:" + account1);
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public Account newTransaction(String id){
return accountMapper.selectById(id);
}
方法内部又开启了一个新事务,导致刚刚新增的数据在新事务方法中无法被找到。
解决办法:
如果外层确实需要事务,可以采用编程式和声明式事务相结合的方式。
@Override
public void useAnnotionAndManage(Account account){
transactionTemplate.execute(new TransactionCallback<Account>() {
@Override
public Account doInTransaction(TransactionStatus status) {
accountMapper.insert(account);
return account;
}
});
Account account1 = accountService.newTransaction(account.getId());
log.info("查询结果:" + account1);
}
- 手动抛了别的异常
spring默认回滚的异常
@Override
@Transactional
public void useExceptionByError(Account account) throws MyException {
accountMapper.insert(account);
throw new MyException("自定义Exception异常");
}
上述情况事务不会回滚,因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。
解决方法:
@Override
@Transactional(rollbackFor = Exception.class)
public void manualDealThrow(Account account){
try {
accountMapper.insert(account);
int i = 10 / 0;
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public int generateAccount(Account account){
int insert = accountMapper.insert(account);
if(insert == 1){
log.info("生成账户:" + account.toString());
}else{
log.error("生成账户失败");
}
return insert;
}
- 自己吞了异常
@Override
@Transactional(rollbackFor = Exception.class)
public void manualDealThrow(Account account){
try {
accountMapper.insert(account);
int i = 10 / 0;
} catch (Exception e) {
e.printStackTrace();
}
}
通过分析源码可以看出,spring回滚机制是根据捕获的异常与rollbackFor值进行比较来判断是否回滚,如果在业务代码中手动处理掉异常,那么异常机制将会失效最终导致事务回滚失效。