本篇内容包括:MySQL锁、MySQL MVCC概述以及MySQL Mvcc实现三大要素
一、MySQL锁
数据库的锁是为了解决事务的隔离性问题,为了让事务之间相互不影响,每个事务进行操作的时候都会对数据加上一把特有的锁,防止其他事务同时操作数据。如果你想一个人静一静,不被别人打扰,那么请在你的房门上加上一把锁。
数据库里面的锁是基于索引实现的,在Innodb中我们的锁都是作用在索引上面的,当我们的SQL命中索引时,那么锁住的就是命中条件内的索引节点(行锁),如果没有命中索引的话,那我们锁的就是整个索引树(表锁)
数据库里有的锁有很多种,大致分别如下
- 基于锁的属性分类:共享锁、排他锁。
- 基于锁的粒度分类:表锁、行锁、记录锁、间隙锁、临键锁。
- 基于锁的状态分类:意向共享锁、意向排它锁。
1、共享锁(Share Lock)
共享锁又称读锁,简称S锁;当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。
共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。
益与 MVCC 的功劳,普通的 select 是不需要加锁的,而SELECT ... LOCK IN SHARE MODE;
这种读取需要对记录上 S 锁。SELECT ... FOR UPDATE;
需要对记录上 X 锁。
2、排他锁(eXclusive Lock)
排他锁又称写锁,简称X锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。
排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题。
3、表锁
表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问;
特点: 粒度大,加锁简单,容易冲突;
4、行锁
行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问;
特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高;
MyISAM 只支持表锁,一锁就锁整张表,而 InnoDB 不仅支持表锁,还支持粒度更低的行锁,仅对相关的记录上锁即可,所以对于写入操作来说 InnoDB 的性能更高。
5、记录锁(Record Lock)
记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。
触发条件:精准条件命中,并且命中的条件字段是唯一索引;
例如:update user_info set name=’张三’ where id=1
,这里的id是唯一索引。
记录锁的作用:加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。
6、间隙锁(Gap Lock)
间隙锁属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。
触发条件:范围查询并且查询未命中记录,查询条件必须命中索引、间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中。
间隙锁作用:防止幻读问题,事务并发的时候,如果没有间隙锁,就会发生如下图的问题,在同一个事务里,A事务的两次查询出的结果会不一样。
7、临键锁(Next-Key Lock)
临键锁也属于行锁的一种,并且它是 INNODB 的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。
例如:下面表的数据执行 select * from user_info where id>1 and id<=13 for update ;
会锁住ID为 1,5,10的记录;同时会锁住,1至5,5至10,10至15的区间。
触发条件:范围查询并命中,查询命中了索引。
临键锁的作用:结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。
二、MySQL MVCC概述
MVCC(Multi-Version Concurrency Control ,多版本并发控制),其实指的是一条记录会有多个版本,每次修改记录都会存储这条记录被修改之前的版本,多版本之间串联起来就形成了一条版本链。
这样不同时刻启动的事务可以无锁地获得不同版本的数据(普通读)。此时读(普通读)写操作不会阻塞,写操作可以继续写,无非就是多加了一个版本,历史版本记录可供已经启动的事务读取。
1、InnoDB与MVCC
MVCC只在 读已提交 和 可重复读 两个隔离级别下工作。其他两个隔离级别够和 MVCC不兼容, 因为 读未提交 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 串行化 则会对所有读取的行都加锁。
2、Redo log, bin log, Undo log
binlog:是mysql服务层产生的日志,常用来进行数据恢复、数据库复制,常见的mysql主从架构,就是采用slave同步master的binlog实现的, 另外通过解析binlog能够实现mysql到其他数据源(如ElasticSearch)的数据复制。
redo log:记录了数据操作在物理层面的修改,mysql 中使用了大量缓存,缓存存在于内存中,修改操作时会直接修改内存,而不是立刻修改磁盘,当内存和磁盘的数据不一致时,称内存中的数据为脏页(dirty page)。为了保证数据的安全性,事务进行中时会不断的产生 redo log,在事务提交时进行一次 flush 操作,保存到磁盘中, redo log 是按照顺序写入的,磁盘的顺序读写的速度远大于随机读写。当数据库或主机失效重启时,会根据redo log进行数据的恢复,如果redo log中有事务提交,则进行事务提交修改数据。这样实现了事务的原子性、一致性和持久性。
undo log::除了记录redo log外,当进行数据修改时还会记录undo log,undo log 用于数据的撤回操作,它记录了修改的反向操作,比如,插入对应删除,修改对应修改为原来的数据,通过 undo log 可以实现事务回滚,并且可以根据 undo log 回溯到某个特定的版本的数据,实现 MVCC。
3、快照读和当前读
对当前读、快照读理解,简单点说加锁就是当前读,不加锁的就是快照读。
给读操作加上共享锁、排它锁,DML操作加上排它锁,这些操作就是当前读。
共享锁、排它锁也被称之为读锁、写锁。共享锁与共享锁是共存的,但是要修改、添加、删除时,必须等到共享锁释放才可进行操作。因为在Innodb存储引擎中,DML操作都会隐式添加排它锁。所以说当前读所读取的记录就是最新的记录,读取数据时加上锁,保证其它事务不能修改当前记录。
快照读的前提是隔离级别不是串行级别,串行级别的快照读会退化成当前读。
快照读的出现旨在提高事务并发性,其实现基于MVCC。MVCC可以认为是行锁的一个变种,但是它在很多情况下避免了加锁操作。所以说快照读的数据有可能不是最新的,而是之前版本的数据。
实际上 InnoDB 不会真的存储了多个版本的数据,只是借助 undolog 记录每次写操作的反向操作,所以索引上对应的记录只会有一个版本,即最新版本。只不过可以根据 undolog 中的记录反向操作得到数据的历史版本,所以看起来是多个版本。
undo log除了实现MVCC外,还用于事务的回滚。MySQL Innodb中存在多种日志,除了错误日志、查询日志外,还有很多和数据持久性、一致性有关的日志。
三、MySQL Mvcc实现三大要素
MVCC实现原理是由俩个隐式字段、undo日志、Read view来实现的。
1、隐式字段
在Innodb存储引擎中,在有聚簇索引的情况下每一行记录中都会隐藏俩个字段,如果没有聚簇索引则还有一个 6byte 的隐藏主键。
这俩个隐藏列一个记录的是何时被创建的,一个记录的是什么时候被删除。这里不要理解为是记录的是时间,存储的是事务ID。
俩个隐式字段为 DB_TRX_ID、DB_ROLL_PTR,没有聚簇索引还会有DB_ROW_ID这个字段。
- DB_TRX_ID:记录创建这条数据上次修改它的事务 ID
- DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本
隐式字段实际还有一个 delete flag 字段,即记录被更新或删除,这里的删除并不代表真的删除,而是将这条记录的delete flag改为true(这里埋下一个伏笔,数据库的删除是真的删除吗?)
2. undo log(回滚日志)
之前对undo log的作用只提到了回滚操作实现原子性,现在需要知道的另一个作用就是实现MVCC多版本控制器。
undo log 细分为俩种,insert 时产生的 undo log,update,delete 时产生的 undo log
在 Innodb 中 insert 产生的 undo log 在提交事务之后就会被删除,因为新插入的数据没有历史版本,所以无需维护undo log。
update 和 delete 操作产生的 undo log 都属于一种类型,在事务回滚时需要,而且在快照读时也需要,则需要维护多个版本信息。只有在快照读和事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一删除。
purge 线程会清理 undo log 的历史版本,同样也会清理 del flag 标记的记录。
undo log在mvcc中的作用:
- undo log 保存的是一个版本链,也就是使用 DB_ROLL_PTR 这个字段来连接的。
- 当数据库执行一个select语句时会产生一致性视图
read view
。 - 那么这个 read view 是由查询时所有未提交事务 ID 组成的数组,数组中最小的事务ID为 min_id 和已创建的最大事务 ID 为 max_id 组成,查询的数据结果需要跟 read-view 做比较从而得到快照结果。
所以说 undo log 在 mvcc 中的作用就是为了根据存储的事务 ID 和一致性视图做对比,从而得到快照结果。
3. undo log底层实现
假设一开始的数据为下图
此时执行了一条更新的SQL语句update user set name = 'niuniu where id = 1'
,那么undo log的记录就会发生变化
也就是说当执行一条更新语句时会把之前的原有数据拷贝到undo log日志中。
同时你可以看见最新的一条记录在末尾处连接了一条线,也就是说DB_ROLL_PTR
记录的就是存放在undo log日志的指针地址。
最终有可能需要通过指针来找到历史数据。
4. read-view
当执行SQL语句查询时会产生一致性视图,也就是read-view,它是由查询的那一时间所有未提交事务ID组成的数组,和已经创建的最大事务ID组成的。
在这个数组中最小的事务ID被称之为min_id,最大事务ID被称之为max_id,查询的数据结果要根据read-view做对比从而得到快照结果。
于是就产生了以下的对比规则,这个规则就是使用当前的记录的trx_id跟read-view进行对比,对比规则如下。
5. 版本链对比规则
如果落在 trx_id < min_id,表示此版本是已经提交的事务生成的,由于事务已经提交所以数据是可见的
如果落在 trx_id > max_id,表示此版本是由将来启动的事务生成的,是肯定不可见的
若在min_id<=trx_id<=max_id时
- 如果row的trx_id在数组中,表示此版本是由还没提交的事务生成的,不可见,但是当前自己的事务是可见的
- 如果row的trx_id不在数组中,表明是提交的事务生成了该版本,可见
在这里还有一个特殊情况那就是对于已经删除的数据,在之前的undo log日志讲述时说了update和delete是同一种类型的undo log,同样也可以认为delete就是update的特殊情况。
当删除一条数据时会将版本链上最新的数据复制一份,然后将trx_id修改为删除时的trx_id,同时在该记录的头信息中存在一个delete flag标记,将这个标记写上true,用来表示当前记录已经删除。
在查询时按照版本链的规则查询到对应的记录,如果delete flag标记位为true,意味着数据已经被删除,则不返回数据。
如果你对这里的read-view的生成和版本链对比规则不懂,不要着急,也不要在这里浪费时间,请继续往下看,咔咔会使用一个简单的案例和一个复杂的案例给大家重现上述的规则。