《MySQL8.0.22:Lock(锁)知识总结以及源码分析》⽬录
1、关于锁的⼀些零碎知识,需要熟知
事务加锁⽅式:
两阶段锁:
整个事务分为两个阶段,前⼀个阶段加锁,后⼀个阶段为解锁。在加锁阶段,事务只能加锁,也可以操作数据,但是不能解锁,直到事务释放第⼀个锁,就进⼊了解锁阶段,此阶段事务只能解锁,也可以操作数据,不能再加锁。
两阶段协议使得事务具有⽐较⾼的并发度,因为解锁不必发⽣在事务结尾。
不过它没有解决死锁问题,因为它在加锁阶段没有顺序要求,如果两个事务分别申请了A,B锁,接着⼜申请对⽅的锁,此时进⼊死锁状态。Innodb事务隔离
在MVCC并发控制中,读操作可以分为两类:快照读和当前读。
快照读读取的是记录的可见版本(有可能是历史版本),不⽤加锁。
当前读,读取的是记录的最新版本,并且当前读返回的记录都会加上锁,保证其他事务不再会并发修改这条记录。
Read Uncommited:可以读未提交记录
Read Committed(RC):当前读操作保证对独到的记录加锁,存在幻读现象。使⽤MVCC,但是读取数据时读取⾃⾝版本和最新版本,以最新为主,可以读已提交记录,存在不可重复
Repeatable Read(RR):当前读操作保证对读到的记录加锁,同时保证对读取的范围加锁,新的满⾜查询条件的记录不能够插⼊(间隙锁),不存在幻读现象。使⽤MVCC保存两个事务操作的数据互相隔离,不存在不可重复读现象。
Serializable:MVCC并发控制退化为基于锁的并发控制。不区分快照读和当前读,所有读操作均为当前读,读加S锁,写加X锁。MVCC多版本并发控制
MVCC是⼀种多版本并发控制机制。锁机制可以控制并发操作,但是其系统开销较⼤,⽽MVCC可以在⼤多数情况下替代⾏级锁,降低系统开销。
MVCC是通过保存数据在某个时间点的快照来实现的,典型的有乐观并发控制和悲观并发控制。
InnoDB的MVCC,是通过在每⾏记录后⾯保存两个隐藏的列来实现的,这两个列,分别保存这个⾏的创建时间和删除时间,这⾥存储的并不是实际的时间值,⽽是版本号,可以理解为事务的ID。每开始⼀个新的事务,这个版本号就会⾃动递增。
对于⼏种的操作:
INSERT:为新插⼊的每⼀⾏保存当前版本号作为版本号
UPDATE:新插⼊⼀⾏记录,并且保存其创建时间为当前事务ID,同时保存当前
DELETE:为删除的每⼀⾏保存当前版本号作为版本号
SELECT:
InnoDB只会查版本号⼩于等于事务系统版本号
⾏的删除版本要么未定义要么⼤于当前事务版本号,这样可以确保事务读取的⾏,在事务开始删除前未被删除
事实上,在读取满⾜上述两个条件的⾏时,InnoDB还会进⾏⼆次检查。
活跃事务列表:RC隔离级别下,在语句开始时从全局事务表中获取活跃(未提交)事务构造Read View,RR隔离级别下,事务开始时从全局事务表获取活跃事务构造Read View:
1、取当前⾏的修改事务ID,和Read View中的事务ID做⽐较,若⼩于最⼩的ID或⼩于最⼤ID但不在列表中,转2步骤。若是⼤于最⼤ID,转3
2、若进⼊此步骤,可说明,最后更新当前⾏的事务,在构造Read View时已经提交,返回当前⾏数据
3、若进⼊此步骤,可说明,最后更新当前⾏的事务,在构造Read View时还未创建或者还未提交,取undo log中记录的事务ID,重新进⼊步骤1.
根据上⾯策略,在读取数据的时候,InnoDB⼏乎不⽤获得任何锁,每个查询都能通过版本查询,只获得⾃⼰需要的数据版本,从⽽⼤⼤提⾼了系统并发度。
缺点是:每⾏记录都需要额外的存储空间,更多的⾏检查⼯作,额外的维护⼯作。
⼀般我们认为MVCC有⼏个特点:
每个数据都存在⼀个版本,每次数据更新时都更新该版本
修改时copy出当前版本修改,各个事务之间没有⼲扰
保存时⽐较版本号,如果成功,则覆盖原记录;失败则rollback
看上去保存是根据版本号决定是否成功,有点乐观锁意味,但是Innodb实现⽅式是:
事务以排他锁的形式修改原始数据
把修改前的数据存放于undo log,通过回滚指针与主数据关联
修改成功后啥都不做,失败则恢复undo log中的数据。
innodb没有实现MVCC核⼼的多版本共存,undo log内容只是串⾏化的结果,记录了多个事务的过程,不属于多版本共存。当事务影响到多⾏数据,理想的MVCC⽆能为⼒。
如:事务1执⾏理想MVCC,修改row1成功,修改row2失败,此时需要回滚row1,但是由于row1没有被锁定,其数据可能⼜被事务2修改,如果此时回滚row1内容,会破坏事务2的修改结果,导致事务2违反ACID。
理想的MVCC难以实现的根本原因在于企图通过乐观锁代替⼆阶段提交。修改两⾏数据,但为了保证其⼀致性,与修改两个分布式系统数据并⽆区别,⽽⼆阶段提交是⽬前这种场景保证⼀致性的唯⼀⼿段。⼆阶段提交的本质是锁定,乐观锁的本质是消除锁定,⼆者⽭盾。innodb只是借了MVCC名字,提供了读的⾮阻塞。
采⽤MVCC⽅式,读-写操作彼此并不冲突,性能更⾼;如果采⽤加锁⽅式,读-写操作彼此需要排队执⾏,从⽽影响性能。⼀般情况下,我们更愿意使⽤MVCC来解决读-写操作并发执⾏的问题,但是在⼀些特殊业务场景中,要求必须采⽤加锁的⽅式执⾏。
常⽤语句与锁的关系
对读取的记录加S锁:
对读取的记录加X锁:
delete:
对⼀条语句执⾏delete,先在B+树中定位到这条记录位置,然后获取这条记录的X锁,最后执⾏delete mark操作。
update:
如果未修改该记录键值并且被更新的列所占⽤的存储空间在修改前后未发⽣变化,则现在B+树定位到这条记录的位置,然后再获取记录的X锁,最后在原记录的位置进⾏修改操作。
如果为修改该记录的键值并且⾄少有⼀个被更新的列占⽤的存储空间在修改后发⽣变化,则先在B+树中定位到这条记录的位置,然后获取记录的X锁,然后将原记录删除,再重新插⼊⼀个新的记录。
如果修改了该记录的键值,则相当于在原记录上执⾏delete操作之后再来⼀次insert操作。
insert:
新插⼊的⼀条记录收到隐式锁保护,不需要在内存中为其⽣成对应的锁结构。
意向锁
为了允许⾏锁和表锁共存,实现多粒度锁机制。InnoDB还有两种内部使⽤的意向锁,两种意向锁都是表锁。
意向共享锁(IS):事务打算给数据⾏加⾏共享锁,事务在给⼀个数据⾏加共享锁前必须先取得该表的IS锁
意向排他锁(IX):事务打算给数据⾏加排他锁,事务在给⼀个数据⾏加排他锁前必须先取得该表的IX锁。
意向锁仅仅⽤于表锁和⾏锁的共存使⽤。它们的提出仅仅是为了在之后加表级S锁或者X锁是可以快速判断表中的记录是否被上锁,以避免⽤遍历的⽅式来查看表中有没有上锁的记录。
需要注意的三点:
1、意向锁是表级锁,但是却表⽰事务正在读或写某⼀⾏记录
2、意向锁之间不会冲突,因为意向锁仅仅代表对某⾏记录进⾏操作,在加⾏锁的时候会判断是否冲突
3、意向锁是InnoDB⾃动加的,不需要⽤户⼲预。
⾏级锁
Record Lock:就是普通的⾏锁,官⽅名称:LOCK_REC_NOT_GAP,⽤来锁住在聚集索引上的⼀条⾏记录
Gap Lock:⽤来在可重复读隔离级别下解决幻读现象。已知幻读还有⼀种⽅法解决:MVCC,还⼀种就
是加锁。但是在使⽤加锁⽅案时有个问题,事务在第⼀次执⾏读取操作时,“幻影记录”还没有插⼊,所以我们⽆法给“幻影记录”加上Record Lock。InnoDB提出了Gap锁,官⽅名称:LOCK_GAP,若⼀条记录的numberl列为8,前⼀⾏记录number列为3,我们在这个记录上加上gap锁,意味着不允许别的事务在number值为(3,8)区间插⼊记录。只有gap锁的事务提交后将gap锁释放掉后,其他事务才能继续插⼊。
注意:gap锁只是⽤来防⽌插⼊幻影记录的,共享gap和独占gap起到作⽤相同。对⼀条记录加了gap锁不会限制其他事务对这条记录加Record Lock或者继续加gap锁。另外对于向限制记录后⾯的区间的话,可以使⽤Supremum表⽰该页⾯中最⼤记录。
Next-Key Lock:当我们既想锁住某条记录,⼜想阻⽌其他事务在该记录前⾯的间隙插⼊新记录,使⽤该锁。官⽅名
称:LOCK_ORDINARY,本质上就是上⾯两种锁的结合。
Insert Intention Lock:⼀个事务在插⼊⼀条你记录时需要判断该区间点上是否存在gap锁或Next-Key Lock,如果有的话,插⼊就需要阻塞。设计者规定,事务在等待时也需要在内存中⽣成⼀个锁结构,表明有个事务想在某个间隙中插⼊记录,但是处于等待状态。这种状态锁称为Insert Intention Lock,官⽅名称:LOCK_INSERT_INTENTION,也可以称为插⼊意向锁。
2、锁的内存结构以及⼀些解释
⼀个事务对多条记录加锁时不⼀定就要创建多个锁结构。如果符合下⾯条件的记录的锁可以放到⼀个锁结构中:在同⼀个事务中进⾏加锁操作
被加锁的记录在同⼀个页⾯中
加锁的类型是⼀样的
等待状态是⼀样的
type_mode是⼀个32位⽐特的数,被分为lock_mode、lock_type、rec_lock_type三个部分。
低4位表⽰:lock_mode,锁的模式
0:表⽰IS锁
1:表⽰IX锁
2:表⽰S锁
3:表⽰X锁
4:表⽰AI锁,就是auto-inc,⾃增锁
第5~8位表⽰:lock_type,锁的类型
LOCK_TABLE:第5位为1,表⽰表级锁
LOCK_REC:第6位为1,表⽰⾏级锁
其余⾼位表⽰:rec_lock_type,表⽰⾏锁的具体类型,只有lock_type的值为LOCK_REC时,才会出现细分LOCK_ORDINARY:为0,表⽰next-key锁
LOCK_GAP:为512,即当第10位设置为1时,表⽰gap锁
LOCK_REC_NOT_GAP:为1024,当第11位设置为1,表⽰正常记录锁
LOCK_INSERT_INTENTION:为2048,当第12位设置为1时,表⽰插⼊意向锁
LOCK_WAIT:为256,当第9位设置为1时,表⽰is_waiting为false,表明当前事务获取锁成功。
⼀堆⽐特位
其他信息:涉及了⼀些哈希表和链表
更加细节的结构可以看这⼀张图:
3、InnoDB的锁代码实现
锁系统结构lock_sys_t
/** The lock system struct */
struct lock_sys_t {
/** The latches protecting queues of record and table locks */
locksys::Latches latches;
/** The hash table of the record (LOCK_REC) locks, except for predicate
(LOCK_PREDICATE) and predicate page (LOCK_PRDT_PAGE) locks */
hash_table_t *rec_hash;
/** The hash table of predicate (LOCK_PREDICATE) locks */
hash_table_t *prdt_hash;
/
** The hash table of the predicate page (LOCK_PRD_PAGE) locks */
hash_table_t *prdt_page_hash;
/** Padding to avoid false sharing of wait_mutex field */
char pad2[ut::INNODB_CACHE_LINE_SIZE];
/** The mutex protecting the next two fields */
Lock_mutex wait_mutex;
/** Array of user threads suspended while waiting for locks within InnoDB.
mysql下载32位
Protected by the lock_sys->wait_mutex. */
srv_slot_t *waiting_threads;
/** The highest slot ever used in the waiting_threads array.
Protected by lock_sys->wait_mutex. */
srv_slot_t *last_slot;
/** TRUE if rollback of all recovered transactions is complete.
Protected by exclusive global lock_sys latch. */
bool rollback_complete;
/** Max lock wait time observed, for innodb_row_lock_time_max reporting. */
ulint n_lock_max_wait_time;
/** Set to the event that is created in the lock wait monitor thread. A value
of 0 means the thread is not active */
os_event_t timeout_event;
#ifdef UNIV_DEBUG
/** Lock timestamp counter, used to assign lock->m_seq on creation. */
std::atomic<uint64_t> m_seq;
#endif/* UNIV_DEBUG */
};
lock_t 、lock_rec_t 、lock_table_t
⽆论是⾏锁还是表锁都使⽤lock_t结构保存,其中⽤⼀个union来分别保存⾏锁和表锁不同的数据,分别为lock_table_t和lock_rec_t