数据库事务隔离级别及底层原理详解

本文详细记录了数据库中事务的隔离级别及其定义,以及每个隔离级别下可能会遇到哪些问题及对应的解决方案和原理,以下内容结合为各大平台的知识点加自己的理解进行的总结,希望大家在读完以后能对事务有个全新的认识~~

1. MySQL事务管理

自动提交

  • 在MySQL中,默认DML指令的执行时自动提交的,当我们执行一个DML指令之后,自动同

步到数据库中。

事务管理

开启事务,就是关闭自动提交

  • 在开始事务第一个操作之前,执行 start transaction 开启事务 。
  • 依次执行事务中的每个DML操作
  • 如果在执行的过程中的任何位置出现异常,则执行 rollback 回滚事务 。
  • 如果事务中所有的DML操作都执行成功,则在最后执行 commit 提交事务。

事务管理示例sql:

create database db_test3;

use db_test3;

# 库存表
create table stock(
  id int primary key auto_increment,
  name varchar(200),
  num int not null
)

# 订单表
create table order_db(
  id int primary key auto_increment,
  name varchar(200) not null,
  price double,
  num int
);

insert into stock(name,num) values('鼠标',10);
insert into stock(name,num) values('键盘',20);
insert into stock(name,num) values('耳机',30);

# 开启事务
start transaction;

# 操作1:扣减库存
update stock set num = num-1 where name = '鼠标';
select aaa; # 此处会执行失败

# 操作2:新增订单
insert into order_db(name,price,num) values('鼠标',20.5,1);

# 事务回滚:清除缓存中的操作,撤销当前事务已经执行的操作
rollback;

# 提交事务: 将缓存中的操作写入数据文件
commit;

2. 事务隔离级别

注:可重复读可以解决大部分的幻读现象,但仍有可能发生幻读(先执行快照读,后执行当前读)

数据库在执行DML语句(update、delete、insert)底层会默认执行一遍当前读

查看与设置隔离级别:

mysql默认的隔离级别:可重复读(REPEATABLE READ)。

  • 查看当前会话的隔离级别:select @@transaction_isolation;
  • 查看全局的隔离级别:select @@gobal.transaction_isolation;

设置事务隔离级别:

  • 会话级:set session transaction isolation level read committed(设置当前会话的隔离级别为读已提交);
  • 全局级:set global transaction isolation level read committed(设置全局会话的隔离级别为读已提交);

不同的现象

脏读(Dirty Read)事务读取了其他事务中未提交的数据,当其他事务将未提交的数据进行修改,导致另一个事务读取到的数据是不正确的,这种现象就是脏读,会导致数据的不一致性。

不可重复读(Non-Repeatable Read)指在一个事务内,不同时刻多次读取同一行的数据得到的结果不同。通常发生在一个事务内多次读取同一行数据,但在这些读取过程中,另一个事务修改了该行数据并且已经提交。因此,第一个事务两次读取同一行数据时得到的结果不一致,这种现象就是不可重复读。不可重复读可以导致事务逻辑错误或数据不一致的问题。

幻读(Phantom Read)指在同一个事务中,多次查询同一个范围的数据时,得到的结果集不一致。这是因为在查询期间,另一个事务插入或删除了符合查询条件的数据。通常发生在一个事务内查询某个范围的数据,但在这两次查询之间,另一个事务插入了新的符合查询条件的数据并且已经提交。因此,第一个事务两次查询同一范围的数据时得到的结果集不同,这种现象就是幻读。幻读可能会导致查询结果不一致或业务逻辑错误。


MySQL数据库事务隔离级别:

  • 读未提交
  • 读已提交 (Oracle数据库默认)
  • 可重复读 (MySQL默认级别)
  • 串行化

读未提交(read uncommitted)

T2可以读取T1执行但未提交的数据;可能会导致出现脏读、不可重复读和幻读。

//设置全局事务隔离级别为读未提交
set global transaction isolation level read uncommitted;

脏读,一个事务读取到了另一个事务中未提交的数据

演示:

T1事务

T2事务

mysql> use test_transaction

mysql> use test_transaction

mysql> start transaction;

mysql> start transaction;

mysql> select * from a;

mysql> insert into a values(4);

mysql> select * from a;

通过以上测试,可以看到,A事务读取到了B事务还没有提交的数据。这种现象就是脏读。


读已提交(read committed)

T2只能读取T1已经提交的数据;避免了脏读,但可能会导致不可重复度、幻读。

//设置全局事务隔离级别为读已提交
set global transaction isolation level read committed;

演示:

T1事务

T2事务

mysql> use test_transaction

mysql> use test_transaction

mysql> start transaction;

mysql> start transaction;

mysql> select * from a;

mysql> insert into a values(4);

mysql> select * from a;

mysql> commit;

mysql> select * from a;

通过以上测试看出,A事务只能读取到B事务提交之后的数据。这种隔离级别解决了脏读问题,但肯定是存在不可重复读和幻读问题。因为只要事务B进行了增删改操作之后并提交了,事务A读取到的数据肯定是不同的。即:不可重复读和幻读都存在。


可重复读(repeatable read)

T2执行第一次查询之后,在事务结束之前其他事务不能修改对应的数据,但可以操作其他数据;避免了不可重复读,解决了大部分幻读问题,但可能会存在少部分幻读现象。

//设置全局事务隔离级别为可重复读
set global transaction isolation level repeatable read;

演示:

A事务

B事务

mysql> use test_transaction

mysql> use test_transaction

mysql> start transaction;

mysql> start transaction;

mysql> select empno,ename,sal

from emp

