Spring事务隔离级别详解

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: Spring事务通过Transactional.isolation属性进行定义,其具体值则存储在Isolation枚举中。Spring对事务隔离级别的定义与数据库隔离级别的定义是完全一致的,因而本文主要从数据库的层面对事务进行讲解。

事务定义的是一系列数据库操作的序列,这个序列是一个不可分割的逻辑单元,在其中的操作要么全部完成,要么全部无法完成。Spring事务通过Transactional.isolation属性进行定义,其具体值则存储在Isolation枚举中。Spring对事务隔离级别的定义与数据库隔离级别的定义是完全一致的,因而本文主要从数据库的层面对事务进行讲解。

1. 事务

在事务的定义上,其主要有四大特性:原子性、一致性、隔离性和持久性,简称为ACID。这四大特性的含义如下:

  • 原子性:在一个事务中的操作都是一个逻辑的单元,在执行事务序列时,这些操作要么全部成功,要么全部失败;
  • 一致性:对于事务操作,其始终能够保证在事务操作成功或者失败回滚时能够达到一种一致的状态,简而言之,事务中的操作要么全部成功,要么全部失败;
  • 隔离性:各个事务之间的执行是相互不可见的,在事务还未执行成功时,其他的事务只能看到当前事务开始执行的状态,只有在当前事务执行完成之后才能看到执行之后的状态;
  • 持久性:事务的持久性指的是事务一旦执行成功,那么其所做的修改将永久的保存在数据库中,此时即使数据库崩溃,修改的数据也不会丢失。

关于事务的持久性需要说明的是,从事务的角度能够保证数据能够一致性的保存在磁盘上,即使数据库发生故障也能够从故障中恢复,但是如果是数据库之外的问题,比如RAID卡损坏,自然灾害等,这种问题在数据库层面是无法避免的,其也不属于事务的范畴。事务能够保证数据的高可靠性,但是事务并不能保证系统的高可用行。

事务能够始终保证数据保持在一种一致的状态,但是如果严格按照事务的定义来处理事务,那么事务的执行效率将会很低,因为只有保证了所有事务的串行执行才能保证事务,因而在事务规范中为事务定义了四种隔离级别:Read uncommitted、Read committed、Repeatable read和Serializable。关于这四种隔离级别,其主要区别在于三个点:脏读、不可重复读和幻读。这三个点的主要含义如下:

  • 脏读:脏读表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录A,此时该事务还未提交,然后另一个事务尝试读取记录A,这时其是会成功读取到记录A的;
  • 不可重复读:不可重复读表示当前事务对同一记录的两次重复读取结果不一致。比如一个事务首先读取一条记录A,读完之后另一个事务将该记录修改并且成功提交了,然后当前事务再次读取记录A,此时该事务会发现两次读取的结果不一致;
  • 读:幻读指的是一个事务在进行一次查询之后发现某个记录不存在,然后会根据这个结果进行下一步操作,此时如果另一个事务成功插入了该记录,那么对于第一个事务而言,其进行下一步操作(比如插入该记录)的时候很可能会报错。从事务使用的角度来看,在检查一条记录不存在之后,其进行插入应该完全没问题的,但是这里却抛出主键冲突的异常。

关于事务的四种隔离级别,其主要区别点也就在于是否能够解决这三个问题。这四种事务的隔离级别主要区别如下:

  • Read uncommitted:这是隔离性最低的一种隔离级别,在这种隔离级别下,当前事务能够读取到其他事务已经更改但还未提交的记录,也就是脏读;
  • Read committed:顾名思义,这种隔离级别只能读取到其他事务已经提交的数据,也就解决了脏读的问题,但是其无法解决不可重复读和幻读的问题;
  • Repeatable read:从事务的定义上,这种隔离级别能够解决脏读和不可重复读的问题,但是其无法解决幻读的问题;
  • Serializable:也称为序列化读,这是隔离性最高的一种隔离级别,所有的事务执行(包括查询)都会为所处理的数据加锁,操作同一数据的事务将会串行的等待。

