一篇文章搞定系列:事务的隔离级别和MVCC多版本控制

本文详细介绍了事务的传播属性、隔离级别及其影响,如脏读、不可重复读和幻读,并通过实例解析了MySQL的MVCC机制如何在可重复读隔离级别下保证数据一致性。同时,讨论了更新和删除操作在MVCC中的处理方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文事务传播行为参考:java - Spring事务传播行为详解_个人文章 - SegmentFault 思否

本文事务隔离级别参考:MySQL的四种事务隔离级别 - 花弄影 - 博客园

一篇文章搞定系列有:

java深拷贝和浅拷贝

java深拷贝和浅拷贝_zhangxiaomin19921的博客-优快云博客

1、事务传播属性

PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务。如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

PROPAGATION_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER--以非事务方式执行,如果当前存在事务,则抛出异常。

PROPAGATION_NESTED—如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,就新建事务

2、关于NESTED、REQUIRED、REQUIRES_NEW的区别

外围事务是REQUIRED的情况下,内部方法也是Nested会加入外围事务作为子事务,内部方法抛出异常也会影响外部事务,从而外部事务,required事务和子事务是不插入的

外围事务是REQUIRED的情况下,内部方法也是Nested会加入外围事务作为子事务,内部方法抛出异常,被cache住了不会影响外部事务,从而外部事务,required事务和子事务是可以插入的

外围事务是REQUIRED的情况下,内部方法也是REQUIRES_NEW不会加入外围事务,内部方法抛出异常,无论是否被cache住都不会影响外部事务,从而REQUIRED是插入的

外围事务是REQUIRED的情况下,内部方法也是REQUIRED会加入外围事务,内部方法抛出异常即使被cache住了也会被外围事务感知,从而这2REQUIRED都不会插入

3、例子

ok表示方法无错误,no表示方法内部抛异常,NESTED、REQUIRED、REQUIRES_NEW表示外部事务或者内部方法的传播行为,结果表示内部方法1,2,3的数据是否可插入,ok表示可插入,no表示不可插入

外围事务是REQUIRED的情况下,内部方法也是Nested会加入外围事务作为子事务,内部方法抛出异常也会影响外部事务,从而外部事务,required事务和子事务是不插入的

外围事务是REQUIRED的情况下,内部方法也是Nested会加入外围事务作为子事务,内部方法抛出异常,被cache住了不会影响外部事务,从而外部事务,required事务和子事务是可以插入的

外围事务是REQUIRED的情况下,内部方法也是REQUIRES_NEW不会加入外围事务,内部方法抛出异常,无论是否被cache住都不会影响外部事务,从而REQUIRED是插入的

外围事务是REQUIRED的情况下,内部方法也是REQUIRED会加入外围事务,内部方法抛出异常即使被cache住了也会被外围事务感知,从而这2REQUIRED都不会插入

外围事务

方法内事务1

方法内事务2

方法内事务3

步骤4

结果

REQUIRED,ok

REQUIRED,ok

throw RuntimeException

okok

REQUIRED,ok

REQUIRED,no

okno

有,REQUIRED

REQUIRED,ok

REQUIRED,ok

throw RuntimeException

nono

REQUIRED,ok

REQUIRED,no

nono

REQUIRED,ok

REQUIRED,ok,cache住了

nono

REQUIRES_NEW,ok

REQUIRES_NEW,ok

throw RuntimeException

Ok, ok

REQUIRES_NEW,ok

REQUIRES_NEW,no

Ok,no

REQUIRED

REQUIRED,ok

REQUIRES_NEW,ok

REQUIRES_NEW,ok

throw RuntimeException

No,ok,ok

REQUIRED,ok

REQUIRES_NEW,ok

REQUIRES_NEW,no

Ok, ok,no

REQUIRED,ok

REQUIRES_NEW,ok

REQUIRES_NEW,no,异常被捕获

okokno

Nested,ok

Nested,ok

throw RuntimeException

okok

Nested,ok

Nested,no

okno

Nested,ok

Nested,ok

throw RuntimeException

nono

Nested,ok

Nested,no

nono

Nested,ok

Nested,no,异常被捕获

okno

4、事务隔离级别

读未提交,会引起脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

读已提交,会引起不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致

可重复读,会引起幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

串行化:读写数据都会锁住整张表

可以使用 select @@tx_isolation 查看默认事务隔离级别