where empno=7369;

mysql> update emp

set ename='SMITH',sal=8000

where empno=7369;

mysql> commit;

mysql> select empno,ename,sal

from emp

where empno=7369;

通过以上测试得知:当事务隔离级别设置为可重复读时,避免了不可重复读问题。

那么在MySQL当中,当事务隔离级别设置为可重复读时,能够避免幻读问题吗?测试一下:

事务A

事务B

mysql> use test_transaction

mysql> use test_transaction

mysql> start transaction;

mysql> start transaction;

mysql> select * from a;

mysql> insert into a values(5);

mysql> commit;

mysql> select * from a;

通过以上测试得知:当事务隔离级别设置为可重复读时,也避免了幻读问题。是完全避免了幻读问题吗?并不是。请看以下测试:

事务A

事务B

mysql> use test_transaction

mysql> use test_transaction

mysql> start transaction;

mysql> start transaction;

mysql> select * from a;

mysql> insert into a values(6);

mysql> commit;

mysql> select * from a for update;

通过以上测试得知:当事务隔离级别设置为可重复读,MySQL会尽最大努力避免幻读问题,但这种隔离级别无法完全避免幻读问题。


串行化(serializable)

同时只允许一个事务对数据表进行操作;避免了脏读、虚读、幻读问题,缺点是效率低,因为这种隔离级别会导致事务排队处理,不支持并发。

//设置全局事务隔离级别为串行化 
set global transaction isolation level serializable;

演示:

事务A

事务B

mysql> use test_transaction

mysql> use test_transaction

mysql> start transaction;

mysql> start transaction;

mysql> select * from a;

mysql> insert into a values(7);

mysql> select * from a;

mysql> commit;

通过以上测试得知:当事务隔离级别设置为串行化时,事务只能排队执行,不支持并发

3. 可重复读的幻读问题

        通过以上的分析,在MySQL中默认的事务隔离级别为可重复读完全避免了脏读和不可重复读,在很大程度上避免了幻读问题(并不能完全解决),那么可重复读这一隔离级别是如何解决幻读的呢?方案包括两种:

  • 针对快照读(普通的select语句,如:select * from test;),是通过MVCC方式解决了幻读。因为在可重复读隔离级别下,通过快照读查询到的数据跟事务刚启动时查询到的数据是一致的,即使中途进行了DML(insert、update、delete)操作,读出来的结果也是一致的,因此解决了幻读问题。
  • 针对当前读(select * from test for update;),是通过next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行select * from test for update操作时,会为当前查询到的数据加上next-key lock锁,如果有其他的事务想在加了next-key lock锁范围内的数据进行插入、更新或删除时,该语句会被阻塞,无法执行,等释放next-key lock锁后才能继续执行该操作,因此解决了幻读问题。

3.1. 快照读是如何解决幻读的

        什么是快照读?普通的select语句都是采用的快照读。顾名思义:在整个事务的处理过程中,执行相同的一个select语句时,每次都是读取的快照。(快照指的是固定的某个时刻的数据,是数据库的一个一致性视图)。也就是说,当事务隔离级别是可重复读,并且执行的select语句是一个普通的select语句时,都会采用快照读的方式读取数据,底层实现原理是:

底层由 MVCC(多版本并发控制)实现,实现的方式是开始事务后,在执行第一个查询语句后,会创建一个 Read View ,后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新纪录,是查询不出来这条数据的,所以就很好的避免了幻读问题。

演示:

test_transaction数据库的事务隔离级别为可重复读

事务A

事务B

mysql> use test_transaction

mysql> use test_transaction

mysql> start transaction;

mysql> start transaction;

mysql> select * from a; //快照读

mysql> insert into a values(5);

mysql> commit;

mysql> select * from a; //快照读

3.2. 出现幻读的两种情况

        在同一个事务处理过程中,如果前后两次都采用快照读,或者都采用当前读,则不会出现幻读问题。如果第一次使用快照读,后面使用了当前读,则会出现幻读问题

3.2.1. 第一种产生幻读的场景

        A事务与B事务。在A事务中第一次查询使用快照读,B事务插入数据。然后在A事务中第二次查询使用当前读。则会产生幻读现象。演示:

事务A

事务B

mysql> use test_transaction

mysql> use test_transaction

mysql> start transaction;

mysql> start transaction;

mysql> select * from a;

mysql> insert into a values(5);

mysql> commit;

mysql> select * from a for update; // 产生了幻读

3.2.2. 第二种产生幻读的场景

        事务A与事务B,在事务A中第一次查询使用快照读,在事务B中插入一条数据,然后在事务A中更新事务B插入的那条记录(执行DML操作,底层默认执行一遍当前读),最后在事务A中再次使用快照读(以当前时刻的数据库状态创建Read View)。则会发生幻读现象

事务A

事务B

mysql> use test_transaction

mysql> use test_transaction

mysql> start transaction;

mysql> start transaction;

mysql> select * from a;

mysql> insert into a values(6);

mysql> commit;

mysql> update a set id=100 where id=6; //主要是因为这个SQL语句的执行触发了当前读

mysql> select * from a; // 产生了幻读

3.3. 总结可重复读的幻读问题

        MySQL的可重复读隔离级别(默认隔离级),根据不同的查询方式,分别提出了避免幻读的方案:

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。
  • 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。

举例了两个发生幻读场景的例子。

  • 第一个例子:如果事务开启后,先执行快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。
  • 第二个例子:如果事务开启后,先执行快照读,事务B往表中插入一条数据,当事务 A 更新了事务 B 的插入操作(DML操作)时,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。

        所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值