什么是事务
事务是逻辑上的一组操作,要么都执行,要么都不执行。我们系统的每个业务方法可能包括了多个原子性的数据库操作,比如下面的savePerson()方法中就有两个原子性的数据库操作。这些原子性的数据库操作是有依赖的,它们要么都执行,要不就都不执行。
public void savePerson() {
personDao.save(person);
personDetailDao.save(personDetail);
}
另外,需要格外注意的是:事务能否生效数据库引擎是否支持事务是关键。比如常用的MySQL数据库默认使用支持事务的innodb引擎。但是,如果把数据库引擎变为myisam,那么程序也就不再支持事务了!
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:
- 将小明的余额减少1000元。
- 将小红的余额增加1000元。
万一在这两个操作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
public class OrdersService {
private 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);
}
}
事务的特性(ACID)
- 原子性(Atomicity):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
- 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
- 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
详谈Spring对事务的支持
⚠️再提醒一次:你的程序是否支持事务首先取决于数据库,比如使用MySQL的话,如果你选择的是innodb引擎,那么恭喜你,是可以支持事务的。但是,如果你的MySQL数据库使用的是myisam引擎的话,那不好意思,从根上就是不支持事务的。
这里再提一下一个非常重要的知识点:MySQL怎么保证原子性的?我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,在MySQL中,恢复机制是通过回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用回滚日志中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚之前未完成的事务。
Spring支持两种方式的事务管理
编程式事务管理
通过TransactionTemplate或者TransactionManager手动管理事务,实际应用中很少使用,但是对于你理解Spring事务管理原理有帮助。使用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);
}
}
声明式事务管理
推荐使用(代码侵入性最小),实际是通过AOP实现(基于@Transactional的全注解方式使用最多)。使用@Transactional注解进行事务管理的示例代码如下:
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
B b = new B();
C c = new C();
b.bMethod();
c.cMethod();
}
Spring事务管理接口介绍
Spring框架中,事务管理相关最重要的3个接口如下:
- PlatformTransactionManager:(平台)事务管理器,Spring事务策略的核心。
- TransactionDefinition:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
- TransactionStatus:事务运行状态。
我们可以把PlatformTransactionManager接口可以被看作是事务上层的管理者,而TransactionDefinition和TransactionStatus这两个接口可以看作是事务的描述。PlatformTransactionManager会根据TransactionDefinition的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理,而TransactionStatus接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。
PlatformTransactionManager:事务管理接口
Spring并不直接管理事务,而是提供了多种事务管理器。Spring事务管理器的接口是:PlatformTransactionManager。通过这个接口,Spring为各个平台如:JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。PlatformTransactionManager接口的具体实现如下:PlatformTransactionManager接口中定义了三个方法:
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager {
//获得事务
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事务
void commit(TransactionStatus var1) throws TransactionException;
//回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}
为什么要定义或者说抽象出来PlatformTransactionManager这个接口呢?主要是因为要将事务管理行为抽象出来,然后不同的平台去实现它,这样我们可以保证提供给外部的行为不变,方便我们扩展。
TransactionDefinition:事务属性
事务管理器接口PlatformTransactionManager通过getTransaction(TransactionDefinition definition)方法来得到一个事务,这个方法里面的参数是TransactionDefinition类,这个类就定义了一些基本的事务属性。什么是事务属性呢?事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了5个方面:
- 隔离级别
- 传播行为
- 回滚规则
- 是否只读
- 事务超时
TransactionDefinition接口中定义了5个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等。
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1;
// 返回事务的传播行为,默认值为REQUIRED。
int getPropagationBehavior();
// 返回事务的隔离级别,默认值是DEFAULT
int getIsolationLevel();
// 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
int getTimeout();
// 返回是否为只读事务,默认值为false
boolean isReadOnly();
@Nullable
String getName();
}
TransactionStatus:事务状态
TransactionStatus接口用来记录事务的状态该接口定义了一组方法,用来获取或判断事务的相应状态信息。PlatformTransactionManager.getTransaction(…)方法返回一个TransactionStatus对象。
TransactionStatus接口内容如下:
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事务
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}
事务属性详解
实际业务开发中,大家一般都是使用@Transactional注解来开启事务,但很多人并不清楚这个注解里面的参数是什么意思,有什么用。为了更好的在项目中使用事务管理,强烈推荐好好阅读一下下面的内容。
事务传播行为
事务传播行为是为了解决业务层方法之间互相调用的事务问题。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。举个例子:我们在A类的aMethod()方法中调用了B类的bMethod()方法。这个时候就涉及到业务层方法之间互相调用的事务问题。如果我们的bMethod()如果发生异常需要回滚,如何配置事务传播行为才能让aMethod()也跟着回滚呢?这个时候就需要事务传播行为的知识了,如果你不知道的话一定要好好看一下。
@Service
class A {
@Autowired
B b;
@Transactional(propagation = Propagation.xxx)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
class B {
@Transactional(propagation = Propagation.xxx)
public void bMethod {
//do something
}
}
在TransactionDefinition定义中包括了如下几个表示传播行为的常量:
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
......
}
不过,为了方便使用,Spring相应地定义了一个枚举类:Propagation
package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;
public enum Propagation {
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
NEVER(TransactionDefinition.PROPAGATION_NEVER),
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
正确的事务传播行为可能的值如下:
TransactionDefinition.PROPAGATION_REQUIRED
使用的最多的一个事务传播行为,我们平时经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:
- 如果外部方法没有开启事务的话,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- 如果外部方法开启事务并且被Propagation.REQUIRED的话,所有Propagation.REQUIRED修饰的内部方法和外部方法均属于同一事务,只要一个方法回滚,整个事务均回滚。
举个例子:如果我们上面的aMethod()和bMethod()使用的都是PROPAGATION_REQUIRED传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。
@Service
class A {
@Autowired
B b;
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
class B {
@Transactional(propagation = Propagation.REQUIRED)
public void bMethod {
//do something
}
}
TransactionDefinition.PROPAGATION_REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
举个例子:如果我们上面的bMethod()使用PROPAGATION_REQUIRES_NEW事务传播行为修饰,aMethod还是用PROPAGATION_REQUIRED修饰的话。如果aMethod()发生异常回滚,bMethod()不会跟着回滚,因为bMethod()开启了独立的事务。但是,如果bMethod()抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod()同样也会回滚,因为这个异常被aMethod()的事务管理机制检测到了。
@Service
class A {
@Autowired
B b;
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
class B {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void bMethod {
//do something
}
}
TransactionDefinition.PROPAGATION_NESTED
如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与TransactionDefinition.PROPAGATION_REQUIRED类似的操作。也就是说:
- 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。
- 如果外部方法无事务,则单独开启一个事务,与PROPAGATION_REQUIRED类似。
这里还是简单举个例子:如果bMethod()回滚的话,aMethod()也会回滚。
@Service
class A {
@Autowired
B b;
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
class B {
@Transactional(propagation = Propagation.NESTED)
public void bMethod {
//do something
}
}
TransactionDefinition.PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。若是错误的配置以下3种事务传播行为,事务将不会发生回滚。
TransactionDefinition.PROPAGATION_SUPPORTS
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
更多关于事务传播行为的内容请看这篇文章:《太难了~面试官让我结合案例讲讲自己对Spring事务传播行为的理解。》
总结下Spring中的事务传播机制
Spring事务传播机制共7中,可以分为3组+1个特殊来分析或者记忆
- REQUIRE组
- REQUIRED:当前存在事务则使用当前的事务,当前不存在事务则创建一个新的事务
- REQUIRES_NEW:创建新事务,如果已经存在事务,则把已存在的事务挂起
- SUPPORT组
- SUPPORTS:支持事务.如果当前存在事务则加入该事务,如果不存在事务则以无事务状态执行
- NOT_SUPPORTED:不支持事务.在无事务状态下执行,如果已经存在事务则挂起已存在的事务
- Exception组
- MANDATORY:必须在事务中执行,如果当前不存在事务,则抛出异常
- NEVER:不可在事务中执行,如果当前存在事务,则抛出异常
- NESTED:嵌套事务.如果当前存在事务,则嵌套执行,如果当前不存在事务,则开启新事务
事务隔离级别
TransactionDefinition接口中定义了五个表示隔离级别的常量:
public interface TransactionDefinition {
......
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
......
}
和事务传播行为那块一样,为了方便使用,Spring也相应地定义了一个枚举类:Isolation
public enum Isolation {
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
private final int value;
Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
下面我依次对每一种事务隔离级别进行介绍:
- TransactionDefinition.ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,MySQL默认采用的REPEATABLE_READ隔离级别,Oracle默认采用的READ_COMMITTED隔离级别.
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED:最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- TransactionDefinition.ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- TransactionDefinition.ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- TransactionDefinition.ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
相关阅读:MySQL事务隔离级别详解。
总结5种隔离级别
DEFAULT
使用数据库本身使用的隔离级别(oracle默认读已提交,mysql默认可重复读)READ_UNCOMITTED
读未提交:事务即使未提交,却可以被别的事务读取到的,这级别的事务隔离有脏读、重复读、幻读的问题。READ_COMMITED
读已提交:当前事务只能读取到其他事务提交的数据,这种事务的隔离级别解决了脏读问题,但还是会存在不可重复读、幻读问题。REPEATABLE_READ
可重复读:限制了读取数据的时候,不可以进行修改,所以解决了不可重复读的问题,但是读取范围数据的时候,是可以插入数据,所以还会存在幻读问题。SERLALIZABLE
串行化:事务最高的隔离级别,在该级别下,所有事务都是进行串行化顺序执行的。可以避免脏读、不可重复读与幻读所有并发问题。但是这种事务隔离级别下,事务执行很耗性能。
MySQL事务隔离级别 | 四个案例看懂MySQL事务隔离级别 | 长文捋明白Spring事务!隔离性?传播性?一网打尽! |
---|---|---|
一文详解幻读、脏读和不可重复读 | 关于幻读,该捋清楚了! |
事务超时属性
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在TransactionDefinition中以int的值来表示超时时间,其单位是秒,默认值为-1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。
事务只读属性
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface TransactionDefinition {
......
// 返回是否为只读事务,默认值为false
boolean isReadOnly();
}
对于只有读取数据查询的事务,可以指定事务类型为readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。很多人就会疑问了,为什么我一个数据查询操作还要启用事务支持呢?拿MySQL的innodb举例子,根据官网描述:
MySQL默认对每一个新建立的连接都启用了autocommit模式。在该模式下,每一个发送到MySQL服务器的sql语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。
但是,如果你给方法加上了Transactional注解的话,这个方法执行的所有sql会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。如果不加Transactional,每条sql会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。分享一下关于事务只读属性,其他人的解答:
- 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
- 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持
事务回滚规则
这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(RuntimeException的子类)时才会回滚,Error也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。如果你想要回滚你定义的特定的异常类型的话,可以这样:
@Transactional(rollbackFor= MyException.class)
@Transactional注解使用详解
@Transactional的作用范围
- 方法:推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到public方法上,否则不生效。
- 类:如果这个注解使用在类上的话,表明该注解对该类中所有的public方法都生效。
- 接口:不推荐在接口上使用。
@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()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
TransactionInterceptor类中的invoke()方法内部实际调用的是TransactionAspectSupport类的invokeWithinTransaction()方法。由于新版本的Spring对这部分重写很大,而且用到了很多响应式编程的知识,这里就不列源码了。
SpringAOP自调用问题
若同一类中的其他没有@Transactional注解的方法内部调用有@Transactional注解的方法,有@Transactional注解的方法的事务会失效。这是由于SpringAOP代理的原因造成的,因为只有当@Transactional注解的方法在类以外被调用的时候,Spring事务管理才生效。MyService类中的method1()调用method2()就会导致method2()的事务失效。
@Service
public class MyService {
private void method1() {
method2();
//......
}
@Transactional
public void method2() {
//......
}
}
解决办法就是避免同一类中自调用或者使用AspectJ取代Spring AOP代理。
@Transactional的使用注意事项总结
- @Transactional注解只有作用到public方法上事务才生效,不推荐在接口上使用;
- 避免同一个类中调用@Transactional注解的方法,这样会导致事务失效;
- 正确的设置@Transactional的rollbackFor和propagation属性,否则事务可能会回滚失败;
- 被@Transactional注解的方法所在的类必须被Spring管理,否则不生效;
- 底层使用的数据库必须支持事务机制,否则不生效;
总结事务失效的集中原因
- 数据库引擎不支持事务
- 事务方法未被Spring管理
- 方法使用final类型修饰
- 非public修饰的方法
- 同一个类中的方法相互调用
- 未配置开启事务,数据源没有配置事务管理器
- 方法的事务传播类型不支持事务(Propagation.NOT_SUPPORTED表示不以事务运行)
- 异常被内部catch
- 异常类型错误
@Transactional public void updateOrder(Order order) { try { // update order } catch { throw new Exception("更新错误"); } } /** * 这样事务也是不生效的,因为默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如: * @Transactional(rollbackFor = Exception.class) * 这个配置仅限于Throwable异常类及其子类。 */
- 多线程调用,会导致两个方法不在同一个线程中,从而是两个不同的事务
事务相关文章
三问Spring事务:解决什么问题?如何解决?存在什么问题? | Spring事务失效了,怎么办?(介绍的很好,包括代理相关内容,跨方法调用导致事务失效的原因) | Spring事务(介绍的很详细) |
---|---|---|
事务中存在多线程,怎么处理? |
分布式事务
CAP理论告诉我们,一个分布式系统不可能同时满足一致(C:Consistency),可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中的2个。
BASE:全称:Basically Available(基本可用),Soft state(软状态),和Eventually consistent(最终一致性)。Base理论是对CAP中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于CAP定理逐步演化而来的。其核心思想是:既是无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)
2PC
阶段一(准备阶段):协调者向所有的参与者询问,是否准备好了执行事务,并开始等待各参与者的响应。执行事务各参与者节点执行事务操作,并将Undo和Redo信息记入事务日志中,各参与者向协调者反馈事务询问的响应,如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行;如果参与者没有成功执行事务,就返回No给协调者,表示事务不可以执行。
阶段二:在阶段二中,会根据阶段一的投票结果执行2种操作:执行事务提交,中断事务。
执行事务提交步骤如下:发送提交请求:协调者向所有参与者发出commit请求。参与者收到commit请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行期间占用的事务资源。参与者在完成事务提交之后,向协调者发送Ack信息。协调者接收到所有参与者反馈的Ack信息后,完成事务。
中断事务步骤如下:发送回滚请求:协调者向所有参与者发出Rollback请求。参与者接收到Rollback请求后,会利用其在阶段一种记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。参与者在完成事务回滚之后,想协调者发送Ack信息。协调者接收到所有参与者反馈的Ack信息后,完成事务中断。
二阶段提交缺点:
- 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
- 单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
- 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
- 二阶段无法解决的问题:协调者发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
由于二阶段提交存在着诸如同步阻塞、单点问题、脑裂等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。
3PC
与两阶段提交不同的是,三阶段提交有两个改动点。
- 引入超时机制。同时在协调者和参与者中都引入超时机制。
- 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的.如果段时间内没有收到协调者的commit请求,那么就会自动进行commit,解决了2PC单点故障的问题。
TCC
2PC要求参与者实现了XA协议,通常用来解决多个数据库之间的事务问题,比较局限。在多个系统服务利用api接口相互调用的时候,就不遵守XA协议了,这时候2PC就不适用了。现代企业多采用分布式的微服务,因此更多的是要解决多个微服务之间的分布式事务问题。
TCC就是一种解决多个微服务之间的分布式事务问题的方案。TCC是Try、Confirm、Cancel三个词的缩写,其本质是一个应用层面上的2PC,同样分为两个阶段:
准备阶段:协调者调用所有的每个微服务提供的try接口,将整个全局事务涉及到的资源锁定住,若锁定成功try接口向协调者返回yes。
提交阶段:若所有的服务的try接口在阶段一都返回yes,则进入提交阶段,协调者调用所有服务的confirm接口,各个服务进行事务提交。如果有任何一个服务的try接口在阶段一返回no或者超时,则协调者调用所有服务的cancel接口
这里有个关键问题,既然TCC是一种服务层面上的2PC。它是如何解决2PC无法应对宕机问题的缺陷的呢?答案是不断重试。
相关文章
MySQL事务
并发事务带来了哪些问题?
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
脏读(Dirtyread)
脏读是指一个事务读取了另一个事务未提交的数据。换句话说,一个事务正在修改某个数据,而另一个事务在这个数据还未被提交的情况下就读取了这些数据。如果第一个事务回滚,那么第二个事务读取到的数据就是无效的(即“脏”的),因为它从未真正存在于数据库中。例如:事务1读取某表中的数据A=20,事务1修改A=A-1,事务2读取到A=19,事务1回滚导致对A的修改并为提交到数据库,A的值还是20。
丢失修改(Losttomodify)
在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1先修改A=A-1,事务2后来也修改A=A-1,最终结果A=19,事务1的修改被丢失。
不可重复读(Unrepeatableread)
不可重复读发生在同一个事务内,多次读取同一数据集合时,由于其他事务的插入、更新或删除操作,导致同一数据集合的内容发生了变化。这违背了事务的“一致性”要求,因为事务在开始时和结束时看到的数据集是不一致的。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2再次读取A=19,此时读取的结果和第一次读取的结果不同。
幻读(Phantomread)
幻读是不可重复读的一种特殊情况,它发生在同一事务中,通过相同的查询条件多次读取数据时,由于其他事务的插入操作,使得每次查询的结果集行数发生了变化。这通常是因为事务在两次查询之间,有其他事务插入了满足查询条件的新行。与不可重复读不同,幻读的重点在于新增的行,而不是已存在行的数据变更。例如:事务2读取某个范围的数据,事务1在这个范围插入了新的数据,事务2再次读取这个范围的数据发现相比于第一次读取的结果多了新的数据。
不可重复读和幻读有什么区别?
- 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
- 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
- 不可重复读和幻读有什么区别
幻读其实可以看作是不可重复读的一种特殊情况,单独区分幻读的原因主要是解决幻读和不可重复读的方案不一样。举个例子:执行delete
和update
操作的时候,可以直接对记录加锁,保证事务安全。而执行insert
操作的时候,由于记录锁(RecordLock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(GapLock)。也就是说执行insert
操作的时候需要依赖Next-KeyLock(RecordLock+GapLock)进行加锁来保证不出现幻读。
并发事务的控制方式有哪些?
MySQL中并发事务的控制方式无非就两种:锁和MVCC。锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversionconcurrencycontrol)可以看作是乐观控制的模式。
锁控制方式下会通过锁来显示控制共享资源而不是通过调度手段,MySQL中主要是通过读写锁来实现并发控制。
- 共享锁(S锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- 排他锁(X锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。
读写锁可以做到读读并行,但是无法做到写读、写写并行。另外,根据根据锁粒度的不同,又被分为表级锁(table-levellocking)和行级锁(row-levellocking)。InnoDB不光支持表级锁,还支持行级锁,默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说,InnoDB的性能更高。不论是表级锁还是行级锁,都存在共享锁(ShareLock,S锁)和排他锁(ExclusiveLock,X锁)这两类。
MVCC是多版本并发控制方法,即对一份数据会存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的。
MVCC在MySQL中实现所依赖的手段主要是:隐藏字段、readview、undolog。
- undolog:undolog用于记录某行数据的多个版本的数据。
- readview和隐藏字段:用来判断当前版本数据的可见性。
关于InnoDB对MVCC的具体实现可以看这篇文章:InnoDB存储引擎对MVCC的实现。
SQL标准定义了哪些事务隔离级别?
SQL标准定义了四个隔离级别:
- READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交):允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
MySQL的隔离级别是基于锁实现的吗?
MySQL的隔离级别基于锁和MVCC机制共同实现的。SERIALIZABLE隔离级别是通过锁来实现的,READ-COMMITTED和REPEATABLE-READ隔离级别是基于MVCC实现的。不过,SERIALIZABLE之外的其他隔离级别可能也需要用到锁机制,就比如REPEATABLE-READ在当前读情况下需要使用加锁读来保证不会出现幻读。
MySQL的默认隔离级别是什么?
MySQLInnoDB存储引擎的默认支持的隔离级别是REPEATABLE-READ(可重读)。我们可以通过SELECT@@tx_isolation;
命令来查看,MySQL8.0该命令改为SELECT@@transaction_isolation;
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
相关文章
面试官灵魂的一击:你懂MySQL事务吗 | MySQL事务的实现原理,写得太好了! | 一文讲清,MySQL如何解决多事务并发问题 |
---|