MySQL 事务、锁机制和 MVCC

发布于 2024-05-12 16:10:18 字数 5572 浏览 35 评论 0

MySQL 事务

在我的理解下,事务可以使「⼀组操作」要么全部成功,要么全部失败。事务其⽬的是为了「保证数据最终的⼀致性」。举个例⼦,我给你发⽀付宝转了 888 块红包。那⾃然我的⽀付宝余额会扣减 888 块,你的⽀付宝余额会增加 888 块。⽽事务就是保证我的余额扣减跟你的余额增添是同时成功或者同时失败 的,这样这次转账就正常了。

事务特性

是 ACID 嘛,分别是原⼦性(Atomicity)、⼀致性(Consistency)、隔离性(Isolation)、持久性(Durability)。

原⼦性指的是:当前事务的操作要么同时成功,要么同时失败。原⼦性由 undo log⽇志来保证,因为 undo log 记载着数据修改前的信息。⽐如我们要 insert ⼀条数据了,那 undo log 会记录的⼀条对应的 delete ⽇志。我们要 update ⼀条记录时,那 undo log 会记录之前的「旧值」的 update 记录。如果执⾏事务过程中出现异常的情况,那执⾏「回滚」。InnoDB 引擎就是利⽤undo log 记录下的数据,来将数据「恢复」到事务开始之前。

⼀致性我稍稍往后讲,我先来说下隔离性。隔离性指的是:在事务「并发」执⾏时,他们内部的操作不能互相⼲扰。如果多个事务可以同时操作⼀个数据,那 么就会产⽣脏读、重复读、幻读的问题。于是,事务与事务之间需要存在「⼀定」的隔离。在 InnoDB 引擎中,定义了四种隔离级别供我们使⽤:分别是: read uncommit(读未提交)、read commit (读已提交)、repeatable read (可重复复读)、serializable (串⾏)。不同的隔离级别对事务之间的隔离性是不⼀样的(级别越⾼事务隔离性越好,但性能就越低),⽽隔离性是由 MySQL 的各种锁来实现的,只是它屏蔽 了加锁的细节。

image-20220605104231985

持久性指的就是:⼀旦提交了事务,它对数据库的改变就应该是永久性的。说⽩了就是,会将数据持久化在硬盘上。⽽持久性由 redo log ⽇志来保证,当我们要修改数据时,MySQL 是先把这条记录所在的「⻚」找到,然后把该⻚加载到内存中,将对应记录进⾏修改。为了防⽌内存修改完了, MySQL 就挂掉了(如果内存改完,直接挂掉,那这次的修改相当于就丢失

了)。MySQL 引⼊了 redo log,内存写完了,然后会写⼀份 redo log,这份 redo log 记载着这次在某个⻚上做了什么修改。即便 MySQL 在中途挂了,我们还可以根据 redo log 来对数据进⾏恢复。redo log 是顺序写的,写⼊速度很快。并且它记录的是物理修改(xxxx⻚做了 xxx 修改),⽂件的体积很⼩,恢复速度也很快。

image-20220605104336300

回头再来讲⼀致性,「⼀致性」可以理解为我们使⽤事务的「⽬的」,⽽「隔离性」「原⼦性」「持久性」均是为了保障「⼀致性」的⼿段,保证⼀致性需要 由应⽤程序代码来保证。⽐如,如果事务在发⽣的过程中,出现了异常情况,此时你就得回滚事务,⽽不是强⾏提交事务来导致数据不⼀致。

隔离级别

为了讲清楚隔离级别,我顺带来说下 MySQL 锁相关的知识吧。在 InnoDB 引擎下,按锁的粒度分类,可以简单分为⾏锁和表锁。⾏锁实际上是作⽤在 索引之上的(索引上次已经说过了,这⾥就不赘述了)。当我们的 SQL 命中了索引,那锁住的就是命中条件内的索引节点(这种就是⾏锁),如果没有命中索引, 那我们锁的就是整个索引树(表锁)。:简单来说就是:锁住的是整棵树还是某⼏个节点,完全取决于 SQL 条件是否有命中到对应的索引节点。⽽⾏锁⼜可以简单 分为读锁(共享锁、S 锁)和写锁(排它锁、X 锁)。读锁是共享的,多个事务可以同时读取同⼀个资源,但不允许其他事务修改。写锁是排他的,写锁会阻塞

其他的写锁和读锁。

image-20220605104624137

read uncommit(读未提交)

我现在就再回到隔离级别上吧,就直接以例⼦来说明啦。⾸先来说下 read uncommit(读未提交)。⽐如说:A 向 B 转账,A 执⾏了转账语句,但 A 还没有提交事务,B 读取数据,发现⾃⼰账户钱变多了!B 跟 A 说,我已经收到钱 了。A 回滚事务【rollback】,等 B 再查看账户的钱时,发现钱并没有多。简单的定义就是:事务 B 读取到了事务 A 还没提交的数据,这种⽤专业术语来说 叫做「脏读」。对于锁的维度⽽⾔,其实就是在 read uncommit 隔离级别下,读不会加任何锁,⽽写会加排他锁。读什么锁都不加,这就让排他锁⽆法排它了。

image-20220605104705677

⽽我们⼜知道,对于更新操作⽽⾔,InnoDB 是肯定会加写锁的(数据库是不可能允许在同⼀时间,更新同⼀条记录的)。⽽读操作,如果不加任何锁, 那就会造成上⾯的脏读。脏读在⽣产环境下肯定是⽆法接受的,那如果读加锁的话,那意味着:当更新数据的时,就没办法读取了,这会极⼤地降低数据库性能。在 MySQL InnoDB 引擎层⾯,⼜有新的解决⽅案(解决加锁后读写性能问题),叫做 MVCC(Multi-Version Concurrency Control) 多版本并发控制。