从事务隔离级别的定义上可以看出,Serializable级别隔离性最高,但是其效率也最低,因为其要求所有操作相同记录的事务都串行的执行。这里需要说明的是,对于MySql而言,其默认事务级别是Repeatable read,虽然在定义上讲,这种隔离级别无法解决幻读的问题,但是MySql使用了一种Next key-lock的算法来实现Repeatable read,这种算法是能够解决幻读问题的。关于Next key-lock算法,在进行查询时,其不仅会将当前的操作记录锁住,也会将查询所涉及到的范围锁住。也就是说,其他事务如果想要在当前事务查询的范围内进行数据操作,那么其是会被阻塞的,因而MySql在Repeatable read隔离级别下就已经具备了Serializable隔离级别的事务隔离性。

2. 示例演示

关于四种事务隔离级别的演示,我们主要使用MySql客户端进行。这里首先需要说明的几个命令是关于事务的几个基本操作命令:

-- 设置当前会话的事务隔离级别,需要严格注意区分命令中的大小写,这里四种隔离级别分别是:Read uncommitted,Read committed,Repeatable read,Serializable
SET session TRANSACTION ISOLATION LEVEL Read uncommitted;
-- 查看当前会话的事务隔离级别
show variables like 'transaction_isolation';
-- 开始一个事务
start transaction;
-- 回滚当前事务
rollback;
-- 提交当前事务
commit;

首先我们建立如下的数据库表结构:

create table user(
  id bigint auto_increment comment '主键',
  name varchar(20) not null default '' comment '名称',
  age int(3) not null default 0 comment '年龄',
  primary key(id)
);

关于下面的演示过程,这里都省略了事务隔离级别切换的命令,读者可以自行进行切换。

1、Read uncommitted

首先我们开启两个Mysql命令行,并且设置事务隔离级别为Read uncommitted。对于Read uncommitted,理论上在一个会话中开启事务之后,另一个会话插入一条未提交的数据,当前会话是可以读取到这条记录的。这里我们首先在会话A中执行如下命令:

-- 会话A
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

然后在会话B中开启事务,并插入一条记录:

-- 会话B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user(id, name, age) value (1, 'Mary', 23);
Query OK, 1 row affected (0.00 sec)

此时再在会话A中尝试读取该记录:

-- 会话A
mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   23 |
+----+------+------+
1 row in set (0.00 sec)

可以看到,在A和B两个会话事务都未提交的情况下,会话A中的事务是能够成功读取到会话B中事务未提交的数据的,这也就产生了脏读的问题。

2、Read committed

关于Read committed,其表示当前事务只能读取其他事务已经提交的数据,但是无法解决不可重复读的问题。

  • 读取已提交的数据

这里的演示方式与脏读类似,首先在会话A中开启事务,然后在会话B中也开启事务,并且插入一条数据,此时在会话A中尝试读取该记录,应该是无法读取到结果的,如果在会话B中提交该事务之后,会话A中则应该可以读取到这条记录。

-- 会话A
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

然后在会话B中开启事务并插入一条记录:

-- 会话B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user(id, name, age) value (1, 'Mary', 23);
Query OK, 1 row affected (0.01 sec)

此时在会话A中读取该记录应该是无法读取到的:

-- 会话A
mysql> select * from user where id=1;
Empty set (0.00 sec)

可以看到,这里会话A中的事务是无法读取到会话B中事务插入的还未提交的数据的。此时我们提交会话B中的事务,并且再次在会话A中尝试读取该记录:

-- 会话B
mysql> commit;
Query OK, 0 rows affected (0.06 sec)
-- 会话A
mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   23 |
+----+------+------+
1 row in set (0.00 sec)

可以看到,在会话B提交了事务之后,会话A是能够获取到会话B进行的修改的。

  • 不可重复读