mysql默认的事务处理级别是可重复读,oracle默认事务隔离级别是读已提交,sqlserver默认事务隔离级别是读已提交

5、事务隔离级别例子:

CREATE TABLE
    account
    (
        id INT,
        name VARCHAR(64),
        balance INT
    )
    ENGINE=InnoDB DEFAULT CHARSET=utf8 DEFAULT COLLATE=utf8_general_ci COMMENT='account';

5.1、

1、客户端A打开mysql连接,设置当前会话事务隔离级别是读未提交,查看数据,客户端B打开mysql连接,设置当前会话事务隔离级别是读未提交,更新一条记录,客户端A再次查看数据,发现读取到了客户端B修改了未提交的数据。

客户端A:

mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.02 sec)
 
mysql> start transaction;
Query OK, 0 rows affected (0.02 sec)
 
mysql> use data;
Database changed
mysql> select * from account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     100 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.03 sec)
 
mysql> 
mysql> select * from account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     800 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.03 sec)
 
mysql> 

客户端B:

mysql> set session transaction isolation level read uncommitted;

Query OK, 0 rows affected (0.02 sec)
 
mysql>  start transaction;
Query OK, 0 rows affected (0.01 sec)
 
mysql>  use data;

Database changed
mysql> UPDATE account set balance=800 where id=1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> rollback;
Query OK, 0 rows affected (0.03 sec)
 
mysql> 

客户端A查到的数据800就是脏数据,当然,客户端B在回滚后,客户端A可查到正确数据100。

5.2、读已提交

客户端A打开事物,查询表数据,客户端B打开事务,更新一条数据800,这时候客户端B还没有提交,

客户端A再次查询表数据,发现数据没变,解决了脏读的问题。

客户端A:

mysql> set session transaction isolation level read committed;

Query OK, 0 rows affected (0.02 sec)
 
