欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

mysql事务和锁InnoDB

程序员文章站 2022-03-26 09:47:31
背景 mysql/innodb的加锁分析,一直是一个比较困难的话题。我在工作过程中,经常会有同事咨询这方面的问题。同时,微博上也经常会收到mysql锁相关的私信,让我帮助解决一些...
背景

mysql/innodb的加锁分析,一直是一个比较困难的话题。我在工作过程中,经常会有同事咨询这方面的问题。同时,微博上也经常会收到mysql锁相关的私信,让我帮助解决一些死锁的问题。本文,准备就mysql/innodb的加锁问题,展开较为深入的分析与讨论,主要是介绍一种思路,运用此思路,拿到任何一条sql语句,都能完整的分析出这条语句会加什么锁?会有什么样的使用风险?甚至是分析线上的一个死锁场景,了解死锁产生的原因。

注:mysql是一个支持插件式存储引擎的。本文下面的所有介绍,都是基于innodb存储引擎,其他引擎的表现,会有较大的区别。

mvcc:snapshot read vs current read

mysql innodb存储引擎,实现的是基于多版本的并发控制协议——mvcc (multi-version concurrency control) (注:与mvcc相对的,是基于锁的并发控制,lock-based concurrency control)。mvcc最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的oltp应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的rdbms,都支持了mvcc。

在mvcc并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

在一个支持mvcc并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以mysql innodb为例:

快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析) select * from table where ?;

当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。 select * from table where ? lock in share mode; select * from table where ? for update; insert into table values (…); update table set ? where ?; delete from table where ?;

所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加s锁 (共享锁)外,其他的操作,都加的是x锁 (排它锁)。

为什么将 插入/更新/删除 操作,都归为当前读?可以看看下面这个 更新 操作,在数据库中的执行流程:

从图中,可以看到,一个update操作的具体流程。当update sql被发给mysql后,mysql server会根据where条件,读取第一条满足条件的记录,然后innodb引擎会将第一条记录返回,并加锁 (current read)。待mysql server收到这条加锁的记录之后,会再发起一个update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,update操作内部,就包含了一个当前读。同理,delete操作也一样。insert操作会稍微有些不同,简单来说,就是insert操作可能会触发unique key的冲突检查,也会进行一个当前读。

注:根据上图的交互,针对一条当前读的sql语句,innodb与mysql server的交互,是一条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给mysql server,做一些dml操作;然后在读取下一条加锁,直至读取完毕。

cluster index:聚簇索引

innodb存储引擎的数据组织方式,是聚簇索引表:完整的记录,存储在主键索引中,通过主键索引,就可以获取记录所有的列。关于聚簇索引表的组织方式,可以参考mysql的官方文档:clustered and secondary indexes 。本文假设读者对这个,已经有了一定的认识,就不再做具体的介绍。接下来的部分,主键索引/聚簇索引 两个名称,会有一些混用,望读者知晓。

2pl:two-phase locking

传统rdbms加锁的一个原则,就是2pl (二阶段锁):two-phase locking。相对而言,2pl比较容易理解,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。下面,仍旧以mysql为例,来简单看看2pl在mysql中的实现。

从上图可以看出,2pl就是将加锁/解锁分为两个完全不相交的阶段。加锁阶段:只加锁,不放锁。解锁阶段:只放锁,不加锁。

isolation level

隔离级别:isolation level,也是rdbms的一个关键特性。相信对数据库有所了解的朋友,对于4种隔离级别:read uncommited,read committed,repeatable read,serializable,都有了深入的认识。本文不打算讨论数据库理论中,是如何定义这4种隔离级别的含义的,而是跟大家介绍一下mysql/innodb是如何定义这4种隔离级别的。

mysql/innodb定义的4种隔离级别:

read uncommited

可以读取未提交记录。此隔离级别,不会使用,忽略。

read committed (rc)

快照读忽略,本文不考虑。

针对当前读,rc隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。

repeatable read (rr)

快照读忽略,本文不考虑。

针对当前读,rr隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。

serializable

从mvcc并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (s锁),写加写锁 (x锁)。

serializable隔离级别下,读写冲突,因此并发度急剧下降,在mysql/innodb下不建议使用。

一条简单sql的加锁实现分析

在介绍完一些背景知识之后,本文接下来将选择几个有代表性的例子,来详细分析mysql的加锁处理。当然,还是从最简单的例子说起。经常有朋友发给我一个sql,然后问我,这个sql加什么锁?就如同下面两条简单的sql,他们加什么锁?

sql1:select * from t1 where id = 10; sql2:delete from t1 where id = 10;

针对这个问题,该怎么回答?我能想象到的一个答案是:

sql1:不加锁。因为mysql是使用多版本并发控制的,读不加锁。 sql2:对id = 10的记录加写锁 (走主键索引)。