查看是否开启
SHOW VARIABLES LIKE 'optimizer_trace';
开启功能
SET optimizer_trace="enabled=on";
查看具体的优化过程
SELECT * FROM information_schema.OPTIMIZER_TRACE;
这个OPTIMIZER_TRACE表有4个列,分别是:
1. 打开optimizer trace功能 (默认情况下它是关闭的):
SET optimizer_trace="enabled=on";
2. 这里输入你自己的查询语句
SELECT ;
3. 从OPTIMIZER_TRACE表中查看上一个查询的优化过程
SELECT * FROM information_schema.OPTIMIZER_TRACE;
4. 可能你还要观察其他语句执行的优化过程,重复上面的第2、3步
5. 当你停止查看语句的优化过程时,把optimizer trace功能关闭
SET optimizer_trace="enabled=off";
化过程大致分为了三个阶段:
prepare阶段
optimize阶段
execute阶段
于单表查询来说,我们主要关注optimize阶段的"rows_estimation"这个过程
对于多表连接查询来说,我们更多需要关注"considered_execution_plans"这个过程
InnoDB的设计者为了存磁盘中的页,在MySQL服务器启动的时候就向操作系统申请了一片连续的内存,他们给这片内存起了个名,叫做Buffer Pool。
默认情况下Buffer Pool只有128M大小。当然如果你嫌弃这个128M太大或者太小,可以在启动服务器的时候配置innodb_buffer_pool_size参数的值,是以字节为单位的
需要注意的是,Buffer Pool也不能太小,最小值为5M(当小于该值时会自动设置成5M)。
我们就把每个页对应的控制信息占用的一块内存称为一个控制块吧,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前面,缓存页被存放到 Buffer Pool 后边,所以整个Buffer Pool对应的内存空间看起来就是这样的:
把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作free链表
为了管理好这个free链表,特意为这个链表定义了一个基节点,里边儿包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。这里需要注意的是,链表的基节点占用的内存空间并不包含在为Buffer Pool申请的一大片连续内存空间之内,而是单独申请的一块内存空间。
有了这个free链表之后事儿就好办了,每当需要从磁盘中加载一个页到Buffer Pool中时,就从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的free链表节点从链表中移除,表示该缓存页已经被使用了
我们修改了Buffer Pool中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页,我们创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫flush链表,数据结构与free链表一样
1、读取时的Hash处理:
以用表空间号 + 页号作为key,缓存页作为value创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。
2、修改数据时脏页处理:
从flush链表中异步同步脏页数据,这种刷新页面的方式被称之为BUF_FLUSH_LIST
从LRU链表的冷数据中刷新一部分页面到磁盘,方式被称之为BUF_FLUSH_LRU
3、查看状态
SHOW ENGINE INNODB STATUS语句来查看关于InnoDB存储引擎运行过程中的一些状态信息,其中就包括Buffer Pool的一些信息
缓存的页占用的内存大小超过了Buffer Pool大小要进行删除不常用的页
简单设计:
只要我们使用到某个缓存页,就把该缓存页调整到LRU链表的头部,这样LRU链表尾部就是最近最少使用的缓存页喽
但是有两种情况会导致缓存命中大大降低:
1、预读功能
读取一个区,另一个区也会预先读取BufferPool
读取一个区的连续13个页,整个区也会读取到BufferPool
2、全表扫描
这两种情况可能并不是常用数据但是会导致很多BufferPool的数据被删掉
设计InnoDB的大佬把这个LRU链表按照一定比例分成两截,分别是:
一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称young区域。
另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据,或者称old区域。
我们可以通过查看系统变量innodb_old_blocks_pct的值来确定old区域在LRU链表中所占的比例
SHOW VARIABLES LIKE 'innodb_old_blocks_pct';
默认情况下,old区域在LRU链表中所占的比例是37%
针对上面两种情况:
1、当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部。这样针对预读到Buffer Pool却不进行后续访问的页面就会被逐渐从old区域逐出,而不会影响young区域中被使用比较频繁的缓存页。
2、针对全表扫描时,在进行全表扫描时,虽然首次被加载到Buffer Pool的页被放到了old区域的头部,但是后续会被马上访问到,每次进行访问的时候又会把该页放到young区域的头部
在对某个处在old区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。
时间间隔由一下参数决定:
SHOW VARIABLES LIKE 'innodb_old_blocks_time';
更进一步优化LRU链表:只有被访问的缓存页位于young区域的1/4的后边,才会被移动到LRU链表头部,这样就可以降低调整LRU链表的频率,从而提升性能。
还有其他的需要补充
Atomicity:要不全部成功,要么全部失败. 事务是原子的,要么全部执行成功,要么全部失败。如果事务中的任何一部分失败,系统应该回滚到事务开始前的状态,以保持数据库的一致性。
Isolation:现场A对线程B不可见, 事务的执行应该相互隔离,一个事务的执行不应该影响其他事务的执行。这是通过隔离级别来控制的,如读未提交、读提交、可重复读和串行化。
Consistency:事务执行前后数据库应该保持一致性。这包括应用事务中定义的业务规则和完整性约束。例如,数据库中的主键、外键和其他约束条件在事务执行前后都应该得到满足。
Durability:一旦事务被提交,对数据库的更改应该是永久性的,并且在数据库发生故障或系统崩溃时也应该能够恢复。
活动的(active)
部分提交
失败的
终止的
提交的
1、开启事务:BEGIN [WORK];
BEGIN语句代表开启一个事务,后边的单词WORK可有可无。开启事务后,就可以继续写若干条语句,这些语句都属于刚刚开启的这个事务。
或者:START TRANSACTION;
START TRANSACTION语句和BEGIN语句有着相同的功效,都标志着开启一个事务
READ ONLY:标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
READ WRITE:标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
WITH CONSISTENT SNAPSHOT:启动一致性读
可以这样写:
START TRANSACTION READ ONLY, WITH CONSISTENT SNAPSHOT;//开启一个只读事务和一致性读
START TRANSACTION READ WRITE, WITH CONSISTENT SNAPSHOT //开启一个读写事务和一致性读
2、提交事务:COMMIT [WORK]
3、回滚事务:ROLLBACK [WORK]
4、自动提交
SHOW VARIABLES LIKE 'autocommit';
5、隐式提交
显示的提交:当我们使用START TRANSACTION或者BEGIN语句开启了一个事务,或者把系统变量autocommit的值设置为OFF时,事务就不会进行自动提交。
SAVEPOINT 保存点名称;
回滚到某个保存点时
ROLLBACK [WORK] TO [SAVEPOINT] 保存点名称;
redo日志是InnoDB存储引擎的一部分,用于保证事务的持久性和恢复能力。
在InnoDB中,redo日志是以物理方式记录的,而不是直接写入磁盘的数据文件。它包含了对数据库所做的所有修改操作,包括插入、更新和删除等。redo日志的作用是在发生故障或崩溃时,通过重做日志中的操作来恢复数据库到最新的一致状态。
redo日志存储在InnoDB的日志文件中,通常称为redo日志文件或者事务日志文件。在MySQL的数据目录下,可以找到以ib_logfile开头的文件,例如ib_logfile0、ib_logfile1等。这些文件就是InnoDB的redo日志文件。
redo日志文件是循环写入的,当写满一个日志文件后,会继续写入下一个日志文件。当所有的日志文件都写满后,InnoDB会将最旧的日志文件复用,覆盖其中的内容。因此,redo日志文件的大小和数量可以通过配置参数进行调整,以适应不同的需求。
真正访问页面之前,需要把在磁盘上的页缓存到内存中的Buffer Pool之后才可以访问,如果一个事务对于数据做了修改,也是现在BufferPool中生效,后面再刷到磁盘中,但是如果还没有刷到磁盘中导致在事务提交之后出现了事故,那么可能会产生一致性问题。
因此提出了redo日志,也叫重写日志。原理是在提交事务之前把修改了哪些东西记录一下,系统奔溃重启时需要按照记录的步骤重新更新数据页即可。
redo日志有两个优势:
redo日志占用的空间非常小
redo日志是顺序写入磁盘的
redo日志格式:
重启进行恢复时也要保证原子性,因此在redo日志中必须要以一个类型为MLOG_MULTI_REC_END结尾,在系统奔溃重启进行恢复时,只有当解析到类型为MLOG_MULTI_REC_END的redo日志,才认为解析到了一组完整的redo日志,才会进行恢复。否则的话直接放弃前面解析到的redo日志。
当然,如果只有一条语句,那么将会以type字段的第一个比特位为1,代表该需要保证原子性的操作只产生了单一的一条redo日志,不需要关注MLOG_MULTI_REC_END,否则表示该需要保证原子性的操作产生了一系列的redo日志。
通过mtr生成的redo日志都放在了大小为512字节的页中,称为block。
在这里插入图片描述
写入时会把block写入缓冲区,这个缓冲区叫做log buffer(redo日志缓冲区),我们可以通过启动参数innodb_log_buffer_size来指定log buffer的大小,默认是16M,刷入到缓存就会找时机将缓存数据刷到磁盘中。
redo日志刷盘时机:
1、log buffer空间不足时:占据大约1半就会刷
2、事务提交时
3、后台线程不停的刷刷刷: 后台有一个线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘。
4、正常关闭服务器时
5、做的checkpoint时
6、其他
SHOW VARIABLES LIKE 'datadir’查看
下默认有两个名为ib_logfile0和ib_logfile1的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。
参数调节:
innodb_log_group_home_dir
该参数指定了redo日志文件所在的目录,默认值就是当前的数据目录。
innodb_log_file_size
该参数指定了每个redo日志文件的大小,在MySQL 5.7.21这个版本中的默认值为48MB,
innodb_log_files_in_group
该参数指定redo日志文件的个数,默认值为2,最大值为100。
没看完redo日志(下)
我们说过事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。但是偏偏有时候事务执行到一半会出现一些情况,比如:
情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。
情况二:程序员可以在事务执行过程中手动输入ROLLBACK语句结束当前的事务的执行。
我们要对一条记录做改动时(这里的改动可以指INSERT、DELETE、UPDATE),都需要留一手 —— 把回滚时所需的东西都给记下来,把这些为了回滚而记录的这些数据称之为撤销日志,英文名为undo log,称之为undo日志。
InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志。
一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的undo日志,这些undo日志会被从0开始编号,也就是说根据生成的顺序分别被称为第0号undo日志、第1号undo日志、…、第n号undo日志等,这个编号也被称之为undo no。 这些undo日志是被记录到类型为FIL_PAGE_UNDO_LOG的页面中,这些页面可以从系统表空间中分配,也可以从一种专门存放undo日志的表空间,也就是所谓的undo tablespace中分配。
事务id是怎么生成的?
聚簇索引的记录除了会保存完整的用户数据以外,而且还会自动添加名为trx_id、roll_pointer的隐藏列,如果用户没有在表中定义主键以及UNIQUE键,还会自动添加一个名为row_id的隐藏列。
就是某个对这个聚簇索引记录做改动的语句所在的事务对应的事务id而已,此处的改动可以是INSERT、DELETE、UPDATE操作
例如:
SELECT * FROM information_schema.innodb_sys_tables;
InnoDB设计了一个类型为TRX_UNDO_INSERT_REC的undo日志,它的完整结构如下图所示:
需要了解几点:
实例:
插入两条数据
BEGIN; # 显式开启一个事务,假设该事务的id为100
# 插入两条记录
INSERT INTO undo_demo(id, key1, col)
VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪')
记录两条undo日志
第一条undo日志的undo no为0,记录主键占用的存储空间长度为4,真实值为1。画一个示意图就是这样:
第二条undo日志的undo no为1,记录主键占用的存储空间长度为4,真实值为2。画一个示意图就是这样(与第一条undo日志对比,undo no和主键各列信息有不同):
首先了解一下delete mark操作:
Delete mark(删除标记)是InnoDB存储引擎中的一种操作,用于标记数据行被删除的状态。它是InnoDB实现MVCC(多版本并发控制)的关键机制之一。
在InnoDB中,当执行DELETE操作时,并不会立即删除数据行,而是通过设置一个特殊的删除标记来标记该行为已删除状态。这个删除标记称为delete mark。
Delete mark的作用是在事务的隔离级别为可重复读(REPEATABLE READ)或更高级别时,保证在事务执行期间,其他事务不会读取到已删除的数据行。
当一个事务需要读取数据时,InnoDB会根据事务的隔离级别判断是否需要忽略已经被删除的数据行。如果事务的隔离级别为可重复读或更高级别,InnoDB会使用delete mark来判断数据行是否可见。如果数据行被标记为已删除,则InnoDB会忽略这些数据行,不将其包含在查询结果中。
需要注意的是,delete mark并不是永久性的删除数据行,而是在事务提交后,由后台的purge线程负责清理已删除的数据行。purge线程会根据事务的提交时间和MVCC的版本链来判断哪些数据行可以被永久删除。
总结起来,delete mark是InnoDB存储引擎中的一种操作,用于标记已删除的数据行。它是实现MVCC的关键机制之一,用于保证在事务的隔离级别为可重复读或更高级别时,其他事务不会读取到已删除的数据行。删除标记并不是永久性的删除数据行,而是在事务提交后由purge线程清理。
插入到页面中的记录会根据记录头信息中的next_record属性组成一个单向链表,我们把这个链表称之为正常记录链表
被删除的记录其实也会根据记录头信息中的next_record属性组成一个链表,只不过这个链表中的记录占用的存储空间可以被重新利用,所以也称这个链表为垃圾链表,Page Header部分有一个称之为PAGE_FREE的属性,它指向由被删除记录组成的垃圾链表中的头节点。
删除流程如下:
1、仅仅将记录的delete_mask标识位设置为1,其他的不做修改
正常记录链表中的最后一条记录的delete_mask值被设置为1,但是并没有被加入到垃圾链表。也就是此时记录处于一个中间状态
在删除语句所在的事务提交之前,被删除的记录一直都处于这种所谓的中间状态。
2、当该删除语句所在的事务提交之后,会有专门的线程后来真正的把记录删除掉。
所谓真正的删除就是把该记录从正常记录链表中移除,并且加入到垃圾链表中,然后还要调整一些页面的其他信息,比如页面中的用户记录数量PAGE_N_RECS、上次插入记录的位置PAGE_LAST_INSERT、垃圾链表头节点的指针PAGE_FREE、页面中可重用的字节数量PAGE_GARBAGE、还有页目录的一些信息等等。设计InnoDB的大佬把这个阶段称之为purge。
undo日志的操作:
1、在对一条记录进行delete mark操作前,需要把该记录的旧的trx_id和roll_pointer隐藏列的值都给记到对应的undo日志中来
# 删除一条记录
DELETE FROM undo_demo WHERE id = 1;
这个delete mark操作对应的undo日志的结构就是这样:
在执行UPDATE语句时,InnoDB对更新主键和不更新主键这两种情况有截然不同的处理方案。
不更新主键的情况
1、就地更新(in-place update)
2、先删除掉旧记录,再插入新记录
我们这里所说的删除并不是delete mark操作,而是真正的删除掉,也就是把这条记录从正常记录链表中移除并加入到垃圾链表中
更新主键的情况
1、将旧记录进行delete mark操作
2、根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中(需重新定位插入的位置)。
undo日志,默认存储在数据目录下的系统表空间文件中。系统表空间文件的默认命名为ibdata1。
从MySQL 5.7.6版本开始,InnoDB引入了多表空间的概念,可以将Undo表空间存储在独立的文件中,而不再与系统表空间混合。这样可以更好地管理Undo表空间的大小和回收。
表空间的具体数据都是以页的形式存储的,undo日志也不例外,被称为FIL_PAGE_UNDO_LOG的页
执行以下查询语句,查看InnoDB的undo日志信息:
SHOW ENGINE INNODB STATUS\G
找到查询结果中的"Innodb Status"部分,并查找到"TRANSACTIONS"标签下的"History list length"。
例如:
Trx id counter 123456
Purge done for trx's n: 0:123456
Undo log entries 10 // 这里是undo日志的长度
History list length 1000 这里是undo历史列表的长度
因为一个事务可能包含多个语句,而且一个语句可能对若干条记录进行改动,而对每条记录进行改动前,都需要记录1条或2条的undo日志,所以在一个事务执行过程中可能产生很多undo日志,这些日志可能一个页面放不下,需要放到多个页面中,这些页面就通过我们上面介绍的TRX_UNDO_PAGE_NODE属性连成了链表
在一个事务执行过程中,可能混着执行INSERT、DELETE、UPDATE语句,也就意味着会产生不同类型的undo日志。但是我们前面又强调过,同一个Undo页面要么只存储TRX_UNDO_INSERT大类的undo日志,要么只存储TRX_UNDO_UPDATE大类的undo日志,反正不能混着存,所以在一个事务执行过程中就可能需要2个Undo页面的链表,一个称之为insert undo链表,另一个称之为update undo链表。
另外,设计InnoDB的大佬规定对普通表和临时表的记录改动时产生的undo日志要分别记录(我们稍后阐释为什么这么做),所以在一个事务中最多有4个以Undo页面为节点组成的链。
总结:
为了尽可能提高undo日志的写入效率,不同事务执行过程中产生的undo日志需要被写入到不同的Undo页面链表中。
存储结构是也采用了段,称为:Segment Header,每一个Undo页面链表都对应着一个段,称之为Undo Log Segment
段的结构为:
一个事务在向Undo页面中写入undo日志时的方式是十分简单暴力的,就是直接往里怼,写完一条紧接着写另一条,各条undo日志之间是亲密无间的。写完一个Undo页面后,再从段里申请一个新页面,然后把这个页面插入到Undo页面链表中,继续往这个新申请的页面中写。
Undo页面链表的第一个页面在真正写入undo日志前,其实都会被填充Undo Page Header、Undo Log Segment Header、Undo Log Header这3个部分
重用情形:
1、该链表中只包含一个Undo页面。
2、insert undo链表。
insert undo链表中只存储类型为TRX_UNDO_INSERT_REC的undo日志,这种类型的undo日志在事务提交之后就没用了,就可以被清除掉。所以在某个事务提交后,重用这个事务的insert undo链表(这个链表中只有一个页面)时,可以直接把之前事务写入的一组undo日志覆盖掉,从头开始写入新事务的一组undo日志
3、update undo链表
在一个事务提交后,它的update undo链表中的undo日志也不能立即删除掉(这些日志用于MVCC)。所以如果之后的事务想重用update undo链表时,就不能覆盖之前事务写入的undo日志。这样就相当于在同一个Undo页面中写入了多组的undo日志
undo日志(下)
脏读:读取未提交数据导致的
原因:一个事务修改了另一个未提交事务修改过的数据
不可重复度:重复读取同一个数据两次结果不一样
原因:一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值
幻读:范围查找时多了数据
原因:个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来
严重性:脏写 > 脏读 > 不可重复读 > 幻读
设立一些隔离级别,隔离级别越低,越严重的问题就越可能发生
Oracle就只支持READ COMMITTED和SERIALIZABLE
设置方法:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
可选:
level: {
REPEATABLE READ
| READ COMMITTED
| READ UNCOMMITTED
| SERIALIZABLE
}
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL的UNIQUE键时都不会包含row_id列):
1、对于使用READ UNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了
2、对于使用SERIALIZABLE隔离级别的事务来说,设计InnoDB规定使用加锁的方式来访问记录
3、对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的
为了完成上述的不同阶段的读取要求,需要判断一下版本链中的哪个版本是当前事务可见的,提出了一个ReadView的概念,这个ReadView中主要包含4个比较重要的内容:
MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。
在READ COMMITTED隔离级别中,ReadView是在每个SQL语句执行前都会生成的。这意味着在事务中的同一个查询可能会返回不同的结果,因为每次查询都会看到最新的数据。
而在REPEATABLE READ隔离级别中,ReadView是在事务开始时就生成的,并且会一直保持到事务结束。这就保证了在同一个事务中,所有的查询都将看到同样版本的数据。
例如:
例如,考虑以下两个事务:
事务A:
START TRANSACTION;
SELECT * FROM table WHERE id = 1; -- 返回空集
INSERT INTO table (id, name) VALUES (1, 'John');
COMMIT;
事务B:
START TRANSACTION;
SELECT * FROM table WHERE id = 1; -- 可能返回空集,也可能返回(1, 'John')
COMMIT;
如果这两个事务在READ COMMITTED隔离级别下执行,那么事务B可能会在事务A提交后才看到新插入的数据。但是,如果这两个事务在REPEATABLE READ隔离级别下执行,那么事务B将始终看不到新插入的数据,因为它的ReadView是在事务开始时生成的。
所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
怎么解决脏读、不可重复读、幻读这些问题呢?其实有两种可选的解决方案:
方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁。
方案二:读、写操作都采用加锁的方式。
事务利用MVCC进行的读取操作称之为一致性读,或者一致性无锁读,有的地方也称之为快照读。所有普通的SELECT语句(plain SELECT)在READ COMMITTED、REPEATABLE READ隔离级别下都算是一致性读。
一致性读并不会对表中的任何记录做加锁操作,其他事务可以自由的对表中的记录做改动。
共享锁,英文名:Shared Locks,简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁。也称为读锁
独占锁,也常称排他锁,英文名:Exclusive Locks,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁,也称为写锁
两者兼容性如下:
读读兼容
读写不兼容
写写不兼容
总而言之:只有两个线程都是读锁能同时读,其他都不行
锁定读的语句
SELECT ... LOCK IN SHARE MODE; //对读取的记录加S锁
SELECT ... FOR UPDATE; //对读取的记录加X锁
DELETE
对一条记录做DELETE操作的过程其实是先在B+树中定位到这条记录的位置,然后获取一下这条记录的X锁,然后再执行delete mark操作
UPDATE
在对一条记录做UPDATE操作时分为三种情况:
如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化,则先在B+树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作。其实我们也可以把这个定位待修改记录在B+树中位置的过程看成是一个获取X锁的锁定读。
如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取X锁的锁定读,新插入的记录由INSERT操作提供的隐式锁进行保护。
如果修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETE和INSERT的规则进行了。
INSERT
一般情况下,新插入一条记录的操作并不加锁,不过InnoDB通过一种称之为隐式锁的东西来保护这条新插入的记录在本事务提交前不被别的事务访问
前面提到的锁都是针对记录的,也可以被称之为行级锁或者行锁,对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在表级别进行加锁,自然就被称之为表级锁或者表锁
但是表锁加上会有性能问题,因此有了意向锁:
假设我们有一个表T,有两行数据A和B。如果事务T1想要获取A和B的共享锁,它需要在表级别加一个IS锁。如果此时另一个事务T2想要获取A或B的独占锁,它必须等待T1释放IS锁后才能获取IX锁。这样可以防止两个事务同时修改同一行数据,从而保证了数据的一致性。
IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实IS锁和IX锁是兼容的,IX锁和IX锁是兼容的
当一个事务锁定了一条记录时,其他事务不能修改或删除这条记录,直到第一个事务提交或回滚其更改。
记录锁是基于索引的,也就是说,当对某个索引上的数据行进行操作时MySQL会自动获取或释放相应的记录锁
对于使用非唯一索引的数据行,InnoDB引擎使用的是共享锁(Shared Lock),多个事务可以同时对相同的数据行进行读操作。
对于唯一索引的数据行,InnoDB引擎使用的是排他锁(Exclusive Lock),一次只能有一个事务对数据行进行写操作。
Gap Locks是一种锁定机制,用于防止幻读。当一个事务执行范围查询(例如SELECT … WHERE … BETWEEN … AND … FOR UPDATE或SELECT … WHERE … > … FOR UPDATE)时,InnoDB会对查询结果集中的每个行加上一个gap lock,以防止其他事务在这个范围内插入新的行。
虽然有共享gap锁和独占gap锁这样的说法,但是它们起到的作用都是相同的
Next-Key Locks结合了记录锁(Record Lock)和间隙锁(Gap Locks),用于处理范围查询时的并发问题。
Next-Key Locks的特点和工作原理如下:
插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁
摘录链接
Mysql是怎样运行的