欢迎光临BDM
一枚菜鸟码农的成仙之路

Spring 5 事务传播

什么是数据库事务

正常的数据库交互中,数据库执行我们提供的 SQL,然后立即返回 SQL 执行的结果。而数据库事务可以将一系列 SQL 打包成一个整体帮我们执行,要么全部成功,要么一句失败则先前成功的 SQL 执行结果也会被撤销(回滚)。

数据库事务有很多应用场景,因为正常的业务需求中,很少有一句 SQL 便能实现的功能,更多的是:

  1. 准备看场电影:先查有没有票,然后再买下这个票,最后启程出发前往电影院。如果买票失败,则放弃或者返回上一步再查询其他场次,而不会继续执行之后的“前往电影院”。
  2. 准备做个饭。首先前往菜场买菜,买莴笋、辣椒、大肉、大米,结果大肉没有了,唉,大米也不用看了,这场饭肯定做不下去,回家。

数据库事务由数据库引擎提供支持,其他语言的库只是对接了相应的接口。数据库事务拥有四大特点:原子性一致性隔离性持久性

MySQL 原生 SQL 事务控制

这里以 MySQL 为例简要地提到原生事务控制的方法。通常,常用的三个语句如下:

BEGIN -- 开始一个事务

ROLLBACK -- 事务回滚

COMMIT -- 事务确认

常见的用法如下,这里借用的是菜鸟教程的例子,大概看一下执行的顺序就行了:

mysql> use RUNOOB;
Database changed
mysql> CREATE TABLE runoob_transaction_test( id int(5)) engine=innodb;  # 创建数据表
Query OK, 0 rows affected (0.04 sec)

mysql> select * from runoob_transaction_test;
Empty set (0.01 sec)

mysql> begin;  # 开始事务
Query OK, 0 rows affected (0.00 sec)

mysql> insert into runoob_transaction_test value(5);
Query OK, 1 rows affected (0.01 sec)

mysql> insert into runoob_transaction_test value(6);
Query OK, 1 rows affected (0.00 sec)

mysql> commit; # 提交事务
Query OK, 0 rows affected (0.01 sec)

mysql>  select * from runoob_transaction_test;
+------+
| id   |
+------+
| 5    |
| 6    |
+------+
2 rows in set (0.01 sec)

mysql> begin;    # 开始事务
Query OK, 0 rows affected (0.00 sec)

mysql>  insert into runoob_transaction_test values(7);
Query OK, 1 rows affected (0.00 sec)

mysql> rollback;   # 回滚
Query OK, 0 rows affected (0.00 sec)

mysql>   select * from runoob_transaction_test;   # 因为回滚所以数据没有插入
+------+
| id   |
+------+
| 5    |
| 6    |
+------+
2 rows in set (0.01 sec)

可以直观地看到,输入了事务开始语句BEGIN后,事务便已经开始,其后地所有 SQL 语句都处于事务控制中。

在执行完所有的 SQL 后,直到执行了ROLLBACKCOMMIT后,事务才被处理,要么是全部撤回,要么全部提交。这称之为数据库事务的原子性

这里修正一下上文(第一段)的说法:全部失败不是自动完成的。一定得执行 ROLLBACK 才能回滚,若执行 COMMIT,则执行成功的语句仍然会被提交,失败的语句忽略不计。日常开发印象中的自动回滚,均由编程语言的库来支持实现。

什么是事务隔离

Java 开发中,事务隔离有两种语义:

  1. Spring 事务隔离
  2. 数据库事务隔离

虽然本文着重讲 Spring 事务隔离机制,但仍然会提到数据库事务隔离的内容。

什么是数据库事务隔离

数据库事务隔离由数据库引擎提供支持,这称之为数据库事务的隔离性。那么为什么要隔离呢?在详细地解释隔离之前,我们先简要说明不隔离会造成的问题。通常在多线程的情况下,有如下几种问题:

1. 脏读(Dirty Read)

事务中未被提交的数据被另一个事务读取到。

比如说,我超市买着菜呢,老婆打了一个电话过来,一听我准备买莴笋、辣椒、大肉,然后就在家锣鼓喧天地准备了起来。结果一到结账的时候,超市的收银系统出现了故障,无法结账。于是只能把菜放回去,悻悻地回了家。

这个事件中,老婆这个事务就错误地读到了我未提交的事务,然后继续进行了她的逻辑,然后我的事务出现问题发生回滚,这便导致了整体业务的异常。这便是脏读

