1. 什么是事务?
事务是以可控的方式对数据资源进行访问的一组操作。在事务执行的前后,系统状态始终处于一种“正确”的状态。
另外,需要格外注意的是:事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的innodb
引擎。但是,如果把数据库引擎变为 myisam
,那么程序也就不再支持事务了!
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是:
-
将小明的余额减少 1000 元
-
将小红的余额增加 1000 元。
万一在这两个操作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。下面是Spring中声明式事务管理的例子:
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 四大特性是事务的基础,下面简单来了解一下。
2. 事物的特性(ACID)了解么?
- 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性: 执行事务前后,数据保持一致;
- 隔离性: 并发访问数据库时,一个用户的事物不被其他事务所干扰也就是说多个事务并发执行时,一个事务的执行不应影响其他事务的执行;
- 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
3. Spring对事务的支持
3.1 Spring支持两种方式的事务管理
1) 编程式事务管理
Spring中有两种方式可以进行编程式的事务管理,要么直接使用PlatformTransactionManager
,要么使用更方便的TransactionTemplate
,二者各有优缺点,总体来说更推荐后者。
使用 PlatformTransactionManager
进行编程式事务管理的示例代码如下:
@Autowired
private PlatformTransactionManager transactionManager;
// 事务的属性
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
// 设置超时时间
definition.setTimeout(20);
...
TransactionStatus txStatus = transactionManager.getTransaction(definition);
try {
// 业务逻辑实现
} catch (ApplicationException e) {
transactionManager.rollback(txStatus);
throw e;
} catch (RuntimeException e) {
transactionManager.rollback(txStatus);
throw e;
} catch (Error e) {
transactionManager.rollback(txStatus);
throw e;
}
transactionManager.commit(txStatus);
2)声明式事务管理
推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于@Transactional
的全注解方式使用最多)。
使用 @Transactional
注解进行事务管理的示例代码如下:
@Transactional(propagation=propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
B b = new B();
C c = new C();
b.bMethod();
c.cMethod();
}
3.2. Spring 事务管理接口介绍
Spring 框架中,事务管理相关最重要的 3 个接口如下:
PlatformTransactionManager
: (平台)事务管理器,Spring 事务策略的核心,它的主要作用是为应用程序提供事务界定的统一方式。TransactionDefinition
: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。TransactionStatus
: 事务运行状态。
我们可以把 PlatformTransactionManager
接口可以被看作是事务上层的管理者,而 TransactionDefinition
和 TransactionStatus
这两个接口可以看作是事物的描述。
PlatformTransactionManager
会根据 TransactionDefinition
的定义比如事务超时时间、隔离界别、传播行为等来进行事务管理 ,而 TransactionStatus
接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。
3.2.1 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;
}
3.2.2. TransactionDefinition:事务属性
事务管理器接口 PlatformTransactionManager
通过 getTransaction(TransactionDefinition definition)
方法来得到一个事务,这个方法里面的参数是 TransactionDefinition
接口的实例 ,这个实例中定义了一些基本的事务属性。TransactionDefinition
接口的默认实现类为DefaultTransactionDefinition
,它主要用于编程式事务,提供了各个事务属性的默认值,并且提供了setter
方法让我们可以自己设置这些属性值。
那么什么是 事务属性 呢?
事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。
事务属性包含了 5 个方面:隔离级别、传播行为、回滚规则、是否只读、事务超时。
TransactionDefinition
接口中定义了 5 个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等。
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();
}
3.2.3. TransactionStatus:事务状态
TransactionStatus
接口定义表示整个事务处理过程中的事务状态,更多时候,我们将在编程式事务中使用该接口。该接口定义了一组方法,用来获取或判断事务的相应状态信息。
PlatformTransactionManager.getTransaction(…)
方法返回一个 TransactionStatus
对象。
TransactionStatus 接口接口内容如下:
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事物
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 标记当前事务以使其回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}
3.3. 事务属性详解
3.3.1. 事务传播行为
在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;
......
}
那么,什么是事务的传播行为?
事务的传播行为表示整个事务处理过程所跨越的业务对象,将以什么样的行为参与事务。针对事务的传播行为,TransactionDefinition
提供了以下几种选择:
- PROPAGATION_REQUIRED。
如果当前存在一个事务,则加入当前事务。如果不存在任何事务,则创建一个新事务。**总之,要至少保证一个事务在运行。PROPAGATION_REQUIRED通常是默认的事务传播行为。
举个例子:我们下面的aMethod()
和bMethod()
使用的都是PROPAGATION_REQUIRED
传播行为,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。
Class A {
@Transactional(propagation=propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
B b = new B();
b.bMethod();
}
}
Class B {
@Transactional(propagation=propagation.PROPAGATION_REQUIRED)
public void bMethod {
//do something
}
}
- PROPAGATION_SUPPORTS。如果当前存在一个事务,则加入当前事务。如果当前不存在事务,则直接执行。对于一些查询的方法来说,PROPAGATION_SUPPORTS通常是比较合适的选择,因为如果当前方法直接执行,那么本就不需要事务的支持。如果当前方法被其他方法调用,而其他方法启动了一个事务,使用PROPAGATION_SUPPORTS可以保证当前方法能够加入到当前事务,并洞悉当前事务对数据资源所做的更新。
- PROPAGATION_MANDATORY。PROPAGATION_MANDATORY强制要求当前存在一个事务,如果不存在,则抛出异常。如果某个方法需要事务支持,但自身又不管理事务提交或者回滚,那么比较适合使用PROPAGATION_MANDATORY。
- PROPAGATION_REQUESTS_NEW。不管当前是否存在事务,都会创建新的事务,且开启的事务相互独立,互不干扰。如果当前存在事务,则会将当前的事务挂起。PROPAGATION_REQUESTS_NEW主要用于某个业务对象想做的事情不想影响到外层事务,比如更新某些日志信息,即使更新失败,我们也不想因此而回滚事务,从而影响到外界事务的成功提交。
举个例子:如果我们上面的bMethod()
使用PROPAGATION_REQUIRES_NEW
事务传播行为修饰,aMethod
还是用PROPAGATION_REQUIRED
修饰的话。如果aMethod()
发生异常回滚,bMethod()
不会跟着回滚,因为bMethod()
开启了独立的事务。但是,如果bMethod()
抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod()
同样也会回滚,因为这个异常被aMethod()
的事务管理机制检测到了。
Class A {
@Transactional(propagation=propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
B b = new B();
b.bMethod();
}
}
Class B {
@Transactional(propagation=propagation.REQUIRES_NEW)
public void bMethod {
//do something
}
}
- PROPAGATION_NOT_SUPPORTED。不支持当前事务,而是在没有事务的情况下执行。如果当前存在事务的话,当前事务原则上挂起,但这要看对应的
PlatformTransactionManager
实现类是否支持事务的挂起。 - PROPAGATION_NEVER。永远不需要当前存在事务,如果存在当前事务,则抛出异常。
- PROPAGATOION_NESTED。如果存在当前事务,则在当前事务的一个嵌套事务中执行,否则将创建新的事务,在创建的新事务中执行。PROPAGATOION_NESTED可能的应用场景在于,你可以将一个大的事务划分为多个小的事务来处理,并且外层事务可以根据各个内部嵌套事务的执行结果,来选择不同的执行流程。
这里还是简单举个例子:
如果aMethod()
回滚的话,bMethod()
和bMethod2()
都要回滚,而bMethod()
回滚的话,并不会造成aMethod()
和bMethod()
回滚。
Class A {
@Transactional(propagation=propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
B b = new B();
b.bMethod();
b.bMethod2();
}
}
Class B {
@Transactional(propagation=propagation.PROPAGATION_NESTED)
public void bMethod {
//do something
}
@Transactional(propagation=propagation.PROPAGATION_NESTED)
public void bMethod2 {
//do something
}
}
3.3.2 事务隔离级别
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;
......
}
下面我依次对每一种事务隔离级别进行介绍:
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 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
3.3.3. 事务超时属性
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition
中以 int 的值来表示超时时间,其单位是秒,默认值为-1。
3.3.4. 事务只读属性
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface TransactionDefinition {
......
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();
}
对于只有读取数据查询的事务,可以指定事务类型为read-only,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
很多人就会疑问了,为什么我一个数据查询操作还要启用事务支持呢?
拿 MySQL 的 innodb 举例子,根据官网 https://dev.mysql.com/doc/refman/5.7/en/innodb-autocommit-commit-rollback.html 描述:
MySQL 默认对每一个新建立的连接都启用了
autocommit
模式。在该模式下,每一个发送到 MySQL 服务器的sql
语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。
但是,如果你给方法加上了Transactional
注解的话,这个方法执行的所有sql
会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。
如果不加Transactional
,每条sql
会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。
3.3.4. 事务回滚规则
这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。
如果你想要回滚你定义的特定的异常类型的话,可以这样:
@Transactional(rollbackFor= MyException.class)
3.4. @Transactional 注解使用详解
1) @Transactional
的作用范围
- 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
- 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
- 接口 :不推荐在接口上使用。
2) @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 | 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。 |
3)@Transactional
事务注解原理
面试中在问 AOP 的时候可能会被问到的一个问题。简单说下吧!
我们知道,@Transactional
的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
关于Spring中AOP的实现机制,可以参考前文https://www.cnblogs.com/muuu520/p/12828290.html。我们知道,如果我们需要实现AroundAdvice类型的横切逻辑,我们需要去实现MethodInterceptor
接口,在业务方法执行开始之前开启一个事务,当方法执行完成或者异常退出的时候就提交事务或者回滚事务。下面我们看一下这个拦截器的原型代码实例:
public class PrototypeTransactionInterceptor implements MethodInterceptor {
private PlatformTransactionManager transactionManager;
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
TransactionDefinition definition = getTransactionDefinitionByMethod(method);
TransactionStatu txStatus = transactionManager.getTransaction(definition);
Object result = null;
try {
result = invocation.proceed();
} catch (Throwable t) {
if (needRollBackOn(t)) {
transactionManager.rollback(txStatus);
} else {
transactionManager.commit(txStatus);
}
throw t;
}
transactionManager.commit(txStatus);
return result;
}
}
如果一个类或者一个类中的 public 方法上被标注@Transactional
注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解的 public 方法的时候,实际调用的是,TransactionInterceptor
类中的 invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
TransactionInterceptor
类中的invoke()
方法内部实际调用的是TransactionAspectSupport
类的invokeWithinTransaction()
方法。由于新版本的 Spring 对这部分重写很大,而且用到了很多响应式编程的知识,这里就不列源码了。
4)Spring AOP 自调用问题
若同一类中的其他没有 @Transactional
注解的方法内部调用有 @Transactional
注解的方法,有@Transactional
注解的方法的事务会失效。这是为什么呢?首先,我们要知道,我们对于没有@Transactional
注解的方法是不开启事务的,所以我们调用this.method2()
的时候此时并没有事务,而method2()
在method1()
中现在只是一个普通方法而已。如果我们是从Controller
调用带有注解的method2()
的Service方法时,系统才会为我们自动生成一个代理对象供我们调用,这个代理对象的method2()
方法是开启了事务的。
MyService
类中的method1()
调用method2()
就会导致method2()
的事务失效。
@Service
public class MyService {
private void method1() {
this.method2();
//......
}
@Transactional
public void method2() {
//......
}
}
解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。还可以在method1
中获取该Service类的代理对象,再执行proxyObject.method2()
。
5) @Transactional
的使用注意事项总结
@Transactional
注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;- 避免同一个类中调用
@Transactional
注解的方法,这样会导致事务失效; - 正确的设置
@Transactional
的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败。