锁的类型
根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类。
全局锁
全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
全局锁的典型使用场景是,做全库逻辑备份。
官方自带的逻辑备份工具是mysqldump。当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
single-transaction方法只适用于所有的表使用事务引擎的库。
MyISAM不支持事务,逻辑备份只能锁整个数据库实例。
表级锁分为两类
表锁
表锁的语法是 lock tables … read/write。在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。
元数据锁(meta data lock,MDL)
MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是,保证读写的正确性。在MySQL 5.5版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
MDL锁是系统默认会加的,但却是你不能忽略的一个机制。
经常看到有人掉到这个坑里:给一个小表加个字段,导致整个库挂了。
MDL会直到事务提交才释放,在做表结构变更的时候,一定要小心不要导致锁住线上查询和更新。
如何安全地给小表加字段?
首先要解决长事务,事务不提交,就会一直占着MDL锁。虽然5.6版本支持了Online DDL,具体过程如下:
- 拿MDL写锁
- 降级成MDL读锁
- 真正做DDL
- 升级成MDL写锁
- 释放MDL锁
1、2、4、5如果没有锁冲突,执行时间非常短。第3步占用了DDL绝大部分时间,这期间这个表可以正常读写数据,是因此称为“online ”。解决长事务就是为了避免出现锁冲突。
行锁
**行锁就是针对数据表中行记录的锁。**这很好理解,比如事务A更新了一行,而这时候事务B也要更新同一行,则必须等事务A的操作完成后才能进行更新。
两阶段锁协议:在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。
死锁和死锁检查
**死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。**若无外力作用,事务都将无法推进下去。
解决死锁的方法:
等待超时
即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。这个超时时间可以通过参数innodb_lock_wait_timeout来设置,默认值是50s。等待时间太长,业务无法接受,太短容易造成误伤把正常事务也干掉了。
主动死锁检测
发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑(默认开启)。当前数据库还都普遍采用 wait-for graph(等待图)的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。
事务
事务是并发控制单位,是用户定义的一个操作序列,这些操作要么都做,要么都不做,是一个不可分割的工作单位。
ACID特性
事务需要满足ACID四个特性:
A (Atomicity) 原子性。
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。
C (Consistency) 一致性。
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
I (Isolation) 隔离性
事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,
D (Durability) 持久性
事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。
SQL标准中的事务隔离级别

READ UNCOMMITTED(读未提交)
该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。
1 | ##准备 |
READ COMMITTED(读提交)
一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题。在同一个事务里面,读取会不一致。
1 | ##准备 |
Mysql使用了多版本并发控制(MVCC) 实现了RC级别的事务隔离。
REPEATABLE READ(可重复读)
REPEATABLE READ解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是可重复读隔离级别还是无法解决另外一个幻读的问题。
幻读指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行( Phantom row)。
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
可重复读是 MySQL的默认事务隔离级别。
为了解决当前读中的幻读问题,InnoDB事务使用了Next-Key锁。
sql标准允许在RR级别下出现幻读的问题,但是InnoDB事务使用了Next-Key锁避免了幻读的问题。
SERIALIZABLE(序列化)
在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。
事务隔离级别参考
Innodb 中 RR 隔离级别能否防止幻读?
Innodb中的事务隔离级别和锁的关系
MySQL 事务隔离级别和锁
高性能mysql
行锁的三种算法
InnoDB 存储引擎使用三种行锁的算法用来满足相关事务隔离级别的要求。
Record Locks
该锁为索引记录上的锁,如果表中没有定义索引,InnoDB 会默认为该表创建一个隐藏的聚簇索引,并使用该索引锁定记录。
Gap Locks
该锁会锁定一个范围,但是不括记录本身。
Next-key Locks
该锁就是 Record Locks 和 Gap Locks 的组合,即锁定一个范围并且锁定该记录本身。
在Next-KeyLock算法下, InnoDB对于行的查询都是采用这种锁定算法。例如一个索引有10,11,
13和20这四个值,那么该索引可能被 Next-Key Locking的区间为:
(-∞ 10] ,(10,11],(11,13],(13,20],(20,+∞)
采用 Next-Key Lock的锁定技术称为 Next-Key Locking。除了 next-key locking,还
有 previous- key locking技术。同样上述的索引10、11、13和20,若采用 previous-key
locking技术,那么可锁定的区间为:(-∞ 10) ,[10,11),[11,13),[3,20),[20,+∞)
若事务T1已经通过next- key locking锁定了如下范围:
(10,11]、(11,13]
此时其他事物无法在10-13中插入数据,比如无法插入12
行锁防止别的事务修改或删除,GAP锁防止别的事务新增,行锁和GAP锁结合形成的的Next-Key锁共同解决了RR级别在写数据时的幻读问题。
详细见 Mysql技术内幕 InnoDB引擎
事务的实现
多版本并发控制(MVCC)
MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。
每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。
InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。
而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。
数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id。