mysql> start transaction;
Query OK, 0 rows affected (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     100 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     100 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> 

客户端B:

mysql> set session transaction isolation level read committed;

Query OK, 0 rows affected (0.02 sec)
 
mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
 
mysql> UPDATE account set balance=800 where id=1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> 

这时候客户端B提交事务,客户端A再次读表数据,发现查询结果和上次查询结果不一致,出现了不可重复读问题。

客户端A:

mysql> set session transaction isolation level read committed;

Query OK, 0 rows affected (0.02 sec)
 
mysql> start transaction;
Query OK, 0 rows affected (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     100 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     100 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     800 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> 

客户端B:

mysql> set session transaction isolation level read committed;

Query OK, 0 rows affected (0.02 sec)
 
mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
 
mysql> UPDATE account set balance=800 where id=1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> commit;
Query OK, 0 rows affected (0.02 sec)
 
mysql> 

5.3 幻读

客户端A打开连接,首次查询表数据,客户端B打开事务,更新数据,客户端A再次查询,数据一致,没有出现脏读

客户端B提交事务,客户端A再次查询数据,3次数据都一致,没有出现不可重复读。

客户端A:

mysql> set session transaction isolation level repeatable read;

Query OK, 0 rows affected (0.02 sec)
 
mysql>  start transaction;

Query OK, 0 rows affected (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     800 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     800 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     800 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> 

客户端B

mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.02 sec)
 
mysql> start transaction;
Query OK, 0 rows affected (0.02 sec)
 
mysql> UPDATE account set balance=888 where id=1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> commit;
Query OK, 0 rows affected (0.03 sec)
 
mysql> 

然后再次打开一个事务连接C,然后插入一条数据600,在客户端C查询表数据,有600。在客户端A第四次查询数据,发现没有600这条数据。即使客户端C提交事务后,客户端A还是没有这条600。出现了幻读。

mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.02 sec)
 
mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
 
mysql> INSERT into account VALUE ('4','huandu',600);
Query OK, 1 row affected (0.02 sec)
 
mysql>  SELECT * from data.account;

+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     888 |
|  2 | haixing  |     200 |
|  4 | huandu   |     600 |
+----+----------+---------+
4 rows in set (0.02 sec)
 
mysql> commit;
Query OK, 0 rows affected (0.02 sec)
 
mysql> 

客户端的5次查询,数据全部一致,那么如果客户端A做一次更新操作(UPDATE account set balance=balance+1 where id=1;)呢?发现,客户端A虽然显示的数据从始至终都是800,但是做一个更新操作后,数据是889,和数据库真实数据888+1的结果一致,没有出现数据不一致现象。

客户端A:

mysql> set session transaction isolation level repeatable read;

Query OK, 0 rows affected (0.02 sec)
 
mysql>  start transaction;

Query OK, 0 rows affected (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     800 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     800 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> SELECT * from data.account;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     800 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql>  SELECT * from data.account;

+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     800 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql>  SELECT * from data.account;

+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     800 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> UPDATE account set balance=balance+1 where id=1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql>  SELECT * from data.account;

+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  3 | shaofei  |     900 |
|  1 | zhangmin |     889 |
|  2 | haixing  |     200 |
+----+----------+---------+
3 rows in set (0.02 sec)
 
mysql> 

5.4、可重复读的mvcc机制

可重复读保证多次查询数据一致的机制是。Mvcc机制是怎么保证在可重复读的隔离级别下,select的时候不会出现不可重复读的,又是怎么出现insert插入数据后他不知道的?

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,一个保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID)。select操作不会更新事务id,更新操作会更新事务id。

然后要知道mvcc查询和更新的原则:SELECT操作的查询结果要同时满足条件:

1、只会查询版本号小于等于当前版本号的数据作为结果返回,保证了这个数据要么是当前事务修改过的,要么是事务开始之前就已经存在的。

2、行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除

还有mvcc更新事务的机制:

update的更新原则:会生成新的一行,并在原来要修改的列的删除时间列上添加本事务ID。

下面通过几个例子开始解释以上现象:

5.4.1、首先为什么会导致插入数据后他不知道呢?

假设有3个事务,id=1的事务插入3条数据:

start transaction;

insert into yang values(NULL,'yang') ;

insert into yang values(NULL,'long');

insert into yang values(NULL,'fei');

commit;

id=2的事务查询2次表数据:

start transaction;

select * from yang; //(1)

select * from yang; //(2)

commit;

id=3的事务插入一条数据

start transaction;

insert into yang values(NULL,'tian');

commit;

假设在执行这个事务ID为2的过程中,刚执行到(1),这时,有另一个事务ID为3往这个表里插入了一条数据事务,这时候数据库初始数据是:

然后接着执行事务2中的(2),由于id=4的数据的创建时间(事务ID为3),执行当前事务的ID为2,而InnoDB只会查找事务ID小于等于当前事务ID的数据行,所以id=4的数据行并不会在执行事务2中的(2)被检索出来。也就是为什么新增了数据我却不知道了?

5.3.2、删除机制 delete :

假设在执行这个事务ID为2的过程中,刚执行到(1),假设事务执行完事务3插入一条数据我不知道后,接着又执行了事务4; 
第四个事务:

start transaction;

delete from yang where id=1;

commit;

执行事务4之后的数据库更新了删除事务id;数据库状态为:

接着执行事务ID为2的事务(2),根据SELECT 检索条件可以知道,它会检索创建时间(创建事务的ID)小于当前事务ID的行和删除时间(删除事务的ID)大于当前事务的行。因此1,2,3都会进入select搜索结果。

5.4.5、 更新机制

假设在执行完事务2的(1)后又执行,其它用户执行了事务3,4,这时,又有一个事务5对这张表执行了UPDATE操作: 

start transaction;

update yang set name='Long' where id=2;

commit;

update的更新原则:会生成新的一行,并在原来要修改的列的删除时间列上添加本事务ID,初始状态上图,更新后数据库状态如下:

继续执行事务2的(2),根据select 语句的检索条件,创建时间是5的最新数据不会捞取,捞取的是创建时间为1的就数据,保证了可重复读。得到下表:

假设事务2的(2)执行完之后,未提交事务前,又执行了一个更新操作,会再次改变版本号,那么数据又回又什么变化呢?

假设1:假设更新的是之前没有更新过的id=3的数据:

update yang set name='newfei' where id=3;

这时候数据库状态为:

根据select的查询规则,会得到 yang long newfei。

假设2:假设1执行之后又执行了一个更新操作,更新的是已经被更新的数据id=2

update yang set name='newlong' where id=2;

之后的数据库状态是:

根据select规则,查处的数据是yang newfei newlong。

结束!有错误的欢迎评论或加qq 241317271指教。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值