MySQL系列(二):MySQL是怎么处理并发操作的?
共 3731字,需浏览 8分钟
·
2021-05-11 01:33
作者:z小赵
★一枚用心坚持写原创的“无趣”程序猿,在自身受益的同时也让朋友们在技术上有所提升。
目录
为什么需要锁? MySQL 中锁分类? 什么是事务? 事务的隔离级别 MySQL 是怎么实现事务机制的? MVCC 机制 总结
为什么需要锁?
相信大家都比较熟悉电商系统中库存管理的场景,对于日常活动促销、618、双 11 等场景,会在规定时间内对商品进行促销活动,假设现在有一款 HHKB 机械键盘要参与促销活动,数据库中准备了 10 件,促销活动开始时,多位买家开始争抢,每卖出一件商品,库存减 1,直到卖完,那么怎么能保证商品不会卖超呢?
对于以上这个场景来说,我们需要用到锁机制来保证每卖出一件商品,对库存进行更新操作时,其他用户请求不能对该商品库存进行修改;换句话说,用户 1 拿到了修改库存的锁,则只有用户 1 能修改数据,而用户 2 只能等着不能修改数据。如下图所示:
相反,如果没有锁的加持,用户 1 和用户 2 发现库存还有 1 件商品,同时都开始下单,用户 1 先将库存更新为 0,此时商品已经售完,而用户 2 也将库存更新为 0,就导致了卖超的尴尬情况。
MySQL 中锁分类?
锁根据使用场景不同,被分成了各种各样的锁。比如读写可以分为读锁和写锁,对于读请求之间相互是互不影响的,因为数据没有被所有,大家读取到的数据都是一样的,所以读锁也称之为共享锁;对于写请求,由于存在数据的变更,所以请求之间是互斥的,所以也称之为排它锁。
对于根据锁锁定的范围大小,可以分为全局锁、表锁、元数据锁、行锁:
全局锁:顾名思义就是对整个数据库进行加锁操作,加锁期间,整个数据库只能够进行读操作。 表锁:是对数据库中的某张表进行加锁,此时表与表之间可以同时进行写操作而互不影响,但是同一时刻同一张表只能有一个写操作。 页面锁:页面锁是介于表锁和行锁之间的一种锁,其优势是中和表锁和行锁的锁开销。 行锁:是对数据库表中的某一行进行加锁操作。
从上也能够看出,锁的范围是逐渐减小的,在实际生产环境中需要根据业务场景来选择不同粒度的锁。
什么是事务?
由多个事件组成的一组动作,要么同时成功,要么发生失败时全体进行回滚;换句话说就是多个原子操作合并为一个原子操作。怎么能够保证一个事务能够正确执行呢?想要保证一个事务正确执行的依据是其必须满足 ACID,即原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)。
举个例子:初始状态 A 账户有 100 块,B 账户有 100 块。
原子性:一个事务或执行动作不能被分割为多个阶段去处理,所以整个执行流程要么全部成功,要么失败全部回滚。举例:A 向 B 转账 100 块,A 的账户变成 0,B 的账户变成 200,两个账户的钱增减整体视为一个动作。 一致性:一个状态在经历一些动作之后转变成另外一个状态。举例:A 向 B 转账 100 块,经过 A 向 B 的一个转账动作并且执行成功时,A 的账户变成 0,B 的账户变成 200 块,执行转账动作前后账户的总金额没有发生改变。 隔离性:多个事务在并发操作时,相互之间不能够看到对方执行的动作,即相互之间是隔离的。隔离也分为多个级别,不同的隔离级别所保证的隔离程度也是不一样的。 持久性:一个事务一旦提交以后,其对数据源的修改是永久性的保存到了数据库中。如何做到 100%的持久性是一个几乎不太可能实现的事情,比如数据虽然持久化到了磁盘上,但由于一些不可抗拒的外力因素导致数据发生了丢失,所以在实际情况中需要规定一个持久性的级别,即认为只需要数据持久化到磁盘上就可以认为达到了持久性的特性。
事务的隔离级别
MySQL 的事务隔离级别分为一下几种:
举个例子:初始状态 A 账户有 100 块,B 账户有 100 块。
读未提交(Read Uncommitted):在一个事务中,部分提交后对其他事务可见。举例:A 转账给 B,A 的账户金额变成 0,B 的账户还未增加到 200,A 读取自己的账户时,发现自己的账户金额变为 0,但是由于事务执行中途出现故障(假设 B 账户因为某种原因账户被锁定不能被转账),此时事务进行了回滚操作,那么就会导致 A 读取到账户金额是错误的。这种现象也称之为脏读。 读已提交(Read Committed):读操作只能够读取到自己事务中的数据或者是事务提交后的结果。举例:转账操作,如果一个请求读取账户 A 的金额,如果事务正常提交了,则其读取的账户金额一定是 0,如果事务回滚,则读取到的金额一定是 100。但是读已提交不能够避免重复读,有可能两次读取到结果不一致。 可重复读(Repeatable Read):该级别能够保证多次读取到的结果是相同的,但是不能够解决幻读的情况。幻读在数据库中具体的体现是范围查询,比如第一次一个范围查询到的结果集的同时,另外一个事务插入了数据,第二次查询的结果和第一次查询的结果集不一样。 串行化(Serializable):强制所有事务按照顺序依次执行,只有一个事务执行完成后,下一个事务才能接着执行。这样就能够避免产生脏读、不可重复读、幻读等情况,但是同时也降低了数据库的并发度。
在实际开发场景中,同样需要根据实际业务场景来选择合适的隔离级别,一般用的比较普遍的两种隔离级别是读已提交和可重复读。
MySQL 是怎么实现事务机制的?
MySQL 中实现了事务机制的常见存储引擎有 InnoDB 和 NDB(上篇文章也介绍过,本系列全程以 InnoDB 展开介绍)。使用 InnoDB 提交事务的时候,首先需要关闭自动提交,通过 set autocommit = 0
命令关闭自动提交功能,然后通过 start transaction
开启一个事务,接着编写在事务内要执行的 SQL,最后通过 commit
提交事务。
如果两个事务同时提交,并且都需要操作同一个资源,此时会产生 死锁,那么 MySQL 是怎么处理这样情况的呢?InnoDB 采用的是将持有最少行级排他锁进行回滚操作,什么叫持有最少行级排排它锁?大白话解释一下:就是每个事务开启后,需要执行增删改等操作 SQL 的个数。比如有两个事务,一个需要执行 2 个 select 语句和 3 个 update 语句,另外一个需要执行 1 个 select 语句和 1 个 update 语句,当两个事务的 update 同一时刻需要对方锁住的资源时,会将后者的事务进行回滚操作。
MVCC 机制
为了降低死锁情况的发生,MySQL 引入录入 MVCC 机制,从而来进一步降低因为加减锁而造成的系统开销。通过在数据表上增加两个隐藏列,一个列用于存储当前行的创建时的版本,另外一个列用于存储当前行的删除时的版本。下面我们来看看 MVCC 机制在 CRUD 中是怎么工作的。
SELECT 操作(由两个条件决定): InnoDB 会查找创建行的版本小于等于当前事务版本的数据行。比如目前数据表里的第一行现在有两个版本,分别为 1 和 2,而当前事务的版本号是 1,则此时会选择版本号为 1 的这一行. InnoDB 会查找删除行的版本大于当前事务版本的数据行。比如目前数据表里的第一行有两个版本,分别为 1 和 2,但是版本号为 1 的那行的删除列标记为 0,而当前书屋的版本号为 1,则此时会选择版本号为 1 的这一行。
当同时满足以上两个条件的时候,select 语句就可以得到一条最新的已提交的且未被删除的行记录。
UPDATE 操作:新插入一条数据到表中,并且将创建列的版本号设置为当前事务的版本号,并且将当前事务的版本号保存到原来记录的删除列上。
INSERT 操作:新插入一条数据到表中,并且将创建列的版本号设置为当前的事务版本号。
DELETE 操作:会将要删除的行的历史所有版本号全部设置上当前的事务版本号。
通过 CRUD 四种操作可以看出,MVCC 机制只能作用于 读已提交 和 可重复读 两个事务隔离级别下,因为读未提交会使得 SELECT 产生脏读,而串行化本身已经是顺序操作了,没有增加 MVCC 机制的必要。
至于 MVCC 的具体实现细节,它结合了 undo log 来实现的,我们在后面讲解 MySQL 的相关日志文件功能的时候再详细展开。
总结
本文介绍了 MySQL 的锁机制和事务的概念及实现原理。下篇文章我们来接着研究MySQL 的索引机制,看看它到底该怎么用才能使得查询操作变得更加高效,敬请期待。
欢迎关注微信公众号:互联网全栈架构,收取更多有价值的信息。