图中的三个虚线箭头,就是undo log;而V1、V2、V3并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的。比如,需要V2的时候,就是通过V4依次执行U3、U2算出来。
按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。
InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的就是,启动了但还没提交。
数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。

对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能:
- 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
- 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
- 如果落在黄色部分,那就包括两种情况
- 若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;
- 若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。
更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。(不用于上面说的水位,只能获取到数据的最后一个版本,不做往前计算的逻辑)
可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
- 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
- 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。(每次获取到最新的版本,就可以看到已提交的数据)
参考: mysql45讲 第八讲
快照读与当前读
在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,不是数据库最新的数据。这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式,叫当前读 (current read)。
1. 快照读
当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。
快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。
之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。
2. 当前读
对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。
在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。
redo
redo log用来实现事务的持久性。其由两部分组成:
- redo log buffer,其是易失的
- redo log file,其是持久的
当事务提交时,必须先将该事务的所有重做日志写入到重做日志文件进行持久化,该事务的COMMIT操作完成才算完成。
为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后, InnodB存储引擎都需要调用一次 fsync操作。(可以设置异步刷盘,等主线程刷盘,但是会造成事务丢失,不建议这样子用!)
redo log用来保证事务的持久性, undo log用来帮助事务回滚及MVCC的功能。
log block
在 InnoDB存储引擎中,redo log都是以512字节进行存储的。这意味着redo log buffer、redo log file都是以块( block)的方式进行保存的,称之为重做日志块(redolog block),每块的大小为512字节。
若一个页中产生的重做日志数量大于512字节,那么需要分割为多个重做日志块进
行存储。
由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要 doublewrite技术。
log buffer根据一定的规则将内存中的 log block刷新到磁盘:
- 事务提交时
- 当 log buffer中有一半的内存空间已经被使用时
- log checkpoint时
对于 log block的写人追加( append)在 redo log file的最后部分,当一个 redo logfle被写满时,会接着写入下一个 redo log file,其使用方式为round-robin。
LSN
LSN是 Log Sequence Number的缩写,其代表的是日志序列号。在 InnoDB存储引擎中,LSN占用8字节,并且单调递增。LSN表示的含义有:
- 重做日志写人的总量
- checkpoint的位置
- 页的版本
LSN不仅记录在重做日志中,还存在于每个页中。在每个页的头部,有一个值记录了该页的LSN。在页中,LSN表示该页最后刷新时LSN的大小。因为重做日志记录的是每个页的日志,因此页中的LSN用来判断页是否需要进行恢复操作。
恢复
由于 checkpoint表示已经刷新到磁盘页上的LSN,因此在恢复过程中仅需恢复checkpoint开始的日志部分。对于图中的例子,当数据库在 checkpoint的LSN为10000时发生宕机,恢复操作仅恢复LSN10000~13000范围内的日志。

undo
undo存放在数据库内部的一个特殊段( segment)中,这个段称为undo段( undo segment)。undo段位于共享表空间内。
undo log会产生 redo log,也就是 undo log的产生会伴随着 redo log的产生,这是因为 undo log也需要持久性的保护。
在 InnoDB存储引擎中, undo log分为:
Insert undo log
insert undo log是指在 insert操作中产生的 undo log。因为 Insert操作的记录,只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),故该 undo log可以在事务提交后直接删除。不需要进行 purge操作。
update undo log
update undo log记录的是对 delete和 update操作产生的 undo log。该 undo log可能需要提供MvcC机制,因此不能在事务提交时就进行删除。提交时放 undo log链表等待purge线程进行最后的删除。
delete操作并不直接删除记录,而只是将记录标记为已删除,也就是将记录的 delete flag设置为1。而记录最终的删除是在 purge操作中完成的。
update主键的操作其实分两步完成。首先将原主键记录标记为已删除的undo log,之后插入一条
新的记录的undo log。
purge
对数据的真正删除这行记录的操作其实被“延时”了,最终在 purge操作中完成。