用户登录
用户注册

分享至

mysql事务和锁InnoDB

  • 作者: 弱水三千_只留一瓢
  • 来源: 51数据库
  • 2022-04-05
背景

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的记录加写锁 (走主键索引)。

 

软件
前端设计
程序设计
Java相关