本文是mysql 事务的第二部分-事务的隔离级别与锁。自己不是专职的DBA,理解难免有偏差,只是简单地总结一下。希望对你有所帮助,如果有纰漏,也请留言指出,感谢~
文章主要内容如下:
- 锁
- 并发产生的三个经典问题
- 事务隔离级别介绍
- 综合:四种隔离级别是如何利用锁解决并发产生的问题的?以及为什么有些情况解决不了
在讲解事务隔离级别之前,我们想讲一下锁与三种并发控制模式,因为在隔离级别这部分会用到。
1 锁
锁,就是将某些资源进行锁定。
1.1 锁的分类
1.1.1 粒度
按照粒度分行锁、表锁、页锁。mysql中 采用的是行锁跟表锁。顾名思义,行锁就是锁定一行资源,表锁就是锁定表。加锁是需要消耗资源的,所以锁的粒度越小,消耗的资源越多,但是并发度越高。
1.1.2 共享
按照是否可以共享,锁又可以分为共享锁(读锁)跟排他锁(写锁)。
1.1.3 意向锁
意向锁是一种表锁,无需程序员自己加锁,是数据库自动帮添加。
我们用一个例子说明意向锁的用途:
事务A锁住了表中的一行,让这一行只能读,不能写。
之后,事务B申请整个表的写锁。
如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。
数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。
数据库要怎么判断这个冲突呢?
step1:判断表是否已被其他事务用表锁锁表
step2:判断表中的每一行是否已被行锁锁住。
注意step2中通过遍历查询,这样的判断方法效率实在不高,因为需要遍历整个表。
于是就有了意向锁。
意向互斥锁(IX),意向共享锁(IS)是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。行级别的X和S按照普通的共享、排他规则即可。
1.1.4 悲观锁与乐观锁
悲观锁就是上文讲的实际的锁,而乐观锁不是真是的锁,而是一种思想。
乐观锁的一种实现方式是通过时间戳来模拟锁。
- 给每一个事务加一个全局唯一的时间戳。
- 每个数据项有两个时间戳:读时间戳、写时间戳,分别代表了当前数据被成功执行的的对应事务的时间戳。
当读取数据时,将version字段的值一同读出;
数据每更新一次,对此version值加1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
1.2 锁的实现算法
记录锁(Record-Lock)
记录锁是锁住记录的,锁住的是索引记录,而不是我们真正的数据记录:
如果锁的是非主键索引,会在自己的索引上面加锁之后然后再去主键上面加锁锁住。
如果表上没有索引(包括没有主键),则会使用隐藏的主键索引进行加锁。
如果要锁的没有索引,则会进行全表记录加锁。
间隙锁
间隙锁顾名思义锁间隙,不锁记录。锁间隙的意思就是锁定某一个范围,间隙锁又叫 gap 锁,其不会阻塞其他的 gap 锁,但是会阻塞插入间隙锁,这也是用来防止幻读的关键。
next-key 锁
这个锁本质是记录锁加上 gap 锁。在 RR 隔离级别下(InnoDB 默认),InnoDB 对于行的扫描锁定都是使用此算法,但是如果查询扫描中有唯一索引会退化成只使用记录锁。
因为唯一索引能确定行数,而其他索引不能确定行数,需要使用间隙锁防止其他事务中再次添加这个索引的数据造成幻读。RR 隔离级别下,InnoDB 使用 Next-Key Lock 算法避免了幻读。
1.3 死锁
1.3.1 死锁的产生
多个事务之间,彼此持有对方需要的锁,但是又彼此之间不释放,形成了一个资源依赖的环,僵持不下,就形成了死锁。
1.3.2 死锁的预防
主要有2条吧,其实本质就是尽量不要形成资源依赖的环。
- 1 保证事务间的等待不要出现环,让各个事务都按照一定的顺序为资源加锁。
*2 如果出现了锁,就是用抢占的方式预防死锁。比如为每个事务加一个时钟,如果时间到了,就继续等或者被抢夺。
1.3.3 死锁的检测与恢复
检查
看是否出现了环。
恢复
按照最小代价的原则,选择一个事务进行回滚,打破环即可。
2 并发产生三个问题
2.1 脏读
脏读是指某个事务读取了另外一个事务没有提交的记录。
2.2 不可重复读
不可重复读是指一个事务连续两次读取某条记录,但是结果不一致。这个过程中主要是另外一个事务对某些记录进行了更新,导致了用相同条件查询时,结果是不一样的。
比如 一个表有id, name,age 三个字段。 当事务A要查询一个name = zp
的记录,在第一次读取的时候(select * from person where id = 1),可能获取到的记录是 (1,“zp”, 1)。但是在事务A准备利用相同条件查询第二次之前,事务B,操作了一条 “将 原本(1, “zp”, 1)的记录 的name改为frank的操作”,那么此时事务A再根据id = 1
查询,此时就查出 (1,“frank”, 1),两次查询的结果是不一样的。此时对于事务A来说,就是不可重复读的。
注意,不可重复读主要是另外一个事务执行了update 的操作,导致第一个事务两次查询不一致。
2.3 幻读
幻读,是指一个事务两次查询的结果不一致。跟不可重复读的区别是其他事务执行了insert操作,导致记录条数变更了。
还是以上面的例子讲解一下:
比如 一个表有id, name,age 三个字段。 当事务A要查询一个name = zp
的记录,在第一次读取的时候,可能获取到的记录是 (1,“zp”, 1)。但是在事务A准备利用相同条件查询第二次之前,事务B,执行插入操作 “ 将(3, “zp”, 3)插入表中。”,那么此时事务A再根据name = zp
查询,此时就查出 (1,“zp”, 1), (3,"zp",3) 两条记录,此时对于事务A来说,就发生了幻读。
3 事务隔离级别介绍
InnoDB 支持四种隔离级别。
读未提交、读已提交、可重复读、可串行化。
四种隔离级别能否解决三种并发问题:
下面一部分,将会讲解各种隔离级别是如何解决并发问题的。
4 四种隔离级别是如何利用锁解决并发产生的问题的?
4.1 隔离级别是怎么解决并发问题的?
总结一句话:隔离界别是靠锁来解决并发问题的。
我们先画个图,更清晰一些:
注意 可重复读 是对满足条件的所有行,每一行都加了行锁。但是行间其他新插入的行是没有加锁的,所以再插入,就产生了幻读。
图中已经从隔离级别最低到高,是如何一步步解决对应的并发问题的。
如图中所示,通过加Gap锁,可以解决幻读的问题,但是这样整个事务就进入了串行的模式,是单版本的。MySQL中另外一种常见解决幻读问题的方案是多版本并发控制的方式-MVCC.
4.2 利用MVCC 解决并发问题
4.2.1 MVCC的使用场景
虽然锁机制能够从根本上解决并发事务的可串行化的问题,但是在实际环境中数据库的事务大读多写少,如果写请求和读请求之前没有并发控制机制,那么最坏的情况也是读请求读到了已经写入的数据,这对很多应用完全是可以接受的。
在这种大前提下,数据库系统引入了另一种并发控制机制 - 多版本并发控制(Multiversion Concurrency Control)。读写操作之间的冲突就不再需要被关注,而管理和快速挑选数据的版本就成了 MVCC 需要解决的主要问题。
4.2.2 MVCC的机制
写操作:每一个写操作都会创建一个新版本的数据;
读操作:读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回;
版本删除:根据时间戳,会将版本最低,且不再使用的数据定时清除。
5 总结
本文首先讲解了锁、事务隔离级别,以及由并发引起的经典问题。然后重点分析了各个隔离级别是如何利用锁解决并发引起的三类问题的。最后讲了MVCC。
6 参考文献
mysql 共享锁 https://www.jianshu.com/p/e937830bc2de
锁算法 https://www.cnblogs.com/wade-luffy/p/9689975.html#_label1_0
MVCC与锁https://draveness.me/database-concurrency-control
7 其他
本文是mysql学习的第二篇-事务隔离级别、并发与锁,希望对你有所帮助~
如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~