Spring声明式数据库事务约定
为了省去令人厌烦的try···catch···finally语句,减少那些数据库连接开闭和事务回滚提交的代码,Spring利用其AOP为我们提供了一个数据库事务的约定流程,通过这个约定流程就可以减少大量冗余代码和一些没必要的try···catch···finally语句,让开发者能够更加集中于业务的开发,而不是数据库连接资源和事务的功能开发,这样开发的代码可读性更高,也更好维护。
对于事务,需要通过标注告诉Spring在什么地方启用数据库事务功能。对于声明式事务,是使用@Transactional进行标注的。这个注解可以标注在类上或者方法上,当标注在类上时,代表这个类所有公共非静态的方法都将启用事务功能。在@Transactional中,还允许配置许多的属性, 事务的隔离级别和传播行为:又如异常类型 ,从而确定方法发生什么异常 下回滚事务或者发生什么异常下不回滚事务等。这些配置内容,是在 Spring IoC 容器在加载时就会将这些配置信息解析出来,然后把这些信息存到事务定义器( TransactionDefinition 接口的实现类) 并且记录哪些类或者方法需要启动事务功能,采取什么策略去执行事务。这个过程中,我们所需要做的只是给需要事务的类或者方法标注@Transactional 和配置其属性而己,并不是很复杂。
隔离级别
因为互联网应用时刻面对着高并发的环境 ,如商品库存,时刻都是多个线程共 享的数据,这样就会在多线程的环境中扣减商品库存。对于数据库而言 就会出现多个事务同时访问同一记录的情况, 这样引起数据出现不一致的情况,便是数据库的丢失更新( Lost Update)问 题。应该说,隔离级别是数据库 的概念,有些难度 ,所以在使用它之前应该先了解数据库的相关知识。
数据库事务的知识
数据库事务具有以下4个基本特征 也就是著名的 ACID 。
- Atomic (原子性):事务中包含的操作被看作一个整体的业务单元, 这个业务单元中的操作 要么全部成功,要么全部失败,不会出现部分失败、部分成功的场景。
- Consistency (一致性):事务在完成时,必须使所有的数据都保持一致状态,在数据库中所有的修改都基于事务,保证了数据的完整性。
- Isolation (隔离性) 这是我们讨论的核心内容,正如上述,可能多个应用程序线程同时访问同一数据,这样数据库同样的数据就会在各个不同的事务中被访问,这样会产生丢失更新。 为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过它的选择,可以在不同程度 上压制丢失更新的发生。因为互联网的应用常常面对高并发的场景,所以隔离性是需要掌握 的重点内容。
- Durability (持久性):事务结束后,所有的数据会固化到一个地方,如保存到磁盘当中,即 使断电重启后也可以提供给应用程序访问。
这4个特性,除了隔离性,都还是比较好理解的,所以这里会更为深入地讨论隔离性。在多个事务同时操作数据的情况下,会引发丢失更新的场景,例如,电商有一种商品,在疯狂抢购中,会出现多个事务同时访问商品库存的场景,这样就会产生丢失更新。下面假设一种商品的库存数量还有 100, 每次抢购都只能抢购1件商品, 那么在抢购中就可能出现下面这种场景。
| 时刻 | 事务1 | 事务2 |
|---|---|---|
| T1 | 初始库存100 | 初始库存100 |
| T2 | 扣减库存,余99 | — |
| T3 | — | 扣减库存,余99 |
| T4 | — | 提交事务,库存变为99 |
| T5 | 提交事务,库存变为99 | — |
注意T5时刻提交的事务。因为在事务1中,无法感知事务2的操作,这样它就不知道事务2已经修改过了数据 ,因为它依旧认为只是发生了一笔业务,所以库存变为了 99 ,而这个结果又是一个 错误的结果。这样, T5时刻事务1提交的事务,就会引发事务2提交结果的丢失 ,为了克服这些问题,数据库提出了事务之间的隔离级别概念。
为了压制丢失问题,数据库提出了4类隔离级别,在不同程度上压制丢失更新,这4类隔离级别是未提交读、读写提交、可重复读和串行化,它们会在不通程度上压制丢失更新的情景。
也许你会有一个疑问,都全部消除丢失更新不就好了吗,为什么只是在不同的程度上压制丢失更新呢?其实这个问题是从两个角度去看的,一个是数据的一致性,另一个是性能。数据库现有的技术完全可以避免丢失更新,但是这样做的代价,就是付出锁的代价,在互联网中,系统不单单要考虑数据的一致性,还要考虑系统的性能。试想,在互联网中使用过多的锁,一旦出现商品抢购这样的场景必然会导致大量的线程被挂起和恢复,因为使用了锁之后,一个时刻只能有一个线程访问数据,这样整个系统就会十分缓慢,当系统被数千甚至数万用户同时访问时,过多的锁就会引发宕机,大部分用户线程被挂起,等待持有锁事务的完成,这样用户体验就会十分糟糕。因为用户等待的时间会十分漫长,一般而言,互联网系统响应超过5秒,就会让用户觉得很不友好,进而引发用户忠诚度下降的问题。所以选择隔离级别的时候,既需要考虑数据的一致性避免脏数据,又要考虑系统性能的问题。因此数据库的规范就提出了4种隔离级别来在不同的程度上压制丢失更新。下面我们通过商品抢购的场景来讲述这4种隔离级别的区别。
-
未提交读
未提交读(read uncommitted)是最低的隔离级别,其含义是允许一个事务读取另外一个事务没 有提交的数据。未提交读是一种危险的隔离级别,所以一般在我们实际的开发中应用不广,但是它 的优点在于并发能力高,适合那些对数据一致性没有要求而追求高并发的场景,它的最大坏处是出现脏读。让我们看看可能发生的脏读场景。
时刻 事务1 事务2 备注 T0 ······ ······ 商品库存初始化为2 T1 读取库存为2 T2 扣减库存 库存为1 T3 扣减库存 库存为0,读取事务1未提交的库存数据 T4 提交事务 库存保存为0 T5 回滚事务 库存为0,结果错误 脏读一般是比较危险的隔离级别,在我们实际应用中采用的不多,为了克服脏读问题,数据库隔离级别还提供了读写提交(read commited)的级别。
2.读写提交
读写提交(read commited)隔离级别,是指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。脏读的场景限制为读写提交之后,就变成下面这种场景。
| 时刻 | 事务1 | 事务2 | 备注 |
|---|---|---|---|
| T0 | ······ | ······ | 商品库存初始化为2 |
| T1 | 读取库存为2 | ||
| T2 | 扣减库存 | 库存为1 | |
| T3 | 扣减库存 | 库存为1,读取不到事务1未提交的库存数据 | |
| T4 | 提交事务 | 库存保存为1 | |
| T5 | 回滚事务 | 库存为1,结果正确 |
在T3时刻,由于采用读写提交的隔离级别,因此事务2不能读取事务1中未提交的库存1,所以扣减库存的结果依旧为1,然后它提交事务,则库存在T4时刻变为了1。T5时刻,事务1回滚,结果库存仍为1,这是一个正确结果。但是读写提交会产生不可重复读场景。
| 时刻 | 事务1 | 事务2 | 备注 |
|---|---|---|---|
| T0 | ······ | ······ | 商品库存初始化为1 |
| T1 | 读取库存为1 | ||
| T2 | 扣减库存 | 事务为提交 | |
| T3 | 读取库存为1 | 认为可扣减 | |
| T4 | 提交事务 | 库存变为0 | |
| T5 | 扣减失败 | 失败,因为此时库存为0,无法扣减 |
在T3时刻事务2读取库存的时候,因为事务1未提交事务,所以读出的库存为1,于是事务2 认为当前可扣减库存;在T4时刻,事务1己经提交事务,所以在T5时刻,它扣减库存的时候就发 现库存为0,于是就无法扣减库存了。这里的问题在于事务2之前认为可以扣减,而到扣减那一步却 发现己经不可以扣减,于是库存对于事务2而言是一个可变化的值,这样的现象我们称为不可重复读,这就是读写提交的一个不足。为了克服这个不足,数据库的隔离级别还提出了可重复读的隔离级别,它能够消除不可重读的问题。
3.可重复读
可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候,可能出现一些值的变化,影响当前事务的执行,如上述的库存是个变化的值,这个时候数据库提出了可重 复读的隔离级别 。这样就能够克服不可重复读的现象。
| 时刻 | 事务1 | 事务2 | 备注 |
|---|---|---|---|
| T0 | ······ | ······ | 商品库存初始化为1 |
| T1 | 读取库存为1 | ||
| T2 | 扣减库存 | 事务未提交 | |
| T3 | 尝试读取库存 | 不允许读取,等待事务1提交 | |
| T4 | 提交事务 | 库存变为0 | |
| T5 | 读取库存 | 库存为0,无法扣减 |
可以看到,事务 2在T3 时刻尝试读取库存,但是此时这个库存己经被事务1事先读取,所以这 个时候数据库就阻塞它的读取, 直至事务1提交,事务2才能读取库存的值 此时己经 是T5 时刻, 而读取到的值为0,这时就已经无法扣减了,显然在读写提交中出现的不可重复读的场景被消除了,但是这样也会引发新的问题的出现,这就是幻读。假设现在商品交易正在进行中,而后台有人也在 进行查询分析和打印的业务,让我们看看幻读的场景。
| 时刻 | 事务1 | 事务2 | 备注 |
|---|---|---|---|
| T1 | 读取库存50件 | 商品库存初始化为100,现在已经销售50笔,库存50件 | |
| T2 | 查询交易记录,50笔 | ||
| T3 | 扣减库存 | ||
| T4 | 插入一笔交易记录 | ||
| T5 | 提交事务 | 库存49件,交易记录51笔 | |
| T6 | 打印交易记录,51笔 | 这里与查询的不一致,在事务2看来有一笔是幻读的,与之前查询的不一致。 |
这便是幻读现象。可重复读和幻读,是比较难以理解的内容,这里稍微论述一下。首先这里的笔数不是数据库存储的值,而是一个统计值,商品库存则是数据库存储的值,这一点是要注意的。也就是幻读不是针对一条数据库记录而言,而是多条记录,例如,这51 笔交易笔数就是多条数据库记录统计出来的,而可重复读是针对数据库的单一条记录,例如,商品的库存是以数据库里面的一条记录存储的,它可以产生可重复读,而不能产生幻读
4.串行化
串行化( Serializable)是数据库最高的隔离级别,它会要求所有的 SQL 都会按照顺序执行,这 样就可以克服上述隔离级别出现的各种问题,所以它能够完全保证数据的一致性。
-
使用合理的隔离级别
隔离级别和可能发生的现象
隔离级别 脏读 不可重复读 幻读 未提交读 √ √ √ 读写提交 × √ √ 可重复读 × × √ 串行化 × × × 在现实中一般而言,选择隔离级别会以读写提交为主,它能够防止脏读,而不能避免不可 重复读和幻读。为了克服数据不一致和性能问题,程序开发者还设计了乐观锁,甚至不再使用数据 库而使用其他的手段。例如,使用 Redis作为数据载体。对于隔离 级别,不同的数据库的支持也是不一样的。例如, Oracle 只能支持读写提交和串行化,而 MySQL则能够支持4种,对于 Oracle 默认的隔离级别为读写提交, MySQL 则是可重复读,这些需要根据具体 数据库来决定。
只要掌握了隔离级别的含义,使用隔离级别就很简单,只需要在@Transactiona 配置对应即可
@Transactional(isolation = Isolation.SERIALIZABLE)上面的代码中我们使用了序列化的隔离级别来保证数据的一致性,这使它将阻塞其他的事务进 行并发,所以它只能运用在那些低井发而又需要保证数据一致性的场景下。对于高井发下又要保证 数据一致性的场景,则需要另行处理了。 当然,有时候一个个地指定隔离级别会很不方便,因此 Spring Boot 可以通过配置文件指定默认的隔离级别。例如,当我们需要把隔离级别设置为读写提交时,可以在 application.properties 文件加 入默认的配置。
#隔离级别数字配置的含义 #-1 数据库默认隔离级别 #1 未提交读 #2 读写提交 #4 可重复读 #8 串行化 #tomcat数据源默认隔离级别 spring.datasource.tomcat.default-transaction-isolation=2 #dbcp2数据库连接池默认隔离级别 #spring.datasource.dbcp2.default-transaction-isolation=2代码中配置了 tomcat 数据源的默认隔离级别,而注释的代码则是配置了 DBCP2 数据源的隔离 级别,注释中己经说明了数字所代表的隔离级别,相信读者也有了比较清晰的认识,这里配置为 2, 说明将数据库的隔离级别默认为读写提交。
传播行为
package org.springframework.transaction.annotation;
public enum Propagation {
/**
* 需要事务,它是默认传播行为,如果当前存在事务,就沿用当前事务,
* 否则新建一个事务运行子方法
*/
REQUIRED(0),
/**
* 支持事务,如果当前存在事务,就沿用当前事务,
* 如果不存在,则继续采用无事务的方式运行子方法
*/
SUPPORTS(1),
/**
* 必须使用事务,如果当前没有事务,则会抛异常,
* 如果存在当前事务,就沿用当前事务
*/
MANDATORY(2),
/**
* 无论当前事务是否存在,都会创建新的事务运行方法,
* 这样新事务就可以拥有新的锁和隔离级别等特性,与当前事务相互独立
*/
REQUIRES_NEW(3),
/**
* 不支持事务,当前存在事务时,将挂起事务,运行方法
*/
NOT_SUPPORTED(4),
/**
* 不支持事务,如果当前方法存在事务,则抛出异常,否则继续使用无事务机制运行
*/
NEVER(5),
/**
* 在当前方法调用子方法时,如果子方法发生异常,
* 只回滚子方法执行过的SQL,而不回滚当前方法的事务
*/
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}