业务研发团队 唐蕴梦
什么是锁
锁是计算机协调多个进程或线程并发访问某一资源的机制。
Mysql锁
- 行锁
开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 - 页锁
开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。 - 表锁
开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。
事务
事务是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。
事务的四个特性条件
- 原子性:一组事务,要么全部成功;要么撤回。
- 一致性 :满足模式锁指定的约束,比如银行转账前后总金额应该不变。事务结束时,所有的内部数据结构(如B树索引)也都必须是正确的。
- 隔离性:事务独立运行。一个事务所做的修改在最终提交之前,对其它事务是不可见的。事务的100%隔离,需要牺牲速度。
- 持久性:软、硬件崩溃后,InnoDB数据表驱动会利用日志文件重构修改,或者通过数据库备份和恢复来保证。
在默认情况下,MySQL每执行一条SQL语句,都是一个单独的事务。
事务并发的问题
- 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。
- 不可重复读(Non-Repeatable Reads):一个事务读取某些数据,在它结束读取之前,另一个事务可能完成了对数据行的更改。当第一个事务试图再次执行同一个查询,服务器就会返回不同的结果。
- 幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
事务隔离级别
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
mysql默认的事务隔离级别为repeatable-read
- 读未提交 :事务可以读取到其他事务未提交的数据,此时若A事务读取到B事务未提交的修改,后B回滚就会产生脏读。
- 不可重复读:事务只能读取到其他事务提交的数据,不会产生脏读,但若事务B提交在A的两次查询间就会产生不可重复读。
- 可重复读:可重复读的隔离级别下使用了MVCC机制,A事务中读取的是记录的快照版本,而非最新版本,B事务的更新是创建了一个新版本来更新,不同事务的读和写是分离的
- 串行化:mysql中事务隔离级别为serializable时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低。
事务加锁方式
- 一次性锁协议,事务开始时,即一次性申请所有的锁,之后不会再申请任何锁,如果其中某个锁不可用,则整个申请就不成功,事务就不会执行,在事务尾端,一次性释放所有的锁。一次性锁协议不会产生死锁的问题,但事务的并发度不高。
- 两阶段锁协议,整个事务分为两个阶段,前一个阶段为加锁,后一个阶段为解锁。在加锁阶段,事务只能加锁,也可以操作数据,但不能解锁,直到事务释放第一个锁,就进入解锁阶段,此过程中事务只能解锁,也可以操作数据,不能再加锁。两阶段锁协议使得事务具有较高的并发度,因为解锁不必发生在事务结尾。它的不足是没有解决死锁的问题,因为它在加锁阶段没有顺序要求。如两个事务分别申请了A, B锁,接着又申请对方的锁,此时进入死锁状态。
Innodb的事务隔离
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
MySQL/InnoDB定义的4种隔离级别:
-
Read Uncommited
可以读取未提交记录。
- Read Committed (RC)
当前读操作RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。使用MVCC,但读取数据时读取自身版本和最新版本,以最新为主,可以读已提交记录,存在不可重复读现象。 - Repeatable Read (RR)
当前读操作RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。使用MVCC保存两个事物操作的数据互相隔离,不存在不可重复读现象。 - Serializable
从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。
Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。
InnoDB的MVCC多版本并发控制
MVCC是一种多版本并发控制机制。锁机制可以控制并发操作,但是其系统开销较大,而MVCC可以在大多数情况下代替行级锁,使用MVCC,能降低其系统开销。
MVCC是通过保存数据在某个时间点的快照来实现的。 不同存储引擎的MVCC、不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制。
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID),没开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID。IN
- INSERT
InnoDB为新插入的每一行保存当前系统版本号作为版本号. - UPDATE
InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间。 - DELETE
InnoDB会为删除的每一行保存当前系统的版本号(事务的ID)作为删除标识 -
SELECT
InnoDB会根据以下两个条件检查每行记录,需要同时满足以下两个条件:- InnoDB只会查找版本早于当前事务版本的数据行(也就是行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
- 行的删除版本要么未定义要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除。
**注意:
在SELECT时,只满足上述两个条件也是不能达到快照读的要求的,比如在RR的隔离级别下会有如下情况
启动1号事务、启动2号事务、1号事务更新x行并提交事务(此时x行的修改版本号为1,删除版本号为未定义)、2号事务读取x行
安装如上步骤,如果只满足上述两个条件的话,显然2号事务时可以读取到1号事务所做的更新(x行修改版本号为1满足小于2,删除版本号为未定义满足事务开始之前未删除),显然是不足够满足快照读的要求**
事实上,在读取到满足上述两个条件的行时,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几乎不用获得任何锁,每个查询都通过版本检查,只获得自己需要的数据版本,从而大大提高了系统的并发度。 这种策略的缺点是,每行记录都需要额外的存储空间,更多的行检查工作和一些额外的维护工作。
上述更新前建立undo log,根据各种策略读取时非阻塞就是MVCC,undo log中的行就是MVCC中的多版本,这个可能与我们所理解的MVCC有较大的出入,一般我们认为MVCC有下面几个特点:
- 每行数据都存在一个版本,每次数据更新时都更新该版本
- 修改时Copy出当前版本随意修改,各个事务之间无干扰
- 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道,而Innodb的实现方式是:
- 事务以排他锁的形式修改原始数据
- 把修改前的数据存放于undo log,通过回滚指针与主数据关联
- 修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)
二者最本质的区别是,当修改数据时是否要排他锁定,如果锁定了还算不算是MVCC?
Innodb的实现真算不上MVCC,因为并没有实现核心的多版本共存,undo log中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。但理想的MVCC是难以实现的,当事务仅修改一行记录使用理想的MVCC模式是没有问题的,可以通过比较版本号进行回滚;但当事务影响到多行数据时,理想的MVCC据无能为力了。
比如,如果Transaciton1执行理想的MVCC,修改Row1成功,而修改Row2失败,此时需要回滚Row1,但因为Row1没有被锁定,其数据可能又被Transaction2所修改,如果此时回滚Row1的内容,则会破坏Transaction2的修改结果,导致Transaction2违反ACID。
理想MVCC难以实现的根本原因在于企图通过乐观锁代替二段提交。修改两行数据,但为了保证其一致性,与修改两个分布式系统中的数据并无区别,而二提交是目前这种场景保证一致性的唯一手段。二段提交的本质是锁定,乐观锁的本质是消除锁定,二者矛盾,故理想的MVCC难以真正在实际中被应用,Innodb只是借了MVCC这个名字,提供了读的非阻塞而已。
Innodb事务锁
锁模式
- 共享锁(S):又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
- 排他锁(X):又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
- 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
意向锁仅仅用于表锁和行锁的共存使用。如果我们的操作仅仅涉及行锁,那么意向锁不会对我们的操作产生任何影响。在任一操作给表A的一行记录加锁前,首先要给该表加意向锁,如果获得了意向锁,然后才会加行锁,并在加行锁时判断是否冲突。如果现在有一个操作要获得表A的表锁,由于意向锁的存在,表锁获取会失败(如果没有意向锁的存在,加表锁之前可能要遍历整个聚簇索引,判断是否有行锁存在,如果没有行锁才能加表锁)。
同理,如果某一操作已经获得了表A的表锁,那么另一操作获得行锁之前,首先会检查是否可以获得意向锁,并在获得意向锁失败后,等待表锁操作的完成。也就是说:1.意向锁是表级锁,但是却表示事务正在读或写某一行记录;2.意向锁之间不会冲突, 因为意向锁仅仅代表要对某行记录进行操作,在加行锁时,会判断是否冲突;3.意向锁是InnoDB自动加的,不需用户干预。
锁类型
- 间隙锁(Gap Lock),只锁间隙。表现为锁住一个区间(注意这里的区间都是开区间,也就是不包括边界值)。
- 记录锁(Record Lock),只锁记录。表现为仅仅锁着单独的一行记录。
- Next-Key锁(源码中称为Ordinary Lock),同时锁住记录和间隙。从实现的角度为record lock+gap lock,而且两种锁有可能只成功一个,所以next-key是半开半闭区间,且是下界开,上界闭。一张表中的next-key锁包括:(负无穷大,最小的第一条记录],(记录之间],(最大的一条记录,正无穷大)。
- 插入意图锁(Insert Intention Lock),插入操作时使用的锁。在代码中,插入意图锁实际上是Gap锁上加了一个LOCK_INSERT_INTENTION的标记。也就是说insert语句会对插入的行加一个X记录锁,但是在插入这个行的过程之前,会设置一个Insert intention的Gap锁,叫做Insert intention锁。
- 乐观锁 : 总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
- 悲观锁: 总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁。
一致性非锁定读
InnoDB使用MVCC来实现一致性非锁定读,在read-committed和repeatable-read两种事务隔离级别下使用,且效果不同,具体如下。
read-committed
在读已提交的隔离级别下,事务在一致性非锁定读始终读取当前最新的数据快照,即当其他事务提交更新后快照更新也会读取最新的,也就是出现不可重复读。
repeatable-read
在可重复读的隔离级别下,事务始终读取事务开始时的快照版本。
一致性锁定读
一致性锁定读有两种实现方式,一种是加X锁,一种是加S锁
select ... for update 显示的使用加X锁的方式读取
select ... lock in share mode 显示的使用加S锁的方式读取
自增长与锁
innodb_autoinc_lock_mode有3种配置模式:0、1、2,
- 0:涉及auto-increment列的插入语句加的表级AUTO-INC锁,只有插入执行结束后才会释放锁,即事务在进行插入时获取自增长值时先加锁,后插入,插入完释放
- 1:对于可以事先确定插入行数的语句(包括单行和多行插入),使用互斥量操作自增值,分配连续的确定的auto-increment值,对于插入行数不确定的插入语句仍使用表级AUTO-INC锁。这种模式下,事务回滚,auto-increment值不会回滚,换句话说,自增列内容会不连续。
- 2:对于所有的插入操作使用互斥量操作自增值,来一个插入分配一个auto-increment值,此时一个批量插入的自增长值就可能不连续,且在sql语句级的主从同步可能会出现问题
锁升级
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
InnoDB目前处理死锁的方法是:将持有最少行级排它锁的事务回滚。如果是因为死锁引起的回滚,可以考虑在应用程序中重新执行。
在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。
死锁
通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。介绍几种避免死锁的常用方法。
- (1)在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。
- (2)在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。比如两个会话读取前十个用户的信息,每次读取一个,那么我们可以规定他们从第一个用户开始读,而不是倒序,这样不会死锁。
- (3)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。 (4) 选择合理的事务大小,小事务发生锁冲突的几率也更小;
如果出现死锁,可以用SHOW INNODB STATUS命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。
死锁的发生与否,并不在于事务中有多少条SQL语句,死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。而使用本文上面提到的,分析MySQL每条SQL语句的加锁规则,分析出每条语句的加锁顺序,然后检查多个并发SQL间是否存在以相反的顺序加锁的情况,就可以分析出各种潜在的死锁情况,也可以分析出线上死锁发生的原因。
问题:
按索引项来加锁的话,不同索引相同行,会不会同时获得不同的锁却操作同一行
- 聚簇索引也会加锁,也就是主键会加锁,这样就防止并发修改了
自增锁,是语句级的锁,如果当前事务先获取锁,却后执行完,在从库按语句复制的话,会不会出现ID不一致
InnoDB的锁实现
lock0types.h 事务锁系统的类型定义,包含了 lock_mode定义
lock0priv.ic 锁模块内部的一些方法,被用于除了lock0lock.cc的三个文件里,
lock_get_type_low 获取锁是表锁还是行锁
lock_clust_rec_some_has_impl 检查一行数据上是否有隐示的x锁
lock_rec_get_n_bits 获取一个记录锁的锁位图的bit数目
lock_rec_set_nth_bit 设置第n个记录锁bit位为true
lock_rec_get_next_on_page 获取当前page上的下一个记录锁
lock_rec_get_next_on_page_const
lock_rec_get_first_on_page_addr 获取当前page上第一个记录锁
lock_rec_get_first_on_page
lock_rec_get_next 返回当前记录上的下一个显示锁请求的锁
lock_rec_get_next_const
lock_rec_get_first 获取当前记录上的第一个显示锁请求的锁
lock_rec_get_nth_bit Gets the nth bit of a record lock.
lock_get_mode 获取一个锁的 lock_mode
lock_mode_compatible 判断两个lock_mode是否兼容
lock_mode_stronger_or_eq 判断lock_mode 1 是否比 lock_mode 2更强
lock_get_wait 判断一个锁是不是等待锁
lock_rec_find_similar_on_page 查找一个合适的锁结构在当前事务当前页面下???找到的话就不用新创建锁结构???
lock_table_has 检查一个事务是否有指定类型的表锁,只能由当前事务调用
lock0priv.h 锁模块内部的结构和方法
struct lock_table_t 表锁结构
struct lock_rec_t 行锁结构
struct lock_t 锁通用结构
static const byte lock_compatibility_matrix[5][5] 锁的兼容关系
static const byte lock_strength_matrix[5][5] 锁的强弱关系
enum lock_rec_req_status 记录锁请求状态
struct RecID 记录锁ID
class RecLock 记录锁类
add_to_waitq 入队一个锁等待
create 为事务创建一个锁并初始化
is_on_row Check of the lock is on m_rec_id.
lock_alloc 创建锁实例
prepare 做一些检查个预处理为创建一个记录锁
mark_trx_for_rollback 收集需要异步回滚的事务
jump_queue 跳过所有低优先级事务并添加锁,如果能授予锁,则授予,不能的话把其他都标记异步回滚
lock_add 添加一个记录锁到事务锁列表和锁hash表中
deadlock_check 检查并解决死锁
check_deadlock_result 检查死锁检查的结果
is_predicate_lock 返回时不是predictate锁
init 按照要求设置上下文
lock_get_type_low 返回行锁还是表锁
lock_rec_get_prev 获取一个记录上的前一个锁
锁类型
在 Innodb 内部用一个 unsiged long 类型数据表示锁的类型, 最低的 4 个 bit 表示 lock_mode, 5-8 bit 表示 lock_type, 剩下的高位 bit 表示行锁的类型。
- lock_type
5-8 bit 位标识 lock_type 目前只使用了两个,第5位标识是表锁,第6位标识是行锁
#define LOCK_TABLE 16 /*!< table lock */ //表锁
#define LOCK_REC 32 /*!< record lock */ //记录锁
- lock_mode
lock描述了锁的基本模式,目前有5种模式,IS、IX、S、X、AI
enum lock_mode {
LOCK_IS = 0, /* intention shared */
LOCK_IX, /* intention exclusive */
LOCK_S, /* shared */
LOCK_X, /* exclusive */
LOCK_AUTO_INC, /* locks the auto-inc counter of a table
in an exclusive mode */
LOCK_NONE, /* this is used elsewhere to note consistent read */
LOCK_NUM = LOCK_NONE, /* number of lock modes */
LOCK_NONE_UNSET = 255
};
以下是锁的基本模式的兼容关系和强弱关系
/* LOCK COMPATIBILITY MATRIX
* IS IX S X AI
* IS + + + - +
* IX + + - - +
* S + - + - -
* X - - - - -
* AI + + - - -
*
* Note that for rows, InnoDB only acquires S or X locks.
* For tables, InnoDB normally acquires IS or IX locks.
* S or X table locks are only acquired for LOCK TABLES.
* Auto-increment (AI) locks are needed because of
* statement-level MySQL binlog.
* See also lock_mode_compatible().
*/
static const byte lock_compatibility_matrix[5][5] = {
/** IS IX S X AI */
/* IS */ { TRUE, TRUE, TRUE, FALSE, TRUE},
/* IX */ { TRUE, TRUE, FALSE, FALSE, TRUE},
/* S */ { TRUE, FALSE, TRUE, FALSE, FALSE},
/* X */ { FALSE, FALSE, FALSE, FALSE, FALSE},
/* AI */ { TRUE, TRUE, FALSE, FALSE, FALSE}
};
/* STRONGER-OR-EQUAL RELATION (mode1=row, mode2=column)
* IS IX S X AI
* IS + - - - -
* IX + + - - -
* S + - + - -
* X + + + + +
* AI - - - - +
* See lock_mode_stronger_or_eq().
*/
static const byte lock_strength_matrix[5][5] = {
/** IS IX S X AI */
/* IS */ { TRUE, FALSE, FALSE, FALSE, FALSE},
/* IX */ { TRUE, TRUE, FALSE, FALSE, FALSE},
/* S */ { TRUE, FALSE, TRUE, FALSE, FALSE},
/* X */ { TRUE, TRUE, TRUE, TRUE, TRUE},
/* AI */ { FALSE, FALSE, FALSE, FALSE, TRUE}
};
- record_lock_type
剩下的高位标识行锁的模式,对于表锁这些位都是空的
目前record_lock_type有以下值
#define LOCK_WAIT 256 /*!< Waiting lock flag; when set, it //锁等待
means that the lock has not yet been
granted, it is just waiting for its
turn in the wait queue */
/* Precise modes */
#define LOCK_ORDINARY 0 /*!< this flag denotes an ordinary
next-key lock in contrast to LOCK_GAP
or LOCK_REC_NOT_GAP */
#define LOCK_GAP 512 /*!< when this bit is set, it means that the
lock holds only on the gap before the record;
for instance, an x-lock on the gap does not
give permission to modify the record on which
the bit is set; locks of this type are created
when records are removed from the index chain
of records */
#define LOCK_REC_NOT_GAP 1024 /*!< this bit means that the lock is only on
the index record and does NOT block inserts
to the gap before the index record; this is
used in the case when we retrieve a record
with a unique key, and is also used in
locking plain SELECTs (not part of UPDATE
or DELETE) when the user has set the READ
COMMITTED isolation level */
#define LOCK_INSERT_INTENTION 2048 /*!< this bit is set when we place a waiting
gap type record lock request in order to let
an insert of an index record to wait until
there are no conflicting locks by other
transactions on the gap; note that this flag
remains set when the waiting lock is granted,
or if the lock is inherited to a neighboring
record */
#define LOCK_PREDICATE 8192 /*!< Predicate lock */
#define LOCK_PRDT_PAGE 16384 /*!< Page lock */
锁结构
- lock_sys
首先是锁系统结构,在Innodb启动的时候初始化,在Innodb结束的时候释放
主要保存着锁的hash表,以及相关事务、线程的一些信息
/** The lock system struct */
struct lock_sys_t{
char pad1[CACHE_LINE_SIZE]; /*!< padding to prevent other
memory update hotspots from
residing on the same memory
cache line */
LockMutex mutex; /*!< Mutex protecting the
locks */
hash_table_t* rec_hash; /*!< hash table of the record
locks */
hash_table_t* prdt_hash; /*!< hash table of the predicate
lock */
hash_table_t* prdt_page_hash; /*!< hash table of the page
lock */
char pad2[CACHE_LINE_SIZE]; /*!< Padding */
LockMutex wait_mutex; /*!< Mutex protecting the
next two fields */
srv_slot_t* waiting_threads; /*!< Array of user threads
suspended while waiting for
locks within InnoDB, protected
by the lock_sys->wait_mutex */
srv_slot_t* last_slot; /*!< highest slot ever used
in the waiting_threads array,
protected by
lock_sys->wait_mutex */
ibool rollback_complete;
/*!< TRUE if rollback of all
recovered transactions is
complete. Protected by
lock_sys->mutex */
ulint n_lock_max_wait_time; /*!< Max wait time */
os_event_t timeout_event; /*!< Set to the event that is
created in the lock wait monitor
thread. A value of 0 means the
thread is not active */
bool timeout_thread_active; /*!< True if the timeout thread
is running */
};
lock_sys_create . Creates the lock system at database start.
lock_sys_close Closes the lock system at database shutdown
- lock_t
无论是行锁还是表锁都使用lock_t结构保存,其中用一个union来分别保存行锁和表锁不同的数据,分别为lock_table_t和lock_rec_t
struct lock_t {
trx_t* trx; /*!< transaction owning the lock */
UT_LIST_NODE_T(lock_t)
trx_locks; /*!< list of the locks of the transaction */
dict_index_t* index; /*!< index for a record lock */
lock_t* hash; /*!< hash chain node for a record lock. The link node in a singly linked list, used during hashing. */
union { //表锁和记录锁不同的数据
lock_table_t tab_lock;/*!< table lock */ //表锁结构体
lock_rec_t rec_lock;/*!< record lock */ //记录锁结构体
} un_member; /*!< lock details */
ib_uint32_t type_mode; /*!< lock type, mode, LOCK_GAP or LOCK_REC_NOT_GAP, LOCK_INSERT_INTENTION, wait flag, ORed */ //锁类型
/** Remove GAP lock from a next Key Lock */
void remove_gap_lock() //移除一个next-key锁的gap锁
{
ut_ad(!is_gap());
ut_ad(!is_insert_intention());
ut_ad(is_record_lock());
type_mode |= LOCK_REC_NOT_GAP;
}
/** Determine if the lock object is a record lock.
@return true if record lock, false otherwise. */
bool is_record_lock() const //判断是否是记录锁
{
return(type() == LOCK_REC);
}
/** Determine if it is predicate lock.
@return true if predicate lock, false otherwise. */
bool is_predicate() const
{
return(type_mode & (LOCK_PREDICATE | LOCK_PRDT_PAGE));
}
bool is_waiting() const
{
return(type_mode & LOCK_WAIT);
}
bool is_gap() const
{
return(type_mode & LOCK_GAP);
}
bool is_record_not_gap() const
{
return(type_mode & LOCK_REC_NOT_GAP);
}
bool is_insert_intention() const
{
return(type_mode & LOCK_INSERT_INTENTION);
}
ulint type() const {
return(type_mode & LOCK_TYPE_MASK);
}
enum lock_mode mode() const
{
return(static_cast<enum lock_mode>(type_mode & LOCK_MODE_MASK));
}
/** Get lock hash table
@return lock hash table */
hash_table_t* hash_table() const
{
return(lock_hash_get(type_mode));
}
/** Get tablespace ID for the lock
@return space ID */
ulint space() const
{
return(un_member.rec_lock.space);
}
/** Get page number of the lock
@return page number */
ulint page_number() const
{
return(un_member.rec_lock.page_no);
}
/** Print the lock object into the given output stream.
@param[in,out] out the output stream
@return the given output stream. */
std::ostream& print(std::ostream& out) const;
/** Convert the member 'type_mode' into a human readable string.
@return human readable string */
std::string type_mode_string() const;
const char* type_string() const
{
switch (type_mode & LOCK_TYPE_MASK) {
case LOCK_REC:
return("LOCK_REC");
case LOCK_TABLE:
return("LOCK_TABLE");
default:
ut_error;
}
}
};
/** A table lock */
struct lock_table_t {
dict_table_t* table; /*!< database table in dictionary
cache */
UT_LIST_NODE_T(lock_t)
locks; /*!< list of locks on the same
table */
/** Print the table lock into the given output stream
@param[in,out] out the output stream
@return the given output stream. */
std::ostream& print(std::ostream& out) const;
};
/** Record lock for a page */
struct lock_rec_t {
ib_uint32_t space; /*!< space id */
ib_uint32_t page_no; /*!< page number */
ib_uint32_t n_bits; /*!< number of bits in the lock
bitmap; NOTE: the lock bitmap is
placed immediately after the
lock struct */
/** Print the record lock into the given output stream
@param[in,out] out the output stream
@return the given output stream. */
std::ostream& print(std::ostream& out) const;
};
- bitmap
Innodb 使用位图来表示锁具体锁住了那几行,在函数 lock_rec_create 中为 lock_t 分配内存空间的时候,会在对象地址后分配一段内存空间(当前行数 + 64)用来保存位图。n_bits 表示位图大小。
/* Make lock bitmap bigger by a safety margin */
n_bits = page_dir_get_n_heap(page) + LOCK_PAGE_BITMAP_MARGIN;
n_bytes = 1 + n_bits / 8;
lock = static_cast<lock_t*>(
mem_heap_alloc(trx->lock.lock_heap, sizeof(lock_t) + n_bytes));
显示锁和隐示锁
explicit lock 显示锁
implicit lock 隐示锁
InnoDB增加隐示锁的目的是在INSERT的时候不加锁
具体实现为
- 1.在insert的时候,不进行加锁
- 2.在当前读访问到一行的时候,判断是否有隐示锁且事务是活跃事务,有的话先转为显示锁
锁流程
lock system 开始启动 申请lock_sys_t结构,初始化结构体
lock system 结束关闭 释放lock_sys_t结构的元素,释放结构体
插入加锁流程
https://www.colabug.com/32979...
问题:为什么有GAP也能插入(有GAP是不能插入的),插入意向锁什么时候加(插入之前尝试加插入意向锁,冲突加等待,不冲突直接插数据), 有什么用,唯一键冲突如何处理的(需要检测冲突会先尝试给行加S|next-key lock,加成功再检测)
和加锁有关的流程大概如下
- 1)对表加IX锁
- 2)对修改的页面加X锁
- 3)如果需要检测唯一键冲突,尝试给需要加的唯一键列加一个S|next-key lock锁,可能会产生锁等待
- 4)判断是否插入意向锁冲突,冲突加等待的插入意向锁,不冲突直接插入数据
- 5)释放页面锁
删除加锁流程
删除加锁有个重要的问题是,删除并发的时候的加锁会有以下死锁问题
- 1)事务1获取表IX锁
- 2)事务1获取页面X锁
- 3)事务1获取第n行的x|not gap 锁
- 4)事务1删除第n行
- 5)事务1释放页面X锁
- 6)事务2获取页面X锁
- 7)事务2尝试获取第n行的x|not gap锁,发现冲突,等待
- 8)事务2释放页面X锁
- 9)事务1释放第n行的锁,提交事务
- 10)释放第n行锁的时候,检查到事务2有一个等待锁,发现可以加锁了,唤醒事务2,成功加锁
- 11)事务3获取页面X锁
- 12)事务3尝试删除第n行,发现第n行被删除(注意,此时记录还在还没被从页面刷出),尝试获取第n行的next-key lock,发现事务2有一个x|gap锁冲突,等待
- 13)事务3释放页面X锁
- 14)事务2获取页面X锁,检查页面是否改动,重新检查第n行数据,发现第n行数据被删除,尝试获取第n行的next-key lock,发现有事务3已经在等待这个锁了,事务2冲突,进入等待
- 15)死锁
表锁加锁流程
lock_table
- 1)检查当前事务是否拥有更强的表锁,有的话直接返回成功,否则继续往下走
- 2)遍历表的锁列表,判断是否有冲突的锁,如果没有转3,有转4
- 3)直接创建一个表锁,放入事务的lock list中,放入table的 lock list中,加锁成功
- 4)如3步骤,创建等待的表锁,加入list,然后进行死锁检测和死锁解决,回滚当前事务或者挂起当前事务
行锁加锁流程
- lock_rec_lock
主要的参数是 mode(锁类型),block(包含该行的 buffer 数据页),heap_no(具体哪一行)。就可以确定加什么样的锁,以及在哪一行加。
加锁流程主要是lock fast和lock slow,首先进入lock fast进行快速加锁,如果快速加锁失败则进入lock slow开始正常加锁流程,可能有锁冲突检查、死锁检查等流程
- lock fast
- 1.获取需要加锁的页面上第一个record lock
- 2.判断获取的锁是不是空,是转3,否转4
- 3.如果需要加的是隐示锁直接返回成功,否则,创建一个创建一个RecLock对象然后创建一个锁返回成功
- 4.判断当前页面上是否只有一个锁,且这个锁是当前事务的,且这个锁模式和需要加的模式一样,且bitmap的大小够用,满足前述条件转5,否则转6
- 5.可以快速加锁,直接设置bitmap进行加锁,返回成功
- 6.快速加锁失败,返回失败并进入lock slow流程
- lock slow
- 1.调用lock_rec_has_expl函数判断当前事务是不是有更强的锁,满足转2,不满足转3
lock_rec_has_expl函数遍历rec_hash,获取事务编号是当前事务的锁,同时满足以下五个条件就判定为有更强的锁
-
- 1)不是一个插入意向锁
- 2)不是一个等待中的锁
- 3)根据强弱关系矩阵判断满足更强
- 4)不是lock_rec_not_gap类型,或者要加的锁是lock_rec_not_gap类型或者heap_no是上界
- 5)不是lock_gap类型,或者要加的锁是lock_gap类型或者heap_no是上界
- 2.有更强的锁,直接返回成功,什么都不需要做
-
3.如果没有更强的锁,调用lock_rec_other_has_conflicting判断是否有锁冲突需要等待,如果有转4,没有转5。lock_rec_other_has_conflicting函数遍历rec_hash,拿出对应行上的每一个锁,调用 lock_rec_has_to_wait 进行冲突判断
- 1)如果当前锁和要加的锁是同一个事务的,直接返回,没有冲突
- 2)根据兼容矩阵判断当前锁和要加的锁是否兼容,如果兼容,直接返回,没有冲突
- 3)如果要加的是lock_gap或者heap_no是页面上界,且不是lock_insert_intention的话,可以直接返回,没有冲突,因为非插入意向锁的gap锁是不用等待的,都不冲突
- 4)如果要加的锁不是插入意向锁lock_insert_intention,且当前锁是一个gap锁,直接返回,没有冲突
- 5)如果要加的锁是gap锁,且当前锁是lock_rec_not_gap锁,直接返回,没有冲突
- 6)如果当前锁是一个插入意向锁,直接返回没有冲突
- 7)不满足上述条件,返回冲突
ps:为什么经过2步骤判断锁不兼容还需要往下走5个判断,是因为锁类型lock_mode/lock_type/rec_lock_type三种标记位同时有,如lock_x|lock_gap, lock_s|lock_rec_not_gap 这两个锁虽然lock_mode不兼容,但不冲突
-
4.调用add_to_waitq,入队一个锁等待
- 1)调用creat,创建lock_t,高优先级事务不放入rec_hash表,非高优先级放入
- 2)如果是高优先级事务,调用jump_queue,如果加锁成功直接返回,jump_queue大概为跳过所有优先级比当前锁低的等待锁,加入等待队列中
- 3)调用deadlock_check进行死锁检测
-
5.判断是否是隐示锁,是的话直接返回成功,什么都不做,不是的话调用lock_rec_add_to_queue入队一个锁
- 1)type_mode|=lock_rec,判断heap_no是否是页面上届,是的话,type_mode不能是lock_rec_not_gap
- 2)遍历rec_hash判断当前行上是否有等待的锁,没有转3,有转4
- 3)如果没有,且当前锁不是一个lock_wait,寻找当前页面上有没有相似的锁(当前事务的锁且锁类型和要加的锁一样),有的话直接设置标记位,没有转4
- 4)创建一个锁lock_t,设置bit位,设置lock_type等信息,添加到rec_hash表中和事务的lock_list中
释放锁流程
lock_rec_dequeue_from_page
- 1.把当前锁从全局hash表中删除
- 2.把当前锁从事务锁list中删除
-
3.调用lock_rec_grant函数,尝试给等待锁加锁
- 1)遍历锁hash表中当前页面上的锁,对于每个等待锁,调用lock_rec_has_to_wait_in_queue函数判断是否还需要等待
lock_rec_has_to_wait_in_queue
- 遍历当前锁所在页面的所有非等待锁
- 对于每个锁,根据heap_no判断当前锁需要加锁的行是否被锁上
- 如果被锁上,判断要加的锁是否需要等待
- 2)对于不需要等待的锁,调用lock_grant进行加锁
- 移除lock的lock_wait状态
- 置空lock的wait_lock
- 调用que_thr_end_lock_wait和lock_wait_release_thread_if_suspended唤醒等待中的线程
lock_table_dequeue
-
1.调用lock_table_remove_low
- 1)如果是自增长锁,将锁从事务的autoinc_locks list中移除
- 2)从事务锁列表将锁移除
- 3)从表的锁列表将锁移除
- 2.遍历当前表的表锁列表,判断等待的锁是否能加锁,能的话调用lock_grant函数进行加锁
死锁流程
构造wait-for graph
构造一个有向图,图中的节点代表一个事务,图的一个边A->B代表着A事务等待B事务的一个锁
具体实现是在死锁检测时,从当前锁的事务开始搜索,遍历当前行的所有锁,判断当前事务是否需要等待现有锁释放,是的话,代表有一条边,进行一次入栈操作
死锁检测
有向图判断环,用栈的方式,如果有依赖等待,进行入栈,如果当前事务所有依赖的事务遍历完毕,进行一次出栈
回滚事务选择
如果发现循环等待,选择当前事务和等待的事务其中权重小的一个回滚,具体的权重比较函数是 trx_weight_ge, 如果一个事务修改了不支持事务的表,那么认为它的权重较高,否则认为 undo log 数加持有的锁数之和较大的权重较高。
DeadlockChecker::search()
- 1)将当前要加的等待锁设置为wait_lock,start_lock
- 2)遍历wait_lock锁所在位置上的所有锁
- 3)判断wait_lock锁是否需要等待遍历到的锁lock,不需要等待就跳过,
- 4)如果需要等待,将lock push进栈,同时将lock设置为wait_lock,搜索深度+1,重复上述过程
- 5)如果要等待,判断lock和start_lock是否是一个事务,是的话,发生死锁,选择当前事务和start_lock的事务权重小的回滚
- 6)如果要等待,判断搜索深度是否已经过深,超过阈值,认为发生死锁,直接回滚了start_lock的事务
等待与唤醒
锁的等待以及唤醒实际上是线程的等待和唤醒,调用函数 lock_wait_suspend_thread 挂起当前线程,配合 OS_EVENT 机制,实现唤醒和锁超时等功能
锁分裂、合并、迁移
分裂
索引页面分裂导致的锁分裂
- 1)普通锁,会跟随着记录位置的移动,锁一起移动到新的位置,加锁信息保持不变
- 2)gap锁,会给新页面和老页面的上下界最大最小值上根据情况调整gap锁,目的保持gap锁上的区间保持不变
合并
索引页面合并导致的锁合并
- 合并和分裂基本一致
迁移
插入和删除记录时的GAP锁的迁移
- 1.在有GAP锁的间隙里插入记录时会出现GAP锁的迁移
主要是出现在当前事务拥有一个记录的GAP锁,又在这个记录前插入记录时
set global tx_isolation='repeatable-read';
create table t1(c1 int primary key, c2 int unique) engine=innodb;
insert into t1 values(1,1);
begin;
# supremum 记录上加 LOCK_X|LOCK_GAP 锁住(1~)
select * from t1 where c2=2 for update;
# 发现插入(3,3)的间隙存在GAP锁,因此给(3,3)加LOCK_X | LOCK_GAP锁。这样依然锁住了(1~)
insert into t1 values(3,3);
for (lock = lock_rec_get_first(lock_sys->rec_hash, block, heap_no);
lock != NULL;
lock = lock_rec_get_next(heap_no, lock)) { //遍历当前行的所有锁
if (!lock_rec_get_insert_intention(lock)
&& (heap_no == PAGE_HEAP_NO_SUPREMUM
|| !lock_rec_get_rec_not_gap(lock))) { //如果不是插入意向锁 且 heap是上界或者不是一个非GAP锁
lock_rec_add_to_queue( //添加一个GAP的且mode和lock一致的锁到下一行
LOCK_REC | LOCK_GAP | lock_get_mode(lock),
block, heir_heap_no, lock->index,
lock->trx, FALSE);
}
}
- 2.删除拥有GAP锁的记录时会出现GAP锁的迁移
如有记录 1 3 5
- 1)事务A对记录3加GAP锁,阻止1-3的间隙插入
- 2)事务B对记录3加X锁,GAP锁和X锁不冲突,加锁成功,事务B删除记录3,提交
- 3)此时当后台线程刷盘时,发现记录3已经删除,将从此页面将3记录删除,但发现3上还有个GAP锁,就会把这个GAP锁继承给这个记录后面的记录5
Innodb在RR和RC隔离下的加锁实例分析
例子:select * from meng_hinata where id = 10 for update
组合一:id列是主键,RC隔离级别
在主键id=10列加上X锁
组合二:id列是二级唯一索引,RC隔离级别
在唯一索引id=10列上加X锁,在主键索引上对应列加X锁
组合三:id列是二级非唯一索引,RC隔离级别
在二级索引上所有id=10列加上X锁,这些列对应的主键索引列加上X锁
组合四:id列上没有索引,RC隔离级别
在聚簇索引上扫描,所有列上加X锁,此处有个优化,不满足的列在加锁后,判断不满足即可释放锁,违背二阶段加锁
组合五:id列是主键,RR隔离级别
在主键id=10列上加X锁
组合六:id列是二级唯一索引,RR隔离级别
在唯一索引id=10列上加X锁,在主键索引上对应列加X锁
组合七:id列是二级非唯一索引,RR隔离级别
在二级索引上查找id=10列,找到则加上X锁和GAP锁,然后对应的聚簇索引列加上X锁,最后一个不满足的列只会加上GAP锁
组合八:id列上没有索引,RR隔离级别
在聚簇索引上扫描,所有列加上X锁和GAP锁
测试
Innodb默认事务隔离级别为RR
看出默认隔离级别为Repeatable Read,对user_id = 100000745进行count并显示加一个X锁,使用explain看出使用了uid索引即user_id字段的索引
id = 1449731912的数据user_id = 100000745,可以看出在对user_id字段索引加了X锁之后,操纵相对应的主键索引时也会被阻塞,验证了对非主键索引加X锁的同时会对相应主键索引也加锁
同时在对user_id字段索引加了X锁之后,也不能插入user_id相同的新数据,验证了innodb再RR隔离级别下也是防止了幻读的现象,实际上当范围查找时也会再加一个间隙锁来保证不会有幻读
可以看出不使用for update之后变为非当前读,也没有进行加锁,可以进行插入操作。
以上两个图设置隔离级别为RC后,可以看出在user_id = 100000745 进行for update查询后,还是能进行插入相同user_id值的列,说明只加了X锁并没有加间隙锁,同时因为X锁的原因,不能进行删除,验证了innodb引擎在RC隔离级别对于当前度也是会对读到的信息加锁,但没加间隙锁,会出现幻读。
以上两个图,在RC隔离级别下。
首先事务A对无索引的列进行查找并加锁,会扫描全表,注意并没有加表锁,而是对所有行都加了X锁,但是没加间隙锁,事务B还是可以插入。
此时事务A再扫描全表并加X锁,发现被阻塞,提交事务B后继续运行
事务A对全表加X锁后,事务B再次尝试插入,同样成功,但无法删除数据,说明还是没用表锁,对所有行加了X锁。
InnoDB锁同步机制
InnoDB条件变量核心数据结构为os_event_t,类似pthread_cont_t。如果需要创建和销毁则分别使用os_event_create和os_event_free函数。需要等待某个条件变量,先调用os_event_reset,然后使用os_event_wait,如果需要超时等待,使用os_event_wait_time替换os_event_wait即可
InnoDB自旋互斥锁的实现主要在文件 sync0sync.cc 和sync0sync.ic 中,头文件sync0sync.h 定义了核心数据结构ib_mutex_t。使用方法很简单,mutex_create创建锁,mutex_free释放锁,mutex_enter尝试获得锁,如果已经被占用了,则等待。mutex_exit释放锁,同时唤醒所有等待的线程,拿到锁的线程开始执行,其余线程继续等待。
InnoDB读写锁的核心实现在源文件sync0rw.cc 和sync0rw.ic 中,核心数据结构rw_lock_t 定义在sync0rw.h 中。使用方法与InnoDB 自旋互斥锁很类似,只不过读请求和写请求要调用不同的函数。加读锁调用rw_lock_s_lock, 加写锁调用rw_lock_x_lock,释放读锁调用rw_lock_s_unlock, 释放写锁调用rw_lock_x_unlock,创建读写锁调用rw_lock_create,释放读写锁调用rw_lock_free。函数rw_lock_x_lock_nowait和rw_lock_s_lock_nowait表示,当加读写锁失败的时候,直接返回,而不是自旋等待。