锁机制
- 1. 概述
- 2. 并发事务的不同场景
- 2.1 读-读情况
- 2.2 写-写情况
- 2.3 读-写或写-读情况
- 2.3.1 方案一:读事务使用MVCC(多版本并发控制),写事务加锁
- 2.3.2 方案二:读、写事务均加锁
- 3. 锁分类
- 3.1 从数据操作类型:读锁、写锁
- 3.2 从数据操作粒度:表级锁、页级锁、行锁
- 3.2.1 表级锁(Table Lock)
- 表级别的S锁、X锁
- 意向锁(Intention Lock)
- 自增锁(Auto-Inc Lock)
- 元数据锁(Meta Data Lock)
- 3.2.2 行锁(Row Lock)
- 记录锁(Record Lock)
- 间隙锁(Gap Lock)
- 临键锁(Next-Key Lock)
- 插入意向锁(Insert Intention Lock)
- 3.2.3 页锁
- 3.3 从对待锁的态度:乐观锁、悲观锁
- 3.3.1 悲观锁
- 3.3.2 乐观锁
- 版本号机制
- 3.4 从加锁的方式:显式锁、隐式锁
- 3.4.1 显式锁
- 3.4.2 隐式锁
- 4. 死锁
- 4.1 死锁
- 4.2 死锁产生的必要条件
- 4.3 如何处理死锁
- 4.3.1 方式一:使用超时机制
- 4.3.2 方式二:使用死锁检测机制
- 4.4 如何避免死锁问题
1. 概述
- 锁机制是保证事务隔离性的根本;
- 计算机通过锁机制协调多进程/多线程并发访问相同资源的问题;
- 锁机制保证同一时刻共享资源只能被某一进程/线程访问,以此保证数据的一致性和完整性;
- 从数据库角度而言,共享资源除了硬件资源以外还有表数据,为保证表数据的一致性和完整性,就必须引入锁机制,对并发操作进行控制;
- 锁机制会导致锁冲突甚至死锁的发生,及其影响并发操作的性能;
2. 并发事务的不同场景
2.1 读-读情况
- 多个事务同时读取相同记录,并不会对记录本身造成影响,所以无需进行特殊处理;
2.2 写-写情况
- 多个事务均要对相同记录进行修改,此时可能会发生“脏写”问题;
- 所有隔离级别都解决了“脏写”问题,通过对事务加锁的方式实现,同一时刻只能允许一个事务执行,其他事务排队等待;
- 对于锁机制,每个事务均对应一个锁结构(内存级别)与记录关联,包括事务信息以及该事务是否需要排队等待等信息:
- 事务对应锁结构中is_waiting有两种取值:1)is_waiting=false,说明该事务获取锁成功,可以继续执行事务;2)is_waiting=true,说明该事务获取锁失败,期望操作的记录已经被加锁,当前事务需要排队等待;
2.3 读-写或写-读情况
- 读-写或写-读情况,即多个事务中既有数据读取事务,也有数据写入事务,此时可能会发生“脏读”、“不可重复读”以及“幻读”问题;
- 未解决上述并发事务问题,MySQL提供两种方案:1)读事务使用MVCC(多版本并发控制),写事务加锁;2)读、写事务均加锁;
2.3.1 方案一:读事务使用MVCC(多版本并发控制),写事务加锁
- 此方案适用于读、写事务不会冲突,可同时进行的情况,并发性能较高;
- 写事务需要对最新数据进行修改,所以需要加锁,避免其他事务的干扰;
- 此情况下读事务只能读取到其他事务已提交的数据,因此对于读事务而言其他事务对记录的修改和记录的旧版本不冲突,所以可以直接读取旧版本数据;
- MVCC做法:1)生成ReadView,通过ReadView确定符合条件的记录版本,记录的历史版本通过undo日志构建;2)读事务只能看到生成ReadView之前已提交的数据;
2.3.2 方案二:读、写事务均加锁
- 此方案适用于读、写事务冲突,不能同时进行的情况,并发性能较低;
- 此情况下读事务需要获取最新数据,所以需要避免其他事务的干扰,因此需要加锁;
- 事务通过对数据记录加锁保证数据一致性,避免发生“脏读”、“不可重复读”以及“幻读”问题;
3. 锁分类
3.1 从数据操作类型:读锁、写锁
- 读锁:readLock,也称共享锁:Shared Lock,S Lock;写锁:writeLock,也称排他锁:Exclusive Lock,X Lock;
- 对于读事务(查询操作),可加共享锁或排他锁;
- 对于写事务(增删改操作),可加排他锁;
- 读写锁互斥关系:
1)多个读锁之间不会互斥;
2)读锁-写锁之间会互斥;
3)写锁-写锁之间会呼出; - 如果数据已经加锁,当前事务无法获取锁,则默认进入阻塞状态,通过innodb_lock_wait_timeout参数进行控制。MySQL8.0新特性可在加锁操作后添加NOWAIT或SKIP LOCAKED参数执行不同的选择。
- 读事务加锁方式:
1)读锁:select ... for share
;
2)写锁:select ... for update
; - InnoDB引擎下,读写锁既可以加在表上,也可加在行上;
3.2 从数据操作粒度:表级锁、页级锁、行锁
- MyISAM引擎只支持表级锁,InnoDB引擎支持表级锁、行锁;
- 表级锁对应操作粒度较粗,行锁对应操作粒度较细;
- 操作粒度越细,并发性能越好,但较耗费资源;
3.2.1 表级锁(Table Lock)
- 最基本的锁策略,不依赖于存储引擎;
- 表级锁对应资源开销最小,并发性能最低;
- 表级锁可避免死锁问题;
表级别的S锁、X锁
- 对于InnoDB引擎而言,一般不会使用表级别的S锁、X锁。
- 在某事务执行DDL操作,而其他事务在执行DML操作时,会触发表锁,该过程是在server层通过元数据锁实现的;
- 如果仅使用表级别的S锁、X锁,则使用MyISAM引擎即可;
- 添加表级别的S锁、X锁方式:
1)添加S锁:lock tables 表名 read
;
2)添加X锁:lock tables 表名 write
; - 释放表锁:
unlock tables
; - 查看表锁:
show open tables
; - 互斥关系:
意向锁(Intention Lock)
- InnoDB支持多粒度锁,特殊情境下允许行锁和表锁共存;
- 意向锁是一种表锁,允许和行锁共存;
- 意向锁分类:意向共享锁IS锁、意向排他锁IX锁;
- 当为数据表添加行锁之后,数据库会自动为对应数据页或数据表添加相应意向锁,如此其他事务想要添加表锁就会进入阻塞状态;
- 互斥关系:1)意向锁之间不互斥;2)IS锁与表级S锁不互斥,其他意向锁与表级S锁或X锁互斥;
- 总结:
自增锁(Auto-Inc Lock)
- 插入数据的方式:1)简单插入:事先知道要插入的数据数量;2)批量插入:事先不知道要插入的数据数量;3)混合模式插入:批量插入数据,有些指定了字段序号,有些未指定;
- 对于数据表,如果有字段设置有AUTO_INCREMEN约束,则向该表插入数据时,会添加表级锁,称为自增锁;
- 自增锁并发性能较差,InnoDB通过innodb_autoinc_lock_mode参数提供不同的锁定机制,提高性能:
1)innodb_autoinc_lock_mode = 0(“传统”锁定模式):在此锁定模式下,所有类型的insert语句都会获得一个特殊的表级AUTO-INC锁,用于插入具有AUTO_INCREMENT列的表。
2)innodb_autoinc_lock_mode = 1(“连续”锁定模式): MySQL 8.0 之前的默认模式。在这个模式下,批量插入仍然使用AUTO-INC表级锁,并保持到语句结束。而对于简单插入,则通过在 mutex(轻量锁) 的控制下获得所需数量的自动递增值来避免表级AUTO-INC锁, 它只在分配过程的持续时间内保持,而不是直到语句完成。
3)innodb_autoinc_lock_mode = 2(“交错”锁定模式): MySQL 8.0 开始,交错锁模式是默认设置。在此锁定模式下,自动递增值保证在所有并发执行的所有类型的insert语句中是唯一且单调递增的。但是,由于多个语句可以同时生成数字(即,跨语句交叉编号),为任何给定语句插入的行生成的值可能不是连续的。
元数据锁(Meta Data Lock)
- 简称MDL锁,用于保证表结构与数据的一致性;
- MDL锁在server层实现;
- 当某事务要对数据表进行增删改查操作时,会自动添加MDL读锁;当某事务要对数据表结构进行修改时,会自动添加MDL写锁;
- MDL读锁之间不互斥,但MDL读-写锁之间是互斥的;
3.2.2 行锁(Row Lock)
- 也称为记录锁(Record Lock),即对符合条件的记录加锁;
- MySQL只在引擎层实现了行级锁;
- MyISAM与InnoDB的区别:1)InnoDB支持事务;2)InnoDB支持行锁;
- 行锁优缺点:
1)优点:锁定粒度小,发生锁冲突概率低,提高并发性能;
2)缺点:维护锁的开销较大,加锁速度慢,容易发送死锁问题;
记录锁(Record Lock)
- 官方名称:LOCK_REC_NOT_GAP;
- 仅对满足条件的唯一记录进行加锁;
- 记录锁也分:读锁、写锁;
- 读锁之间兼容,其他情况互斥;
间隙锁(Gap Lock)
- 官方名称:LOCK_GAP;
- 事务隔离级别为可重复读时,可通过事务加锁的方式解决脏读、不可重复读以及幻读问题;
- 需要注意,加锁方式解决幻读问题时会存在一些问题,事务读取数据时,幻影记录并未存在,无法锁定幻影记录;
- 数据库引入间隙锁来解决幻读问题,即GAP锁的提出仅是为了防止插入幻影记录;
- GAP锁与其他记录锁或间隙锁是不互斥的;
- 间隙锁会导致发生死锁问题;
临键锁(Next-Key Lock)
- 官方名称:LOCK_ORDINARY;
- 临键锁本质是记录锁和间隙锁的合体,既能锁定对应记录,也能防止在该记录之前的间隙插入记录;
- 事务隔离级别为可重复读时,InnoDB引擎默认使用的锁机制即为临键锁;
插入意向锁(Insert Intention Lock)
- 官方名称:LOCK_INSERT_INTENTION;
- 插入意向锁为行锁,本质是间隙锁;
- 间隙锁可以防止其他事务在锁定的间隙中插入数据,插入意向锁为插入数据事务对应的锁结构;
- 插入意向锁之间不会互斥;
- 插入意向锁和其他锁不互斥;
3.2.3 页锁
- 对数据页进行加锁,锁定粒度介于表锁与行锁之间;
- 页锁也会发生死锁现象;
- 页锁的开销介于表锁和行锁之间;
- 锁空间大小是有限的,如果某个层级的锁数量超过当前层阈值,则会对锁进行自动升级。如此操作会降低内存开销,但降低了并发性能;
3.3 从对待锁的态度:乐观锁、悲观锁
- 乐观锁、悲观锁只是针对锁的一种设计思想,各自有对应的具体实现;
3.3.1 悲观锁
- 思想:每次假设最坏的情况,即假设每个事务都会修改数据,所以每个事务一来就对数据加锁,其他事务进行等待;
- 数据库中涉及的表锁、行锁等都是悲观锁的具体实现;
- 悲观锁适用于频繁写操作的情况;
3.3.2 乐观锁
- 思想:对并发事务保持乐观态度,认为同一数据的并发事务并不会经常发生,因此事务刚开始并不会进行加锁,而是真正要修改数据时才去判断是否发生了并发冲突;
- 乐观锁在程序级别通过代码实现,而不会通过数据库底层;
- 乐观锁的实现方案:1)版本号机制;2)CAS机制;
- 乐观锁不会发生死锁问题;
- 乐观锁适用于频繁读操作的情况;
版本号机制
- 对数据添加版本号字段version,version有初始值;
- 事务在操作数据时,同时获取数据对应版本号,只有当事务版本号version‘大于最开始获取的数据版本号version时才能完成数据修改的提交;
3.4 从加锁的方式:显式锁、隐式锁
3.4.1 显式锁
- 通过特定语句加的锁,称为显式锁;如
3.4.2 隐式锁
4. 死锁
4.1 死锁
- 两个事务均持有对方需要的锁,且在等待对方释放锁,而事务自身均不会释放自己持有的锁。此时两个事务进入循环等待,均无法继续执行。
- 产生死锁的关键在于:两个事务加锁的顺序不一致;
4.2 死锁产生的必要条件
- 至少存在两个事务;
- 每个事务都持有锁,并需要申请新的锁;
- 锁资源同时只能被同一个事务持有;
- 事务之间因为持有锁和申请锁导致彼此循环等待;
4.3 如何处理死锁
4.3.1 方式一:使用超时机制
- 事务如果无法成功获取锁,则进入等待,直到等待超时;
- 超时时间通过innodb_lock_wait_timeout参数设置;
- 如果事务等待超时,则将其回滚;
- 缺点:不适用于在线任务;
4.3.2 方式二:使用死锁检测机制
-
InnoDB使用wait-for graph算法主动进行死锁检测;
-
事务一旦无法成功获取锁,则主动进行死锁检测,检测是否是自己的加入导致死锁的发生;
-
一旦检测到死锁,InnoDB选择回滚undo量最小的事务,其他事务继续执行;
-
缺点:对于并发事务量较大的业务,死锁检测耗时较大;
4.4 如何避免死锁问题
参考《尚硅谷:康师傅》