MySQL并发控制:隔离级别、锁与MVCC

原文地址:https://www.toutiao.com/a6805760614296715787/?wid=1647050530801

前言

如果数据库中的事务都是串行执行的,这种方式可以保障事务的执行不会出现异常和错误,但带来的问题是串行执行会带来性能瓶颈;而事务并发执行,如果不加以控制则会引发诸多问题,包括死锁、更新丢失等等。这就需要我们在性能和安全之间做出合理的权衡,使用适当的并发控制机制保障并发事务的执行。

并发事务带来的问题

首先我们先来了解一下并发事务会带来哪些问题。并发事务访问相同记录大致可归纳为以下3种情况:

  • 读-读:即并发事务相继读取同一记录;
    因为读取记录并不会对记录造成任何影响,所以同个事务并发读取同一记录也就不存在任何安全 问题,所以允许这种操作。
  • 写-写:即并发事务相继对同一记录做出修改;
    如果允许并发事务都读取同一记录,并相继基于旧值对这一记录做出修改,那么就会出现前一个事务所做的修改被后面事务的修改覆盖,即出现提交覆盖的问题。
    另外一种情况,并发事务相继对同一记录做出修改,其中一个事务提交之后之后另一个事务发生回滚,这样就会出现已提交的修改因为回滚而丢失的问题,即回滚覆盖问题。
    这两种问题都造成丢失更新,其中回滚覆盖称为第一类丢失更新问题,提交覆盖称为第二类丢失更新问题。
  • 写-读或读-写:即两个并发事务对同一记录分别进行读操作和写操作。
    如果一个事务读取了另一个事务尚未提交的修改记录,那么就出现了脏读的问题;
    如果我们加以控制使得一个事务只能读取其他已提交事务的修改的数据,那么这个事务在另一事物提交修改前后读取到的数据是不一样的,这就意味着发生了不可重复读;
    如果一个事务根据一些条件查询到一些记录,之后另一事物向表中插入了一些记录,原先的事务以相同条件再次查询时发现得到的结果跟第一次查询得到的结果不一致,这就意味着发生了幻读。

事务的隔离级别

对于以上提到的并发事务执行过程中可能出现的问题,其严重性也是不一样的,我们可以按照问题的严重程度排个序:

丢失更新 > 脏读 > 不可重复读 > 幻读

因此如果我们可以容忍一些严重程度较轻的问题,我们就能获取一些性能上的提升。于是便有了事务的四种隔离级别:

  • 读未提交(Read Uncommitted):允许读取未提交的记录,会发生脏读、不可重复读、幻读;
  • 读已提交(Read Committed):只允许读物已提交的记录,不会发生脏读,但会出现重复读、幻读;
  • 可重复读(Repeatable Read):不会发生脏读和不可重复读的问题,但会发生幻读问题;但MySQL在此隔离级别下利用间隙锁可以禁止幻读问题的发生;
  • 可串行化(Serializable):即事务串行执行,以上各种问题自然也就都不会发生。
    值得注意的是以上四种隔离级别都不会出现回滚覆盖的问题,但是提交覆盖的问题对于MySQL来说,在Read Uncommitted、Read Committed以及Repeatable Read这三种隔离级别下都会发生(标准的Repeatable Read隔离级别不允许出现提交覆盖的问题),需要额外加锁来避免此问题。

隔离级别的实现

SQL规范定义了以上四种隔离级别,但是并没有给出如何实现四种隔离级别,因此不同数据库的实现方式和使用方式也并不相同。而SQL隔离级别的标准是依据基于锁的实现方式来制定的,因为有必要先了解一下传统的基于锁的隔离级别是如何实现的。

传统的锁有两种:

  • 共享锁(Shared Locks):简称S锁,事务对一条记录进行读操作时,需要先获取该记录的共享锁。
  • 排他锁(Exclusive Locks):简称X锁,事务对一条记录进行写操作时,需要先获取该记录的排他锁。
    需要注意的是,加了共享锁的记录,其他事务也可以获得该记录的共享锁,但是无法获取该记录的排他锁,即S锁和S锁是兼容的,S锁和X锁是不兼容的;而加了排他锁的记录,其他事务既无法获取该记录的共享锁也无法获取排他锁,即X锁和X锁也是不兼容的。
    另外,刚刚说到事务对一条记录进行读操作时,需要先获取该记录的S锁,但有时事务在读取记录时需要阻止其他事务访问该记录,这时就需要获取该记录的X锁。以MySQL为例,有以下两种锁定读的方式:
  1. 读取时对记录加S锁:
SELECT ... LOCK IN SHARE MODE;

如果事务执行了该语句,则会在读取的记录上加S锁,这样就允许其他事务也能获取到该记录的S锁;而如果其他事务需要获取该记录的X锁,那么就需要等待当前事务提交后释放掉S锁。

  1. 读取时对记录加X锁:
SELECT ... FOR UPDATE;

如果事务执行了该语句,则会在读取的记录上加X锁,这样其他事务想要说去该记录的S锁或X锁,那么需要等待当前事务提交后释放掉X锁。

对于锁的粒度而言,锁又可以分为两种:

  • 行锁:只锁住某一行记录,其他行的记录不受影响。
  • 表锁:锁住整个表,所有对于该表的操作都会受影响。

基于锁实现隔离级别

在基于锁的实现方式下,四种隔离级别的区别就在于加锁方式的区别:

  • 读未提交:读操作不加锁,读读,读写,写读并行;写操作加X锁且直到事务提交后才释放。为了解决丢失更新问题,需要对写操作加 X 锁
  • 读已提交:读操作加S锁,写操作加X锁且直到事务提交后才释放;读操作不会阻塞其他事务读或写,写操作会阻塞其他事务写和读,因此可以防止脏读问题。读操作加上 S 锁,这样如果其他事务有正在写的操作,必须等待写操作提交之后才能读,因为 S 和 X 互斥,如果在读的过程中其他事务想写,也必须等事务读完之后才可以。这里的 S 锁是一个临时 S 锁,表示事务读完之后立即释放该锁,可以让其他事务继续写,如果事务再读的话,就可能读到不一样的记录,这就是 不可重复读 了
  • 可重复读:读操作加S锁且直到事务提交后才释放,写操作加X锁且直到事务提交后才释放;读操作不会阻塞其他事务读但会阻塞其他事务写,写操作会阻塞其他事务读和写,因此可以防止脏读、不可重复读。** 为了让事务可以重复读,加在读操作的 S 锁变成了持续 S 锁,也就是直到事务结束时才释放该锁,这可以保证整个事务过程中,其他事务无法进行写操作,所以每次读出来的记录是一样的**
  • 串行化:读操作和写操作都加X锁且直到事务提交后才释放,粒度为表锁,也就是严格串行。

这里面有一些细节值得注意:

  • 如果锁获取之后直到事务提交后才释放,这种锁称为长锁;如果锁在操作完成之后就被释放,这种锁称为短锁。例如,在读已提交隔离级别下,读操作所加S锁为短锁,写操作所加X锁为长锁。
  • 对于可重复读和串行化隔离级别,读操作所加S锁和写操作所加X锁均为长锁,即事务获取锁之后直到事务提交后才能释放,这种把获取锁和释放锁分为两个不同的阶段的协议称为两阶段锁协议(2-phase locking)。两阶段锁协议规定在加锁阶段,一个事务可以获得锁但是不能释放锁;而在解锁阶段事务只可以释放锁,并不能获得新的锁。两阶段锁协议能够保证事务串行化执行,解决事务并发问题,但也会导致死锁发生的概率大大提升。

MySQL隔离级别的实现

不同数据库对于SQL标准中规定的隔离级别支持是不一样的,数据库引擎实现隔离级别的方式虽然都在尽可能地贴近标准的隔离级别规范,但和标准的预期还是有些不一样的地方。
MySQL(InnoDB)支持的4种隔离级别,与标准的各级隔离级别允许出现的问题有些出入,比如MySQL在可重复读隔离级别下可以防止幻读的问题出现,但也会出现提交覆盖的问题。
相对于传统隔离级别基于锁的实现方式,MySQL 是通过MVCC(多版本并发控制)来实现读-写并发控制,又是通过两阶段锁来实现写-写并发控制的。MVCC是一种无锁方案,用以解决事务读-写并发的问题,能够极大提升读-写并发操作的性能。

MVCC的实现原理

为了方便描述,首先我们创建一个表book,就三个字段,分别是主键book_id, 名称book_name, 库存stock。然后向表中插入一些数据:

INSERT INTO book VALUES(1, '数据结构', 100);
INSERT INTO book VALUES(2, 'C++指南', 100);
INSERT INTO book VALUES(3, '精通Java', 100);

版本链

对于使用InnoDB存储引擎的表,其聚簇索引记录中包含了两个重要的隐藏列:

  • trx_id:每当事务对聚簇索引中的记录进行修改时,都会把当前事务的事务id记录到trx_id中。
  • roll_pointer:每当事务对聚簇索引中的记录进行修改时,都会把该记录的旧版本记录到undo日志中,通过roll_pointer这个指针可以用来获取该记录旧版本的信息。
    如果在一个事务中多次对记录进行修改,则每次修改都会生成undo日志,并且这些undo日志通过roll_pointer指针串联成一个版本链,版本链的头结点是该记录最新的值,尾结点是事务开始时的初始值。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容