MySQL/InnoDB的加锁分析,一直是一个比较困难的话题。我在工作过程中,经常会有同事咨询这方面的问题。同时,微博上也经常会收到MySQL锁相关的私信,让我帮助解决一些死锁的问题。
本文,准备就MySQL/InnoDB的加锁问题,展开较为深入的分析与讨论,主要是介绍一种思路,运用此思路,拿到任何一条SQL语句,就能完整的分析出这条语句会加什么锁?会有什么样的使用风险?甚至是分析线上的一个死锁场景,了解死锁产生的原因。
注:MySQL是一个支持插件式存储引擎的数据库系统。本文下面的所有介绍,都是基于InnoDB存储引擎,其他引擎的表现,会有较大的区别。
MVCC:SnapshotReadvsCurrentReadMySQLInnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC(Multi-VersionConcurrencyControl)(注:与MVCC相对的,是基于锁的并发控制,Lock-BasedConcurrencyControl)。MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多些少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。
在MVCC并发控制中,读操作可以分成两类:快照读(snapshotread)与当前读(currentread)。快照读,读取的是记录的可见版本(有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQLInnoDB为例:
快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
select*fromtablewhere?;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select*fromtablewhere?lockinsharemode;select*fromtablewhere?forupdate;insertintotablevalues(…);updatetableset?where?;deletefromtablewhere?;
所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁(共享锁)外,其他的操作,都加的是X锁(排它锁)。
为什么将插入/更新/删除操作,都归为当前读?
当UpdateSQL被发给MySQL后,MySQLServer会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁(currentread)。待MySQLServer收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发UniqueKey的冲突检查,也会进行一个当前读。
注:根据上图的交互,针对一条当前读的SQL语句,InnoDB与MySQLServer的交互,是一条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给MySQLServer,做一些DML操作;然后在读取下一条加锁,直至读取完毕。
ClusterIndex:聚簇索引InnoDB存储引擎的数据组织方式,是聚簇索引表:完整的记录,存储在主键索引中,通过主键索引,就可以获取记录所有的列。关于聚簇索引表的组织方式,可以参考MySQL的官方文档:ClusteredandSecondaryIndexes。本文假设读者对这个,已经有了一定的认识,就不再做具体的介绍。接下来的部分,主键索引/聚簇索引两个名称,会有一些混用,望读者知晓。
PL:Two-PhaseLocking传统RDBMS加锁的一个原则,就是PL(二阶段锁):Two-PhaseLocking。相对而言,PL比较容易理解,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。下面,仍旧以MySQL为例,来简单看看PL在MySQL中的实现。
PL就是将加锁/解锁分为两个完全不相交的阶段。加锁阶段:只加锁,不放锁。解锁阶段:只放锁,不加锁。
IsolationLevel隔离级别:IsolationLevel,也是RDBMS的一个关键特性。相信对数据库有所了解的朋友,对于4种隔离级别:ReadUn