说明
文章的图片来源《MySQL是怎么运行的:从根儿上理解MySQL》,本篇文章只是个人学习总结,欢迎大家买一本正版小册看看,对于mysql是由浅入深的讲解非常细致
目录
25.锁
解决并发事务带来问题的两种基本方式
- 读-读:这种并不会产生并发问题
- 写-写:对同一条记录进行修改,这种是会发生并发问题的。所以需要通过锁来进行处理。事务排队执行
当事务需要对记录做改动的时候,就需要一个所结果和它进行关联
锁结构的信息
- trx信息:锁结构由哪个事务生成
- is_waiting:代表当前事务是否等待。
下面的整个流程其实就是T1先看看记录是不是有锁结构和它关联,如果没有那么T1需要创建一个,这个时候is_waiting是false的意思就是事务1已经获取到了锁。但是对于T2,发现记录已经有锁结构关联,但是仍然需要创建一个T2的锁结构,而且is_waiting是true说明获取锁失败需要等待前一个事务释放锁。
当T1执行完释放锁,那么T2的is_waiting就改变成false也就是这个时候T2获取到了锁。
- 不加锁
不需要在内存生成锁结构,可以直接执行
- 获取锁成功或者是加锁成功
获取了锁结构而且is_waiting是false
- 获取锁失败
生成锁结构但是is_waiting是true
读写或者是写读
- 对于读-写或者是写-读:这种情况会发生脏读、不可重复读、幻读的问题
怎么解决脏读、不可重复读、幻读这些问题?
- 方案1:利用多版本并发控制(MVCC),写操作加锁
对于读可提交级别的readview避免了脏读问题,因为这些未提交的事务是无法被当前事务看到的。对于可重复读来说因为每次readview都是在第一次读的时候固定下来所以并不会产生幻读和不可重复读问题,因为readview相当于就是一个数据快照。
- 方案2:读写操作都加上锁
银行存款的问题,每次修改都需要加上锁,因为这种数据是不能够直接读取之前的记录,防止计算错误。所以在读写的时候需要加上锁,其它事务直接排队等待。
对于脏读是事务读取了另一个未提交事务的修改数据,但是现在加上锁,那么当前事务是无法读取记录的,所以不会产生脏读。对于不可重复读,由于当前事务无法事先读取正在修改当前记录,因为记录被另一个事务加上了锁,所以读取只能够等待另一个事务结束,最后不会发生不可重复读。但是对于幻读可能会难一点,因为innodb是支持行锁的。
- 通常mvcc更好,因为不会影响各自事务处理的事情。但是加锁更安全,性能要差一些。
一致性读
事务利用mvcc的select读取就是一致性读,而且不会加锁
锁定读
共享锁和独占锁
- 共享锁:shared locks,简称s锁,读取一条记录需要该条记录的s锁
- 独占锁:exclusive locks,简称x锁,改动记录需要获取这个记录的排他锁
T1访问记录1获取s锁,T2接着访问
- 如果T2只是读取,那么T1和T2都可以拥有s锁
- 如果是要修改,那么T2无法获取x锁,需要等待T1释放s锁
如果T1获取记录1的x锁
- 对于T2无论做什么都需要等待T1是释放锁。
锁定读语句
- 对读取记录加上s锁
select … lock in share mode
- 对于读取的记录加上x锁
select … for update
写操作
- delete
定位B+树记录的位置,获取记录的x锁,进行delete mark操作,定位b+树的记录过程就是一个获取x锁的锁定读
- update
- 分三种情况
- 第一种就是更新的记录前后存储空间不变,那么就直接定位到这个B+树的记录位置(过程就是获取x锁的锁定读)
- 如果记录其中一个列存储空间发生变化,定位记录,并且删除(移入到垃圾链表),最后插入一条新的记录,定位还是一个获取x锁的锁定读,新插入的记录通过隐式锁保护
- 如果修改记录的键值,那么就在原记录上面删除之后再进行插入。
- insert
插入使用的是隐式锁
多粒度锁
现在用的都是行锁,粒度比较细,可以给表加上对应的锁
- 给表加上s锁
其它事务获取表的s锁
但是不能获取x锁
- 给表加上x锁
对于当前的表什么锁都不能获取
但是有一个问题如果表需要上一个s锁的时候需要确定表里面的记录没有上x锁,而且如果表要上x锁的时候要保证记录没有上s锁或者是x锁,那么怎么确定记录是不是有上锁?
- 可以通过意向锁,
- 意向共享锁:intention shared lock简称is锁,事务给记录上s锁的时候给表也上一个is锁
- 意向独占锁:intention exclusive lock简称ix锁,事务给记录上x锁也会给表上一个ix锁
意向锁可以快速确定表是否还有其他锁,并且可以上表级锁。
MySQL中的行锁和表锁
其他存储引擎中的锁
- MyISAM、MEMORY、MERGE只支持表级,不支持事务
INNODB的锁
- 表锁和行锁都支持
InnoDB中的表级锁
- s和x锁
- 对表执行insert、update、delete、select不会给表加上表级锁
- 但是有一个事务进行DDL的时候,其他事务如果要执行insert、update、delete、select都会被阻塞,相反进行DML语句的时候不能同时进行DDL,这种锁通常是server层提供的元数据锁MDL
- lock table t read上s锁或者是lock table t write上x锁
- 表级别的is和ix锁
- 表级别的auto-inc锁
下面这种就是直接给id自增赋值
CREATE TABLE t (
id INT NOT NULL AUTO_INCREMENT,
c VARCHAR(100),
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;
赋值原理的原因
- 采用auto_inc锁,插入语句的时候就会在表加上这个锁,保证只有一个事务在插入数据
- 采用一个轻量级锁,给生成auto_increment的列加上锁,生成列的自增数值之后就可以释放锁了,没必要完全插入记录之后才释放锁。
InnoDB中的行级锁
行锁类型
- record locks
锁上一条记录,这种锁叫LOCK_REC_NOT_GAP。这种锁也有s锁和x锁的区分。就是我们上面说的那种
- Gap Locks
解决幻读可以通过repeatable read隔离级别。两种方式
- mvcc
- 加锁,但是幻读是因为第一读取的时候没有读到这些记录,第二次读取由于其他事务插入记录并且提交,才会读取到这些幻影记录,但是这些记录不能在一开始就加上正经记录锁(就是上面的record locks)。解决办法就是加入gap锁,这种锁加在下面的8号位置,说明3-8这个间隙是不能够插入任何记录的,如果其他事物需要插入记录,那么就需要等待当前事务处理完释放gap锁
但是问题有来了gap锁只能锁定的当前记录的主键和前一条记录的主键间隙不能插入记录,那么如果现在插入的位置不确定,这个时候又应该如何加入这个gap锁?对于number为20之后的又应该如何锁定?这个时候就需要数据页的两个伪记录
- Infimum页面最小记录
- Supremum:页面最大记录
为了防止在(20,+无穷)上面插入幻影记录,可以给最大的记录加上一个gap锁。
- next-key locks
正经锁+gap锁合体,锁定当前记录,而且还能锁定前面的间隙不允许插入任何记录。
- insert intention locks
如果要插入一条记录,刚好这个位置被加上了gap锁,这个时候需要生成一个插入意向锁结构。
假设现在要插入4、5的记录,那么由于gap锁会被阻塞。就需要锁结构来让这些事务进行排队等待。而且对于这些插入意向锁是可以被多个事务同时获取并且插入的。
- 隐式锁
通常insert并不会加锁,但是插入过程中,另一个事务
- select … lock in share mode读取这个事务获取这条记录的s锁,select … for update或者直接修改这条记录,这个时候需要获取到这条记录的x锁
- 立即修改这条记录也需要获取记录的x锁
上面都可能会产生脏写问题。
事务id的作用
- 情景1:如果是聚簇索引的话,那么如果一个事务a在这里插入一条记录,但是记录没有上锁,这个时候另一个事务b需要修改这条记录,那么就会检查记录的一个trx_id如果发现trx_id不是自己的那么就帮助事务a上一个x锁,并且给自己也上一个锁并且进入等待状态。
- 情景2:对于二级索引,页面上page Header上面有一个page_max_trx_id代表对该页面修改的最大事务id。如果这个事务id小于活跃事务的最小id说明页面修改的事务已经被提交,如果不是,那么就要找到二级索引这条记录去到聚簇索引重复上面过程。
事务id其实就是一个隐式锁,如果某个事务对插入的记录加上s锁或者是x锁都会检查记录的事务id,如果事务id不是自己,那么就需要给他创建一个锁结构,并且给自己也创建一个锁结构等待前一个事务结束。
InnoDB锁的内存结构
在不同记录上面加锁的时候
- 同一个事务
- 被加锁记录在同一个页面
- 加锁类型一样
- 等待状态一样
那么这些记录的锁就可以放到一个锁结构上面。
-
锁所在事务信息:哪个事务生成这个锁,记录当前事务的信息(只是一些指针)
-
索引信息:加锁的记录属于哪个索引
-
表锁/行锁信息
- 表锁:记录哪个表加上的锁
- 行锁:表空间、页号、哪条记录(通过bit来区分,每个bit代表页面的每条记录)
-
type_mode:32位数分成lock_mode、lock_type和rec_lock_type
- 锁模式(lock_mode)
- LOCK_IS:共享意向锁
- LOCK_IX:独享意向锁
- LOCK_S:共享锁
- LOCK_X:独占锁
- LOCK_AUTO_INC:auto_inc锁(处理插入的递增问题)
- 锁类型(lock_type)
- LOCK_TABLE(16):第五个bit位为1的时候,表示表级锁
- LOCK_REC(32):第六个bit为1的时候就是行级锁
- 行锁的具体类型
- LOCK_ORDINARY:next-key锁
- LOCK_GAP:第十个bit是1的时候就是gap锁
- LOCK_REC_NOT_GAP(1024):第十一个bit为1的时候正经记录锁
- LOCK_INSERT_INTENTION:插入意向锁(主要是解决gap锁的阻塞问题)
- 锁模式(lock_mode)
-
一堆bit
也就是上面的n_bits,每个页头都有一个heap_no,Infimum是0,Supremum是1。每次添加一条数据heap_no都会+1,每个bit位映射到一个页
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s5xgWHAX-1636338643714)(…/…/…/…/…/AppData/Roaming/Typora/typora-user-images/image-20211107164322294.png)]
简述过程
T1给number=15的记录加s锁,首先需要给表加上IS锁
-
加的是行锁
- 表空间67,页号3
- n_bits,现在只有5条记录
n_bits = (1 + ((n_recs + LOCK_PAGE_BITMAP_MARGIN) / 8)) * 8
n_recs是当前有多少条记录,LOCK_PAGE_BITMAP_MARGIN默认64。
- type mode
- lock_mode:LOCK_S,共享锁
- lock_type:LOCK_REC记录加锁
- rec_lock_type:对记录加上正经记录锁
- type mode=2|32|1024=1058
- 一堆bit,number=15的heap_no等于5刚好映射到第三个bit上面
下面就是整个锁结构
- 针对于T2给number =3,8,15加上x的next-key锁,因为这个时候15已经被加上了s锁所以需要创建一个新的锁的结构。但是对于3和8仍然存在于一个锁结构成功加锁。所以一共生成了两个锁结构。关键信息还是索引、行锁信息、锁的模式信息(什么类型的锁(独占还是共享),行锁还是表锁、记录的锁类型。)
[外链图片转存中…(img-7NmfIS6U-1636338643714)]
下面就是整个锁结构
[外链图片转存中…(img-9gjjWqys-1636338643715)]
- 针对于T2给number =3,8,15加上x的next-key锁,因为这个时候15已经被加上了s锁所以需要创建一个新的锁的结构。但是对于3和8仍然存在于一个锁结构成功加锁。所以一共生成了两个锁结构。关键信息还是索引、行锁信息、锁的模式信息(什么类型的锁(独占还是共享),行锁还是表锁、记录的锁类型。)
[外链图片转存中…(img-tgcpa5xL-1636338643716)]