image-20220605104749797

在 MVCC 下,就可以做到读写不阻塞,且避免了类似脏读这样的问题。那 MVCC 是怎么做的呢?MVCC 通过⽣成数据快照(Snapshot),并⽤ 这个快照来提供⼀定级别(语句级或事务级)的⼀致性读取。回到事务隔离级别下,针对于 read commit (读已提交) 隔离级别,它⽣成的就是语句级快照,⽽针对于 repeatable read (可重复读),它⽣成的就是事务级的快照。

image-20220605104827164

前⾯提到过 read uncommit 隔离级别下会产⽣脏读。

read commit (读已提交)

⽽read commit (读已提交) 隔离级别解决了脏读。思想其实很简单:在读取的时候⽣成⼀个"版本号",等到其他事务 commit 了之后,才会读取最新已 commit 的"版本号"数据。 ⽐如说:事务 A 读取了记录(⽣成版本号),事务 B 修改了记录(此时加了写锁),事务 A 再读取的时候,是依据最新的版本号来读取的(当事务 B 执⾏ commit 了之后,会⽣成⼀个新的版本号),如果事务 B 还没有 commit,那事务 A 读取的还是之前版本号的数据。

通过「版本」的概念,这样就解决了脏 读的问题,⽽「版本」其实就是对应快照的数据。read commit (读已提交) 解决了脏读,但也会有其他并发的问题。「不可重复读」:⼀个事务读取到另外⼀个事务已经提交的数据,也就是说⼀个事务可以看到其他事务所做的修改。不可重 复读的例⼦:A 查询数据库得到数据,B 去修改数据库的数据,导致 A 多次查询数据库的结果都不⼀样【危害:A 每次查询的结果都是受 B 的影响的】。

repeatable read (可重复复读)

了解 MVCC 基础之后,就很容易想到 repeatable read (可重复复读) 隔离级别是怎么避免不可重复读的问题了(前⾯也提到了)。repeatable read (可重复复读) 隔离级别是「事务级别」的快照!每次读取的都是「当前事务的版本」,即使当前数据被其他事务修改了(commit),也只会读取当前事务版 本的数据。

image-20220605105006130

⽽repeatable read (可重复复读) 隔离级别会存在幻读的问题,「幻读」指的是指在⼀个事务内读取到了别的事务插⼊的数据,导致前后读取不⼀致。在 InnoDB 引擎下的的 repeatable read (可重复复读) 隔离级别下,快照读 MVCC 影响下,已经解决了幻读的问题(因为它是读历史版本的数据)。⽽如果是当前读(指的是 select * from table for update),则需要配合间隙锁来解决幻读的问题。

serializable (串⾏)

剩下的就是 serializable (串⾏) 隔离级别了,它的最⾼的隔离级别,相当于不允许事务的并发,事务与事务之间执⾏是串⾏的,它的效率最低,但同时也是最安全的。

MVCC

MVCC 的主要是通过 read view 和 undo log 来实现的。

image-20220605105404249

undo log 前⾯也提到了,它会记录修改数据之前的信息,事务中的原⼦性就是通过 undo log 来实现的。所以,有 undo log 可以帮我们找到「版本」的数据,⽽read view 实际上就是在查询时,InnoDB 会⽣成⼀个 read view,read view 有⼏个重要的字段,分别是:trx_ids(尚未提交 commit 的事务版本号集合),up_limit_id(下⼀次要⽣成的事务 ID 值), low_limit_id(尚未提交版本号的事务 ID 最⼩值)以及 creator_trx_id(当前的事务版本号)。

在每⾏数据有两列隐藏的字段,分别 是 DB_TRX_ID(记录着当前 ID)以及 DB_ROLL_PTR(指向上⼀个版本数据在 undo log ⾥的位置指针),铺垫到这了,很容易就发现,MVCC 其实就是靠「⽐对版本」来实现读写不阻塞,⽽版本的数据存在于 undo log 中。⽽针对于不同的隔离级别(read commit 和 repeatable read),⽆⾮就是 read commit 隔离级别下,每次都获取⼀个新的 read view,repeatable read 隔离级别则每次事务只获取⼀个 read view。

总结

  • 事务为了保证数据的最终⼀致性
  • 事务有四⼤特性,分别是原⼦性、⼀致性、隔离性、持久性
    • 原⼦性由 undo log 保证
    • 持久性由 redo log 保证
    • 隔离性由数据库隔离级别供我们选择,分别有 read uncommit,read commit,repeatable read,serializable
    • ⼀致性是事务的⽬的,⼀致性由应⽤程序来保证
  • 事务并发会存在各种问题,分别有脏读、重复读、幻读问题。上⾯的不同隔离级别可以解决掉由于并发事务所造成的问题,⽽隔离级别实际上就是由 MySQL 锁来实现的
  • 频繁加锁会导致数据库性能低下,引⼊了 MVCC 多版本控制来实现读写不阻塞,提⾼数据库性能
  • MVCC 原理即通过 read view 以及 undo log 来实现

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

随心而道

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

内心激荡

文章 0 评论 0

JSmiles

文章 0 评论 0

左秋

文章 0 评论 0

迪街小绵羊

文章 0 评论 0

瞳孔里扚悲伤

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文