关于不可重复读,理论上,一个事务中,在对同一条记录的多次重复读取,得到的结果应该是始终一致的。这里Read committed隔离级别是没有这个特性的,因而如果我们在会话A中读取一条记录,然后在会话B中修改该记录并且提交,接着在会话A中再次进行读取,那么此时会话A中读取到的应该是修改之后的值。首先我们在会话A中开启事务,并且读取一条记录:

-- 会话A
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   23 |
+----+------+------+
1 row in set (0.00 sec)

然后在会话B中开启事务,修改一条记录,并且提交:

-- 会话B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update user set age=25 where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.04 sec)

接着我们在会话A中再次读取该记录:

-- 会话A
mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   25 |
+----+------+------+
1 row in set (0.00 sec)

可以看到,会话A在事务还未提交的情况下,其重复读取同一条记录,两次读取的结果居然不一致,这也就是不可重复读。

3、Repeatable read

关于Repeatable read,在定义上,其解决了不可重复读的问题,但是没解决幻读的问题,这里由于MySql使用了Next key-lock算法,因而在这个隔离级别下,其也解决了幻读的问题。这里我们会对着两种情况依次进行演示。

  • 可重复读

这里可重复读的演示与上面不可重复读的演示方式是一样的,只是这里将隔离级别设置为Repeatable read。

-- 会话A
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   25 |
+----+------+------+
1 row in set (0.00 sec)

然后在会话B中开启事务,修改该记录,并且提交:

-- 会话B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update user set age=30 where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.05 sec)

这里会话B在事务中修改了id为1的记录的值,此时我们再次在会话A中读取该记录:

-- 会话A
mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   25 |
+----+------+------+
1 row in set (0.00 sec)

可以看到,在会话A还未提交的时候,其读取的结果始终是一致的,并未受到会话B中已经提交的事务的影响。

  • 幻读

关于幻读,最典型的示例就是在一个事务中进行数据插入时,MySQL首先会先检查该数据是否存在,如果不存在则插入数据,这个过程中,如果另一个事务也插入了同样的数据,那么这个事务是会被阻塞的,如果当前事务提交了,那么另一个事务就会抛出主键冲突的异常。

-- 会话A
mysql> select * from user where id=2 for update;
Empty set (0.01 sec)

mysql> insert into user (id, name, age) value (2, 'Jack', 24);
Query OK, 1 row affected (0.00 sec)

此时在会话B中开启事务,并且尝试插入同一条数据,那么其是会被阻塞的:

-- 会话B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user (id, name, age) value (2, 'Bob', 28);

可以看到,这里会话B中的插入操作是被阻塞了的。如果此时将会话A中的事务提交,那么会话B中将会抛出异常:

-- 会话B
mysql> insert into user (id, name, age) value (2, 'Bob', 28);
ERROR 1062 (23000): Duplicate entry '2' for key 'PRIMARY'

这里关于幻读需要说明的一点是,MySql在Repeatable read级别解决的幻读问题是在MySQL级别处理的,比如上面的示例中,我们在会话A中查询时加上了for update,该命令会针对目标记录加锁,如果目标记录不存在,则会加上Gap锁,这样后面在同一事物中插入是可以成功的。如果在同一事物中只是单纯的查询,然后进行插入,那么还是会出现幻读的问题的。也就是说上面的示例中,如果将for update去掉,那么其还是会出现幻读的问题的。

4、Serializable

关于序列化读,这里就比较简单。对于一个事务而言,其所有的操作都会锁定所操作的数据和Gap,此时另外的事务只能等待该事务完成才能进行下一步操作。这里我们以两个事务同时查询同一事务中的同一记录为例进行展示:

-- 会话A
mysql> select * from user where id=2;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | Jack |   24 |
+----+------+------+
1 row in set (0.00 sec)

这里会话A是可以正常读取记录的,此时我们在会话B中尝试使用加锁的方式读取同一记录:

-- 会话B
mysql> select * from user where id=2 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

可以看到,会话B中的事务尝试加锁是失败的,因为目标记录在会话A中已经被锁定了。

3. 小结