2. 不可重复读(NonRepeatable Read)

同一事务中,因为另一事务修改和删除,多次读取同一数据获得不一样的结果。

这里以看电影为例子。我查了一下 APP,发现绝佳位置有两张空座,赶紧打电话给老婆,问一下要不要一起看,老婆说好呀好呀,然后我返回 APP 去购买,结果好巧不巧,已经被别人购买了。

这个事件中,我前一次和后一次看到了不一样的座位信息,这便是不可重复读

实际买票过程中,我们会经历先下单再付款的流程,这里的下单就类似于数据库中的锁,我锁住了这条数据,那么在我的事务进行中,这条就不会再被其他事务影响,从而导致“我买票看电影”这件事务的异常。

3. 幻读(Phantom Read)

同一事务中,因为另一事务新增数据,多次读取同一数据获得不一样的结果。

幻读虽然与不可重复读有类似的结果,但核心区别在于由新增导致,通常是对数据集的读取造成。幻读和不可重复读都会导致数据读取的混乱,使人产生迷惑。

这里仍然举一个生活中的小例子:双十一准备帮老婆清购物车,零点一到,问老婆选好了吗,老婆说还没呢,过了十分钟再问,老婆说再等等嘛。。。又过了半小时,老婆说烦死了等一下怎么了。

这个事件中,更直观地说明了迷惑性。老婆的购物车处于持续新增的状态,但你的目标是对整个购物车进行处理。于是你只能不停地读,因为你的事务害怕每一次读取的结果可能是幻读的结果,倘若错误地提前处理,可能造成不可磨灭的危害。

事务隔离

数据库设定了事务隔离的概念,来避免上述问题的发生。

以 MySQL 的 InnoDB 引擎为例,共有四种隔离级别:

隔离级别 脏读 不可重复读 幻读
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行读(Serializable) 不可能 不可能 不可能

事务隔离级别可以进行全局设置,也可以进行单条语句的设置。

事务隔离级别通常是以不同粒度的锁来控制,这里暂且不做引申。

什么是 Spring 事务隔离

Spring 事务隔离是 Java 开发中,不同 Service 不同方法间的事务传播机制,与数据库隔离完全不同。

这里不得不提到 Spring 的事务控制。Spring 的自动事务是以 Service 的方法为粒度,不同的方法采用不同的事务。

在没有事务传播的情况下,方法 A 调用方法 B,他们各自都执行自己的 SQL,调用完 B 之后方法 A 执行失败,那么 A 的事务进行回滚,不会影响 B 的事务。

实际业务需求中,大多数情况下都想把一系列方法调用归结到一个事务中,而部分情况下又需要将一些方法的事务独立开来。Spring 的事务控制就用于满足我们的这个需求,Java 开发者得以灵活地设置方法间事务传播,来实现各种各样的业务。

@Transactional 注解

下文基于 Spring 5

在需要事务控制的方法上,加上@Transactional注解即可进行事务控制。该注解亦可加在类上,代表对整个类进行设置。

org.springframework.transaction.annotation.@Transactional中可以对事务进行下列几项配置,这些配置均在org.springframework.transaction.TransactionDefinition 接口中进行定义:

1. 隔离级别

隔离级别默认为数据库隔离级别。由数据库引擎支持。

2. 事务传播机制

定义了事务传播的规则,默认为 Propagation.REQUIRED。由 Spring 支持。

3. 事务超时时间

定义了事务最长操作时间,超过该时间将自动超时回滚,默认为 -1。由 Spring 支持。

4. 事务是否只读

如果该事务只有对数据的读取组成,那么定义该事务为只读将提升性能,默认为否。由 Spring 支持。

5. 回滚触发条件

设定事务回滚的规则,可以设置触发异常的黑白名单,默认为捕获 RuntimeException 时回滚。由 Spring 支持。

事务传播的几种机制

  • 这些类型均为 TransactionDefinition 下的静态变量,枚举类 Propagation 亦提供支持。
事务传播类型 含义
PROPAGATION_REQUIRED 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
PROPAGATION_SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
PROPAGATION_MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
PROPAGATION_REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
PROPAGATION_NEVER 以非事务方式运行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则创建一个新的事务。

在注解中使用@Transactional(propagation=Propagation.REQUIRED)进行设置。

本文遵守知识共享署名-相同方式共享 4.0 国际许可协议,未经允许不得转载BigDickMan » Spring 5 事务传播

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

联系我们GitHub