一. 什么是事务
简单的说事务是逻辑上的一组操作,要么都执行,要么都不执行;
在我们的日常开发中,我们系统的每个业务方法可能包括了多个原子性的数据库操作,例如下面的saveUserInfo()
,就包含多个原子性的数据库操作,这些原子性的数据库操作,要么都执行,要么都不执行。
public void saveUserInfo() {
userInfoDao.save(userBase);
10 / 0;
userInfoDetail.save(userInfoDetail);
}
但是,我们需要注意的是:事务能否生效的关键是数据库引擎是否支持。比如我们常用的MySQL
数据库,默认使用支持事务的innodb
引擎,如果我们使用myisam
引擎,那么就不再支持事务了。
事务举例:
比如张三要给李四转一万块钱,这个转账会涉及两个关键操作
1.张三的银行账户余额减少10000
2.李四的银行账户余额增加10000
如果这两个操作之间突然出现了错误,比如银行系统崩溃、网络故障、服务宕机;导致张三余额减少,而李四的余额没有增加,那么这样就不对了。事务就是要保证这两个关键操作要么都成功,要么都失败。
public class TransferAccountsServiceImpl{
@Resource
private ITransferAccountsDao transferAccountsDao;
@Transactional(propagation = Propagation.REQUIRED)
public void transferAccount() {
transferAccountsDao.addMonney(10000, "lisi");
10 / 0;
transferAccountsDao.reduceMonney(10000, "zhangsan");
}
}
二. 事务的特点ACID
原子性(Atomicity)
:事务最小的执行单位,不允许分割,事务的原子性确保动作要么全部完成,要么完全失败。一致性(Consistency)
:执行事务前后,数据保持一致,例如在上面的转账例子中,无论事务是否成功,转账者和收款人的总额应该是不变的。隔离性(Isolation)
:并发访问数据库时,一个用户的事务不被其它事务干扰,各并发事务之间的数据库是独立的。持久性(Durability)
:一个事务被提交后,它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。三. Spring对事务的支持
程序是否支持事务的先决条件是数据库,比如使用MySQL的话,如果选择的是innodb,那么支持事务,如果选择的是myisam,那么从根上就不支持事务了
思考1:MySQL怎么保证原子性?
如果要保证原子性,就需要在发生异常时,对已经执行的操作进行回滚,在MySQL中,恢复机制是通过
回滚日志
实现的,所有事务进行的修改,都会先记录到这个回滚日志
中,然后再执行相关的操作。如果在执行过程中遇到异常,我们直接利用
回滚日志
中的信息将数据回滚到修改之前的样子即可,并且回滚日志
会先将数据持久化到磁盘上,这样就可以保证即便在遇到数据库突然宕机,当用户再次重启数据库时,数据库还是能够通过查回滚日志
来回滚之前未完成的事务。
1. Spring支持两种事务管理
编程事务管理
通过TransactionTemplate
或者TransactionManager
手动管理事务,在实际应用中却很少使用,下面通过代码来演示,使用TransactionTemplate
进行编程式事务管理
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransactionTemplate() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(final TransactionStatus transactionStatus) {
try {
//TODO 业务代码
} catch (final Exception e) {
// 异常时回滚
transactionStatus.setRollbackOnly();
}
}
});
}
使用TransactionManager
进行编程式事务管理
@Resource
private PlatformTransactionManager transactionManager;
public void testTransactionManager() {
final TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
//TODO 业务代码
transactionManager.commit(status);
} catch (final Exception e) {
// 异常时回滚
transactionManager.rollback(status);
}
}
声明式事务管理
声明式事务管理,实际上是通过AOP
实现,基于@Transactional
的注解使用最多
使用@Transactional
注解进行事务管理
@Transactional
public void testTransactional() {
userInfoDao.save(userInfo);
userInfoDetailDao.save(userInfoDetail);
}
2. Spring事务管理接口介绍
在Spring
框架中,事务管理最重要的3个接口:
PlatformTransactionManager
:平台事务管理器,Spring事务策略核心。TransactionDefinition
:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。TransactionStatus
:事务运行状态。可以将PlatformTransactionManager
接口看作是事务上层管理者,而TransactionDefinition
和TransactionStatus
这两个接口可以看作是事务的描述。
PlatformTransactionManager
会根据TransactionDefinition
定义的事务超时时间、隔离级别、传播行为等来进行事务管理,TransactionStatus
接口则提供一些方法来获取事务相应的状态(比如是否是新事务、是否可以回滚等)。
事务管理器:PlatformTransactionManager
PlatformTransactionManager
:通过这个接口,Spring
为各个数据库持久层框架,例如JDBC(DataSourceTransactionManager)
、Hibernate(HibernateTransactionManager)
、JPA(JpaTransactionManager)
等提供了对应的事务管理器,具体如何实现,就根据各个框架的特性实际出发。
SpringBoot
中PlatformTransactionManager
接口定义如下:
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
Spring
中PlatformTransactionManager
接口定义如下:
package org.springframework.transaction;
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition var1) throws TransactionException;
void commit(TransactionStatus var1) throws TransactionException;
void rollback(TransactionStatus var1) throws TransactionException;
}
思考2:为什么要将PlatformTransactionManager
定义成接口呢?
将事务管理行为抽象出来,然后不同的平台去实现这个接口,这样我们可以保证提供给外部的行为不变,方便扩展。
事务属性:TransactionDefinition
事务管理器接口PlatformTransactionManager
通过getTransaction(TransactionDefinition var1)
方法来得到事务,这个方法的参数是TransactionDefinition
类型,这个类定义了一些基本的事务属性
事务属性包含了以下5个方面:
- 隔离级别
- 传播行为
- 回滚规则
- 是否只读
- 事务超时
TransactionDefinition
接口定义如下:
package org.springframework.transaction;
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;
int getPropagationBehavior();
int getIsolationLevel();
int getTimeout();
boolean isReadOnly();
String getName();
}
事务状态:TransactionStatus
TransactionStatus
接口用来记录事务的状态,这个接口定义了一组方法,用来获取或判断事务相应的状态信息,PlatformTransactionManager.getTransaction(TransactionDefinition var1)
方法返回一个TransactionStatus
对象。
TransactionStatus
接口定义如下:
package org.springframework.transaction;
import java.io.Flushable;
public interface TransactionStatus extends SavepointManager, Flushable {
boolean isNewTransaction();
boolean hasSavepoint();
void setRollbackOnly();
boolean isRollbackOnly();
void flush();
boolean isCompleted();
}
3. 事务注解详解
在实际的业务开发中,大家一般使用@Transactional
注解来开启事务,但很多人并不是很清楚这个注解中的参数是什么意思?有什么用?下面我将通过一些样例代码来介绍这个注解。
事务传播行为是为了解决业务层方法之间互相调用的事务问题
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播:方法可能继续在现有的事务中运行,也有可能开启一个新事务,并在自己的事务中运行
例如:
在DemoService1
类中的service1()
方法中调用了DemoService2
类中的service2()
方法。这就涉及到了业务层方法之间互相调用的事务问题了,如果在service2
方法中出现了异常需要回滚,如何配置事务传播行为才能让service1
也跟着回滚呢?
@Service
public class DemoService1 {
@Resource
private DemoService2 demoService2;
public void service1() {
demoService2.service2();
//
}
}
public class DemoService2 {
@Transactional(propagation = Propagation.XXX)
public void service2() {
//TOOD
}
}
在TransactionDefinition
中,定义了7个表示传播行为的常量
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;
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
事务传播行为可能的值
TransactionDefinition.PROPAGATION_REQUIRED
使用最多的一个事务传播行为,平常我们使用的@Transactional
注解默认使用的就是这个事务传播行为。如果外部方法没有开启事务,
Propagation.REQUIRED
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。如果外部方法开启了事务,且使用了Propagation.REQUIRED
修饰,所有Propagation.REQUIRED
修饰的内部方法和外部方法均属于同一个事务,只要一个方法回滚,整个事务就回滚。
例如:
上面的service1()
和service2()
方法都使用PROPAGATION_REQUIRED
传播事务行为,两者使用的是同一个事务,只要其中的一个方法回滚,整个事务就回滚
@Service
public class DemoService1 {
@Resource
private DemoService2 demoService2;
@Transactional(propagation = Propagation.REQUIRED)
public void service1() {
demoService2.service2();
}
}
public class DemoService2 {
@Transactional(propagation = Propagation.REQUIRED)
public void service2() {
//TOOD
}
}
TransactionDefinition.PROPAGATION_REQUIRES_NEW
创建一个新事务,如果当前存在事务,把当前挂起,也就是说无论外部方法是否开启事务,Propagation.REQUIRES_NEW
修饰的内部方法会新开启自己的事务,且事务相互独立,互不打扰。例如:上面的service2()
使用PROPAGATION_REQUIRES_NEW
事务传播行为修饰,service1()
还是使用PROPAGATION_REQUIRED
修饰,如果service1()
发生异常回滚,service2()
不会跟着回滚,因为service2()
开启了独立的事务。但是service2()
抛出了未捕获的异常且该异常满足事务回滚规则,service2()
也同样会回滚,因为这个异常被service1()
是事务管理机制检测到了。
@Service
public class DemoService1 {
@Resource
private DemoService2 demoService2;
@Transactional(propagation = Propagation.REQUIRED)
public void service1() {
demoService2.service2();
10 / 0
}
}
public class DemoService2 {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void service2() {
//TOOD
}
}
TransactionDefinition.PROPAGATION_NESTED
如果当前存在事务,就在嵌套事务内执行,如果当前没有事务,就执行与TransactionDefinition.PROPAGATION_REQUIRED
类似的操作。在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在;如果外部方法没有开启事务,则单独开启一个事务,与PROPAGATION_REQUIRED
类似。
例如:如果service2()
回滚,service1()
不会回滚,如果service1()
回滚,service2()
也回滚
@Service
public class DemoService1 {
@Resource
private DemoService2 demoService2;
@Transactional(propagation = Propagation.REQUIRED)
public void service1() {
demoService2.service2();
}
}
public class DemoService2 {
@Transactional(propagation = Propagation.NESTED)
public void service2() {
//TOOD
}
}
TransactionDefinition.PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。TransactionDefinition.PROPAGATION_SUPPORTS
如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式继续运行。TransactionDefinition.PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。TransactionDefinition.PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。事务隔离级别
TransactionDefinition
接口中定义了5个表示隔离级别的常量
package org.springframework.transaction;
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
package org.springframework.transaction.annotation;
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private 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 的隔离级别,所有的事务逐个执行,保证事务之间互不干扰,可以有效防止脏读、幻读、不可重复读,这种隔离级别会影响程序性能,通常情况下也不会用到该级别 |
事务超时属性
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition
中以 int
的值来表示超时时间,其单位是秒,默认值为-1
,这表示事务的超时时间取决于底层事务系统或者没有超时时间。
事务只读属性
在TransactionDefinition
接口中定义了isReadOnly()
package org.springframework.transaction;
public interface TransactionDefinition {
boolean isReadOnly();
}
对于只有读取数据查询的事务,可以指定事务类型为 readonly
,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适用在有多条数据库查询操作的方法中。
思考3:为什么数据查询操作还要启用事务支持呢?
MySQL 默认对每一个新建立的连接都启用了
autocommit
模式。在该模式下,每一个发送到 MySQL 服务器的sql
语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。如果在方法加上了Transactional
注解,这个方法执行的所有sql
会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的效率问题。如果不加Transactional
,每条sql
会开启一个单独的事务,期间被其它事务改了数据,都会实时读取到最新值。
事务回滚规则
定义哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(RuntimeException
的子类)才会回滚,Error
也会导致事务回滚,但是,在遇到检查型(Checked
)异常时不会回滚。
如果想要回滚自定义的异常,代码可以如下这样写:
@Transactional(rollbackFor= MyException.class)
Transactional注解
@Transactional
的作用范围
- 方法:推荐将注解用于方法上,不过需要注意的是:该注解只能应用到
public
方法上,否则不生效。 - 类:如果将注解用在类上,表明该类中所有的
public
方法都生效。 - 接口:不推荐在接口上使用。
@Transactional
的参数
@Transactional
注解源码如下,里面包含了基本事务属性的配置:
package org.springframework.transaction.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
String value() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
boolean readOnly() default false;
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.");
} else {
return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
}
}
}
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
Class[] ifcs = config.getProxiedInterfaces();
return ifcs.length == 0 || ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0]);
}
}
如果一个类或者一个类中的 public
方法上被@Transactional
注解修饰,Spring
容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解修饰的 public
方法时,实际调用的是TransactionInterceptor
类中的 invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
Spring AOP 自调用问题
当方法被@Transactional
注解修饰时,Spring
事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。
这是由Spring AOP
工作原理决定的。Spring AOP
使用动态代理来实现事务的管理,它会在运行的时候为带有 @Transactional
注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用,代理对象就会拦截方法调用并处理事务。但是在同类中的其他方法内部调用的时候,我们代理对象就无法拦截到这个内部调用,因此事务也就失效了。
DemoService3
类中的method1()
调用method2()
就会导致method2()
的事务失效。解决办法就是避免同一类中自调用或者使用 AspectJ
取代 Spring AOP
代理
样例:
@Service
public class DemoService3 {
private void method1() {
method2();
//TODO
}
@Transactional
public void method2() {
//TODO
}
}
在自调用的时候开启事务,这是因为使用了 AopContext.currentProxy()
方法来获取当前类的代理对象,然后通过代理对象调用 method2()
。这样就相当于从外部调用了 method2()
,因此事务注解会生效。
但是我们一般也不会在代码中这么写
样例(不推荐):
@Service
public class DemoService4 {
private void method1() {
// 先获取该类的代理对象,然后通过代理对象调用method2。
((MyService)AopContext.currentProxy()).method2();
//TODO
}
@Transactional
public void method2() {
//TODO
}
}
四. 总结
@Transactional
注解只有作用在public
修饰的方法上才生效,不推荐在接口上使用。- 避免同一个类中调用
@Transactional
注解的方法,这样会导致事务失效。 - 正确的设置
@Transactional
的rollbackFor
和propagation
属性,否则事务可能会回滚失败; - 使用
@Transactional
注解的方法所在的类必须被Spring
管理,否则不生效; - 底层使用的数据库必须支持事务机制,否则不生效;