前言
本文出自Java 学习/面试指南,感谢作者的整理.
一、什么是事务?
事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的innodb引擎。但是,如果把数据库引擎变为 myisam,那么程序也就不再支持事务了!
二、事务的四个特性
事务的四个特性:ACID
原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;一致性(Consistency): 执行事务前后,数据保持一致;
隔离性(Isolation): 并发访问数据库时,一个用户的事物不被其他事务所干扰也就是说多个事务并发执行时,一个事务的执行不应影响其他事务的执行;
持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
- 注意:
- 原子性:在MySQL中实现操作的原子性是使用回滚日志实现的,每次进行事务的时候,先将一系列操作写入回滚日志中,然后操作数据库,如果过程中出现异常,就将数据回滚到事务开始前.
- 一致性表示:进行事务之后保持数据一致性,也就是说A转账给B200元,事务结束之后,A确实少了200,B也确实增加了200,前后数据保持一致.
- 隔离性:由于数据的并发读写,很容易出现脏读,错读,重复读等现象,所以需要将事务隔离 .根据不同的情况设置不同的隔离级别.
三、Spring 对事务的支持
1. Spring中实现事务管理的两种方式
(1)编程式事务管理:
通过 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);
}
}
(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();
}
2.Spring中事务管理的核心接口
-
PlatformTransactionManager
:平台事务管理器,也就是Spring框架的核心事务控制代码.里面定义的三个方法,获取事务,提交事务,回滚事务.TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
-
TransactionDefinition
:事务定义信息(主要包含事务的传播行为,事务的隔离级别,超时,只读
, 回滚规则等),platformTransactionManage使用TransactionDefinition作为参数获取TransactionStatus.
对应的接口中定义了想要的方法. -
TransactionStatus
:事务运行状态.在PlatformTransactionManage中的方法返回值就是这个接口类.
我们可以把 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而 TransactionDefinition 和 TransactionStatus 这两个接口可以看作是事物的描述。
PlatformTransactionManager 会根据 TransactionDefinition 的定义比如事务超时时间、隔离界别、传播行为等来进行事务管理 ,而 TransactionStatus 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。
四、事务的基本属性
1. 事务传播行为
事务传播行为是为了解决业务层方法之间互相调用的事务问题。
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
在TransactionDefinition类中定义的传播行为有7种:
- PROPAGATION_REQUIRED 如果当前存在事务,则加入该事务;如果没有,则创建一个新事务。(默认)
- PROPAGATION_SUPPORTS 如果当前存在事务,则加入;如果没有,则以非事务方式执行。
- PROPAGATION_MANDATORY 如果当前存在事务,则加入;如果没有,则抛出异常。
- PROPAGATION_REQUIRES_NEW 无论当前是否存在事务,都创建一个新事务,并挂起当前事务(如果有)。
- PROPAGATION_NOT_SUPPORTED 以非事务方式执行,挂起当前事务(如果有)。
- PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
- PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务中执行;如果没有,则创建一个新事务。
被调用方法的事务传播行为决定了它在被调用时如何处理事务
2. 隔离级别
隔离性:并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的
2.1 事务相互干扰可能出现的问题:
- 脏读:当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
- 丢失修改:两个请求都获取了数据,这时候一个事务先修改了数据,之后另外一个事务也修改了数据,结果第一次的修改就丢失了.因此称为丢失修改.
- 不可重复读:在一次事务中,多次读取同一的数据,在第二次读取的时候,这个数据已经被其他事务修改了,所以这个事务前后读取到的数据不一致,所以称为不可重复读.
- 幻读: 跟不可重复读类似.第一请求获取一个符合条件的集合,在第二次获取的时候,另外一个事务向数据库中添加了另外符合条件的数据.所以第一个请求再次通过相同的条件获取数据集合跟第一次获取的数据集合不同.就好像发生了幻觉一样,所以称为幻读。
2.2 SQL中的四个事务隔离级别
其实就是数据库允许在什么情况下读取数据。
- 读取未提交(READ-UNCOMMITTED): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读.
- 读取已提交(READ-COMMITTED): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生.
- 可重复读(REPEATABLE-READ): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- 可串行化(SERIALIZABLE): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
RR:可重复读隔离级别,由于可串行化导致系统性能非常低,所以,引入间隙锁,在RR级别来解决幻读问题。
注意:实现RR级别解决幻读问题是通过next-key clock算法实现的
,可以查看这个链接理解.
innoDB默认
的隔离级别是可重复读(Repeatable Read),并且会以Next-Key Lock的方式对数据行进行加锁。Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。
当查询的索引含有唯一属性(唯一索引,主键索引)时,Innodb存储引擎会对next-key lock进行优化,将其降为record lock,即仅锁住索引本身,而不是范围(因为加入了唯一索引条件,即便添加了新数据,这个数据也不会出现在上一次查询的有效数据范围之内。除非第一查询时候,这个唯一索引数据不存在,所以数据引擎也会添加间隙锁)。
2.3 Spring中TransactionDefinition定义的五个隔离级别
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 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
注意:Spring中的后面四个事务隔离级别跟Sql数据库的四个隔离级别对应,对照着理解比较容易.
3. 事务超时属性
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1。
int TIMEOUT_DEFAULT = -1;
4. 事务只读属性
对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
- 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;
- 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持.
boolean isReadOnly();
5.事务回滚规则
默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。
如果你想要回滚你定义的特定的异常类型的话,可以这样:
@Transactional(rollbackFor= MyException.class)
五、Spring中使用注解事务管理
@Transactional
1. @Transactional 注解作用范围
- 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到
public
方法上,否则不生效。 - 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的
public
方法都生效。 - 接口 :不推荐在接口上使用。
2. @Transactional 的常用配置参数
属性名 | 说明 |
---|---|
propagation | 事务的传播行为,默认值为 REQUIRED,可选的值在上面介绍过 |
isolation | 事务的隔离级别,默认值采用 DEFAULT,可选的值在上面介绍过 |
timeout | 事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。 |
readOnly | 指定事务是否为只读事务,默认值为 false。 |
rollbackFor | 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。 |
3. AOP问题
@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有@Transactional 注解的方法的事务会失效。
这是由于Spring AOP代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。
4. @Transactional 的使用注意事项总结
- @Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
- 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;
- 正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败