本文首先对事务的四个特性进行了讲解,然后讲解了事务存在的三个问题,接着讲解了事务定义的四种隔离级别是如何解决这三个问题的,最后通过示例讲解了这四个隔离级别的区别。

文章来源:https://my.oschina.net/zhangxufeng/blog/1942110

推荐阅读:https://www.roncoo.com/course/view/7ae3d7eddc4742f78b0548aa8bd9ccdb

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
4月前
|
Java 关系型数据库 MySQL
Spring 事务失效场景总结
Spring 事务失效场景总结
65 4
|
16天前
|
Java 开发者 Spring
Spring高手之路24——事务类型及传播行为实战指南
本篇文章深入探讨了Spring中的事务管理,特别是事务传播行为(如REQUIRES_NEW和NESTED)的应用与区别。通过详实的示例和优化的时序图,全面解析如何在实际项目中使用这些高级事务控制技巧,以提升开发者的Spring事务管理能力。
31 1
Spring高手之路24——事务类型及传播行为实战指南
|
9天前
|
XML Java 数据库连接
Spring中的事务是如何实现的
Spring中的事务管理机制通过一系列强大的功能和灵活的配置选项,为开发者提供了高效且可靠的事务处理手段。无论是通过注解还是AOP配置,Spring都能轻松实现复杂的事务管理需求。掌握这些工具和最佳实践,能
17 3
|
2月前
|
Java 数据库连接 数据库
spring复习05,spring整合mybatis,声明式事务
这篇文章详细介绍了如何在Spring框架中整合MyBatis以及如何配置声明式事务。主要内容包括:在Maven项目中添加依赖、创建实体类和Mapper接口、配置MyBatis核心配置文件和映射文件、配置数据源、创建sqlSessionFactory和sqlSessionTemplate、实现Mapper接口、配置声明式事务以及测试使用。此外,还解释了声明式事务的传播行为、隔离级别、只读提示和事务超时期间等概念。
spring复习05,spring整合mybatis,声明式事务
|
2月前
|
Java 测试技术 数据库
Spring事务传播机制(最全示例)
在使用Spring框架进行开发时,`service`层的方法通常带有事务。本文详细探讨了Spring事务在多个方法间的传播机制,主要包括7种传播类型:`REQUIRED`、`SUPPORTS`、`MANDATORY`、`REQUIRES_NEW`、`NOT_SUPPORTED`、`NEVER` 和 `NESTED`。通过示例代码和数据库插入测试,逐一展示了每种类型的运作方式。例如,`REQUIRED`表示如果当前存在事务则加入该事务,否则创建新事务;`SUPPORTS`表示如果当前存在事务则加入,否则以非事务方式执行;`MANDATORY`表示必须在现有事务中运行,否则抛出异常;
143 4
Spring事务传播机制(最全示例)
|
1月前
|
Java 关系型数据库 MySQL
Spring事务失效,我总结了这7个主要原因
本文详细探讨了Spring事务在日常开发中常见的七个失效原因,包括数据库不支持事务、类不受Spring管理、事务方法非public、异常被捕获、`rollbackFor`属性配置错误、方法内部调用事务方法及事务传播属性使用不当。通过具体示例和源码分析,帮助开发者更好地理解和应用Spring事务机制,避免线上事故。适合所有使用Spring进行业务开发的工程师参考。
32 2
|
1月前
|
Java 程序员 Spring
Spring事务的1道面试题
每次聊起Spring事务,好像很熟悉,又好像很陌生。本篇通过一道面试题和一些实践,来拆解几个Spring事务的常见坑点。
Spring事务的1道面试题
|
2月前
|
Java Spring
Spring 事务传播机制是什么?
Spring 事务传播机制是什么?
23 4
|
1月前
|
监控 Java 数据库
Spring事务中的@Transactional注解剖析
通过上述分析,可以看到 `@Transactional`注解在Spring框架中扮演着关键角色,它简化了事务管理的复杂度,让开发者能够更加专注于业务逻辑本身。合理运用并理解其背后的机制,对于构建稳定、高效的Java企业应用至关重要。
57 0