文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

一文彻底读懂MySQL事务的四大隔离级别

2024-12-24 18:25

关注

 

事务

什么是事务?

事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

假如A转账给B 100 元,先从A的账户里扣除 100 元,再在 B 的账户上加上 100 元。如果扣完A的100元后,还没来得及给B加上,银行系统异常了,最后导致A的余额减少了,B的余额却没有增加。所以就需要事务,将A的钱回滚回去,就是这么简单。

事务的四大特性

 

事务并发存在的问题

事务并发执行存在什么问题呢,换句话说就是,一个事务是怎么干扰到其他事务的呢?看例子吧~

假设现在有表:

 

  1.  CREATE TABLE `account` ( 
  2.    `id` int(11) NOT NULL
  3.    `namevarchar(255) DEFAULT NULL
  4.    `balance` int(11) DEFAULT NULL
  5.    PRIMARY KEY (`id`), 
  6.    UNIQUE KEY `un_name_idx` (`name`) USING BTREE 
  7.  ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

表中有数据:

 

脏读(dirty read)

假设现在有两个事务A、B:

 

由上图可以发现,事务A、B交替执行,事务A被事务B干扰到了,因为事务A读取到事务B未提交的数据,这就是脏读。

不可重复读(unrepeatable read)

假设现在有两个事务A和B:

 

事务A又被事务B干扰到了!在事务A范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。

幻读

假设现在有两个事务A、B:

 

事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入/删除了数据,并静悄悄地提交,然后事务A再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。

事务的四大隔离级别实践

既然并发事务存在脏读、不可重复、幻读等问题,InnoDB实现了哪几种事务的隔离级别应对呢?

读未提交(Read Uncommitted)

想学习一个知识点,最好的方式就是实践之。好了,我们去数据库给它设置读未提交隔离级别,实践一下吧~

 

先把事务隔离级别设置为read uncommitted,开启事务A,查询id=1的数据

 

  1.  set session transaction isolation level read uncommitted
  2.  begin
  3.  select * from account where id =1; 

结果如下:

这时候,另开一个窗口打开mysql,也把当前事务隔离级别设置为read uncommitted,开启事务B,执行更新操作

 

 

  1.  set session transaction isolation level read uncommitted
  2.  begin
  3.  update account set balance=balance+20 where id =1; 

接着回事务A的窗口,再查account表id=1的数据,结果如下:

 

可以发现,在读未提交(Read Uncommitted) 隔离级别下,一个事务会读到其他事务未提交的数据的,即存在脏读问题。事务B都还没commit到数据库呢,事务A就读到了,感觉都乱套了。。。实际上,读未提交是隔离级别最低的一种。

读已提交(READ COMMITTED)

为了避免脏读,数据库有了比读未提交更高的隔离级别,即读已提交。

 

把当前事务隔离级别设置为读已提交(READ COMMITTED),开启事务A,查询account中id=1的数据

 

  1.  set session transaction isolation level read committed
  2.  begin
  3.  select * from account where id =1; 

另开一个窗口打开mysql,也把事务隔离级别设置为read committed,开启事务B,执行以下操作

 

  1.  set session transaction isolation level read committed 
  2.  begin
  3.  update account set balance=balance+20 where id =1; 

接着回事务A的窗口,再查account数据,发现数据没变:

 

我们再去到事务B的窗口执行commit操作:

 

  1. commit

最后回到事务A窗口查询,发现数据变了:

 

由此可以得出结论,隔离级别设置为已提交读(READ COMMITTED) 时,已经不会出现脏读问题了,当前事务只能读取到其他事务提交的数据。但是,你站在事务A的角度想想,存在其他问题吗?

读已提交的隔离级别会有什么问题呢?

在同一个事务A里,相同的查询sql,读取同一条记录(id=1),读到的结果是不一样的,即不可重复读。所以,隔离级别设置为read committed的时候,还会存在不可重复读的并发问题。

可重复读(Repeatable Read)

如果你的老板要求,在同个事务中,查询结果必须是一致的,即老板要求你解决不可重复的并发问题,怎么办呢?老板,臣妾办不到?来实践一下可重复读(Repeatable Read) 这个隔离级别吧~

 

哈哈,步骤1、2、6的查询结果都是一样的,即repeatable read解决了不可重复读问题,是不是心里美滋滋的呢,终于解决老板的难题了~

RR级别是否解决了幻读问题呢?

再来看看网上的一个热点问题,有关于RR级别下,是否解决了幻读问题?我们来实践一下:

由图可得,步骤2和步骤6查询结果集没有变化,看起来RR级别是已经解决幻读问题了~ 但是呢,RR级别还是存在这种现象:

 

其实,上图如果事务A中,没有 update accountsetbalance=200whereid=5;这步操作, select*fromaccountwhereid>2查询到的结果集确实是不变,这种情况没有幻读问题。但是,有了update这个骚操作,同一个事务,相同的sql,查出的结果集不同,这个是符合了幻读的定义~

这个问题,亲爱的朋友,你觉得它算幻读问题吗?

串行化(Serializable)

前面三种数据库隔离级别,都有一定的并发问题,现在放大招吧,实践SERIALIZABLE隔离级别。

把事务隔离级别设置为Serializable,开启事务A,查询account表数据

 

  1.  set session transaction isolation level serializable
  2.  select @@tx_isolation; 
  3.  begin
  4. select * from account; 

另开一个窗口打开mysql,也把事务隔离级别设置为Serializable,开启事务B,执行插入一条数据:

 

  1.  set session transaction isolation level serializable
  2.  select @@tx_isolation; 
  3.  begin
  4.  insert into account(id,name,balance) value(6,'Li',100); 

执行结果如下:

 

由图可得,当数据库隔离级别设置为serializable的时候,事务B对表的写操作,在等事务A的读操作。其实,这是隔离级别中最严格的,读写都不允许并发。它保证了最好的安全性,性能却是个问题~

MySql隔离级别的实现原理

实现隔离机制的方法主要有两种:

MySql使用不同的锁策略(Locking Strategy)/MVCC来实现四种不同的隔离级别。RR、RC的实现原理跟MVCC有关,RU和Serializable跟锁有关。

读未提交(Read Uncommitted)

官方说法:

SELECT statements are performed in a nonlocking fashion, but a possible earlier version of a row might be used. Thus, using this isolation level, such reads are not consistent.

读未提交,采取的是读不加锁原理。

串行化(Serializable)

官方的说法:

InnoDB implicitly converts all plain SELECT statements to SELECT ... FOR SHARE if autocommit is disabled. If autocommit is enabled, the SELECT is its own transaction. It therefore is known to be read only and can be serialized if performed as a consistent (nonlocking) read and need not block for other transactions. (To force a plain SELECT to block if other transactions have modified the selected rows, disable autocommit.)

MVCC的实现原理

MVCC,中文叫多版本并发控制,它是通过读取历史版本的数据,来降低并发事务冲突,从而提高并发性能的一种机制。它的实现依赖于隐式字段、undo日志、快照读&当前读、Read View,因此,我们先来了解这几个知识点。

隐式字段

对于InnoDB存储引擎,每一行记录都有两个隐藏列DBTRXID,DBROLLPTR,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列 DBROWID。

 

 

undo日志

多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(DBROLLPTR)连一条Undo日志链。

我们通过例子来看一下~

 

  1.   mysql> select * from account ; 
  2.   +----+------+---------+ 
  3.   | id | name | balance | 
  4.   +----+------+---------+ 
  5.   |  1 | Jay  |     100 | 
  6.   +----+------+---------+ 
  7.   1 row in set (0.00 sec) 

事务B修改后,形成的Undo Log链如下:

 

快照读&当前读

快照读:

读取的是记录数据的可见版本(有旧的版本),不加锁,普通的select语句都是快照读,如:

 

  1. select * from account where id>2; 

当前读:

读取的是记录数据的最新版本,显示加锁的都是当前读

 

  1. select * from account where id>2 lock in share mode; 
  2.  
  3. select * from account where id>2 for update

 

Read View

为了下面方便讨论Read View可见性规则,先定义几个变量

  1. 如果DBTRXID < minlimitid,表明生成该版本的事务在生成ReadView前已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。
  2. 如果DBTRXID > m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
  3. 如果 minlimitid =

注意啦!! RR跟RC隔离级别,最大的区别就是:RC每次读取数据前都生成一个ReadView,而RR只在第一次读取数据时生成一个ReadView。

已提交读(READ COMMITTED) 存在不可重复读问题的分析历程

我觉得理解一个新的知识点,最好的方法就是居于目前存在的问题/现象,去分析它的来龙去脉~ RC的实现也跟MVCC有关,RC是存在重复读并发问题的,所以我们来分析一波RC吧,先看一下执行流程

 


假设现在系统里有A,B两个事务在执行,事务ID分别为100、200,并且假设存在的老数据,插入事务ID是50哈~

 

 

事务A 先执行查询1的操作

 

  1.  # 事务A,Transaction ID 100 
  2.  begin ; 
  3.  查询1:select *  from account WHERE id = 1;  

 

事务 B 执行更新操作,id =1记录的undo日志链如下

 

  1. begin
  2. update account set balance =balance+20 where id =1; 

 

 

 


回到事务A,执行查询2的操作

 

 

 

  1.  begin ; 
  2.  查询1:select *  from account WHERE id = 1;  
  3.  查询2:select *  from account WHERE id = 1;  

查询2执行分析:

我们回到事务B,执行提交操作,这时候undo日志链不变

 

  1.  begin
  2.  update account set balance =balance+20 where id =1; 
  3.  commit 

 

再次回到事务A,执行查询3的操作

 

  1.  begin ; 
  2.  查询1:select *  from account WHERE id = 1;  
  3.  查询2:select *  from account WHERE id = 1;  
  4.  查询3:select *  from account WHERE id = 1;  

查询3执行分析:

所以,这就是RC存在不可重复读问题的过程啦~有不理解的地方可以多读几遍哈~

可重复读(Repeatable Read)解决不可重复读问题的一次分析

我们再来分析一波,RR隔离级别是如何解决不可重复读并发问题的吧~

你可能会觉得两个并发事务的例子太简单了,好的!我们现在来点刺激的,开启三个事务~

 


假设现在系统里有A,B,C两个事务在执行,事务ID分别为100、200,300,存量数据插入的事务ID是50~

 

 

  1.   # 事务A,Transaction ID 100 
  2.   begin ; 
  3.   UPDATE account SET balance = 1000  WHERE id = 1; 

 

  1. # 事务B,Transaction ID 200 
  2. begin ; //开个事务,占坑先 

 

这时候,account表中,id =1记录的undo日志链如下:

 

 

  1.  # 事务C,Transaction ID 300 
  2.  begin ; 
  3.  //查询1:select * from  account WHERE id = 1; 

查询1执行过程分析:

接着,我们把事务A提交一下:

 

  1.   # 事务A,Transaction ID 100 
  2.   begin ; 
  3.   UPDATE account SET balance = 1000  WHERE id = 1; 
  4.   commit

在事务B中,执行更新操作,把id=1的记录balance修改为2000,更新完后,undo 日志链如下:

 

  1. # 事务B,Transaction ID 200 
  2. begin ; //开个事务,占坑先 
  3. UPDATE account SET balance = 2000 WHERE id = 1; 

 

 

回到事务C,执行查询2

 

  1.   # 事务C,Transaction ID 300 
  2.   begin ; 
  3.   //查询1:select * from  account WHERE id = 1; 
  4.   //查询2:select * from  account WHERE id = 1; 

查询2:执行分析:

锁相关概念补充(附):

共享锁与排他锁

InnoDB 实现了标准的行级锁,包括两种:共享锁(简称 s 锁)、排它锁(简称 x 锁)。

如果事务 T1 持有行 r 的 s 锁,那么另一个事务 T2 请求 r 的锁时,会做如下处理:

如果 T1 持有 r 的 x 锁,那么 T2 请求 r 的 x、s 锁都不能被立即允许,T2 必须等待T1释放 x 锁才可以,因为X锁与任何的锁都不兼容。

记录锁(Record Locks)

记录锁的事务数据(关键词:lock_mode X locks rec butnotgap),记录如下:

 

  1.  RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARYof table `test`.`t`  
  2.  trx id 10078 lock_mode X locks rec but not gap 
  3.  Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 
  4.  0: len 4; hex 8000000a; asc     ;; 
  5.  1: len 6; hex 00000000274f; asc     'O;; 
  6.  2: len 7; hex b60000019d0110; asc        ;; 

间隙锁(Gap Locks)

Next-Key Locks

RC级别存在幻读分析

因为RC是存在幻读问题的,所以我们先切到RC隔离级别,分析一波~

假设account表有4条数据。

 

 

因此,我们可以发现,RC隔离级别下,加锁的select, update, delete等语句,使用的是记录锁,其他事务的插入依然可以执行,因此会存在幻读~

RR 级别解决幻读分析

因为RR是解决幻读问题的,怎么解决的呢,分析一波吧~

假设account表有4条数据,RR级别。

 

可以发现,事务B执行插入操作时,阻塞了~因为事务A在执行select ... lock in share mode的时候,不仅在 id = 3,4 这2条记录上加了锁,而且在id > 2 这个范围上也加了间隙锁。

因此,我们可以发现,RR隔离级别下,加锁的select, update, delete等语句,会使用间隙锁+ 临键锁,锁住索引记录之间的范围,避免范围间插入记录,以避免产生幻影行记录。

来源:捡田螺的小男孩内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