笔记:mysql日志、事务

150 阅读34分钟

日志

InnoDB引擎在执行给ID=2这一行的c字段加1这个简单的update语句时的内部大致流程:

    1. 执行器先找引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中(Buffer Pool),就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
    1. 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据
    1. 引擎将这行新数据更新到内存中[在内存中的前提下],同时将这个更新操作记录到redo log里面
    1. 此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务
    1. 执行器生成这个操作的binlog,并把binlog写入磁盘
    1. 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成
    1. 使用两阶段提交后,写入binlog时发生异常也不会有影响,因为MySQL根据redo log日志恢复数据时,发现redo log还处于prepare阶段,并且没有对应binlog日志,就会回滚该事务。

redo log(重做日志)

    1. 引擎层[InnoDB引擎特有的日志]
    1. 物理日志(记录修改后的值)
    1. redo log是循环写的,空间固定会用完,会覆盖以前的日志
    1. 顺序io(磁盘顺序写)[写redo log 跟写数据到磁盘有一个很大的差异,那就是redo log 是顺序IO,而写数据到磁盘涉及到随机IO,写磁盘需要寻址,找到对应的位置,然后更新/添加/删除,而写 redo log 则是在一个固定的位置循环写入,是顺序IO,所以速度要高于写磁盘数据]
    1. redolog 的具体落盘操作是这样的:在事务运行的过程中,MySQL 会先把日志写到 redolog buffer 中[虽然redo log的磁盘顺序写已经很高效了,但是和内存操作还是有一定的差距,所以给redo log也加一 个内存buffer,就是redo log buffer。用来缓冲redo log写入的, 不同于binlog cache每个线程都 有一个, redolog buffer 只有那么一个],等到事务真正提交的时候,再统一把 redolog buffer 中的数据写到 redo log file中。不过这个从 redolog buffer 写到 redolog 文件中的操作也就是 write 并不就是落盘操作了,这里仅仅是把 redolog 写到了文件缓存系统(page cache)上,最后还需要执行 fsync 才能够实现真正的落盘。
    1. 数据库中innodb_flush_log_at_trx_commit参数就控制了在事务提交时,如何将buffer中的日志数据刷新到file中(fsync的时机)
  • InnoDB提供了配置参数innodb_flush_log_at_trx_commit
参数值为0:提交事务也不进行刷盘操作(即使我们将参数设为0,什么也不做,在数据库中
也有一个后台线程,默认每1秒就帮我们自动刷盘一次)
参数值为1:提交事务一次就刷盘一次(默认刷盘策略)
参数值为2:每次提交事务时,只会将redo log buffer中的内容写入页面缓存中(page 
cache),不会在将页面缓存中的数据刷盘到file(redo log file)中,因为是1秒刷盘一
次,所以如果宕机了,可能丢失一秒内的数据
    1. 除了通过参数控制事务提交时的fsync时机外,InnoDB有一个后台线程,每隔1s就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘
    1. 除了以上两种情况外,还有一种情况也会刷盘,就是当buffer中的记录数据大小达到最大值(16M)的一半的时候,也会自动刷盘(不过由于这个事务并没有提交,所以这个写盘动作只是 write 到了文件系统的 page cache,仍然是在内存中,并没有调用 fsync 真正落盘)

redo log file(日志文件)

MySQL默认目录中,有两个文件ib_logfile0和ib_logfile1,每次刷盘都是将数据刷新到
这两个文件内Redo日志文件有很多个,一般以日志文件组的形式出现,文件统一命名,格
式是ib_logfile+数字,从0开始。日志文件组中每个文件大小相同。
每次写入从0开始,然后是1,2,3…

binlog(归档日志)

    1. Server层
    1. 逻辑日志(用于记录用户对数据库更新的SQL语句信息,例如更改数据库表和更改内容的SQL语句都会记录到binlog里,但是对库表等内容的查询不会记录),binlog会记录所有涉及更新数据的逻辑操作,并且是顺序写
    1. binlog是可以追加写入的。“追加写”是指binlog文件,写到一定大小后会切换到下一个,并不会覆盖以前的日志。
    1. MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性
    1. binlog的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中
    1. 因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。
    1. 我们可以通过binlog_cache_size参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap)。
    1. redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入

binlog的写入机制
binlog的写入逻辑比较简单:

  • 事务执行过程中,先把日志写到binlog cache,这个操作会调用write方法,并没有把数据持久化到磁盘,速度比较快;
  • 事务提交的时候,再把binlog cache写到binlog文件中,这步会调用fsync将数据持久化到磁盘,速度比较慢。
  • write和fsync的时机,是由参数sync_binlog控制的:
 sync_binlog=0的时候,表示每次提交事务都只write,不fsync。
 sync_binlog=1的时候,表示每次提交事务都会执行fsync。
 sync_binlog=N(N>1)的时候,表示每次提交事务都write,但累积N个事务后才fsync。
  • 在出现IO瓶颈的场景中,将sync_binlog设置成一个比较大的值,可以提升性能。实际业务场景中,通常设置为100~1000中的某个数值。这样做对应的风险是:如果主机发生一场重启,会丢失最近N个事务的binlog。

binlog cache(相当于redo log的 redo log buffer)

    1. binlog cache 其实就是一片内存区域,充当缓存的作用,
    1. 每个线程都有自己 binlog cache 区域,在事务运行的过程中,MySQL 会先把日志写到 binlog cache 中,等到事务真正提交的时候,再统一把 binlog cache 中的数据写到 binlog 文件中。(binlog cache 有很多个,binlog 文件只有一个!)
    1. 事实上,这个从 binlog cache 写到 binlog 文件中的操作,并不一定就是落盘操作了(根据参数sync_binlog配置),这里仅仅是把 binlog 写到了文件系统的 page cache 上(write操作)
    1. 最后需要把 page cache 中的数据同步到磁盘上,才算真正完成了binlog的持久化(fsync操作)
    1. binlog与redo log的写入时机不一样

page cache

CPU 如果要访问外部磁盘上的文件,需要首先将这些文件的内容拷贝到内存中,由于硬件
的限制,从磁盘到内存的数据传输速度是很慢的,如果现在物理内存有空余,干嘛不用这些
空闲内存来缓存一些磁盘的文件内容呢,这部分用作缓存磁盘文件的内存就叫做 pagecache。

undo log版本链
我们知道数据库事务有回滚的能力,既然能够回滚,那么就必须要在数据改变之前先把旧的数据记录下来,作为将来回滚的依据,那么这个记录就是 undo log(在事务没提交之前,MySQL会先记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退)。

查询操作因为不涉及回滚操作,所以就不需要记录到 undo log 中

在 MySQL 的数据表中,存储着一行行的数据记录,对每行数据而言,不仅仅记录着我们定义的字段值,还会隐藏两个字段:row_trx_id 和 roll_pointer,前者表示更新本行数据的事务 id,后者表示的是回滚指针,它指向的是该行数据上一个版本的 undo log

当我们进行数据的新增、删除、修改操作时,会写 redo log(解决数据库宕机重启丢失数据的问题)和 binlog(主要用来做复制、数据备份等操作),另外还会写 undo log,它是为了实现事务的回滚操作。

每行 undo log 日志会记录对应的事务 id,还会记录当前事务将数据修改后的最新值,以及指向当前行数据上一个版本的 undo log 的指针,也就是 roll_pointer。 如下图:

image.png

举个例子,现在有一个事务 A,它的事务 id 为 10,向表中新插入了一条数据,数据记为 data_A,那么此时对应的 undo log 应该如下图所示:

image.png 由于是新插入的一条数据,所以这行数据是第一个版本,也就是它没有上一个数据版本,因此它的 roll_pointer 为 null。

接着事务 B(trx_id=20),将这行数据的值修改为 data_B,同样也会记录一条 undo log,如下图所示,这条 undo log 的 roll_pointer 指针会指向上一个数据版本的 undo log,也就是指向事务 A 写入的那一行 undo log。

image.png

只要有事务修改了这一行的数据,那么就会记录一条对应的 undo log,一条 undo log 对应这行数据的一个版本,当这行数据有多个版本时,就会有多条 undo log 日志,undo log 之间通过 roll_pointer 指针连接,这样就形成了一个 undo log 版本链

在更新数据之前,MySQL会提前生成undo log日志,当事务提交的时候,并不会立即删除undo log,因为后面可能需要进行回滚操作,要执行回滚(rollback)操作时,从缓存中读取数据。undo log日志的删除是通过后台purge线程进行回收处理的。

undo日志属于逻辑日志,redo是物理日志,所谓逻辑日志是undo log是记录一个操作过程,不会物理删除undo log,sql执行delete或者update操作都会记录一条undo日志。

fc419d4e5ba3aebb7e9566ea1417c30.jpg InnoDB Buffer Pool(缓冲池)

Buffer Pool是InnoDB存储引擎层的缓冲池,不属于MySQL的Server层
作用:
缓存了数据页,索引页、undo页、插入缓冲、自适应哈希索引、InnoDB存储的锁信息、数
据字典信息等
Buffer Pool 也是按页来划分的,默认和磁盘上的页一样,每页都是 `16KB` 大小

当需要访问某个页时(即使只访问页中的一条记录,也需要把整个页的数据加载到内存中),
就会把这一页的数据全部加载到缓冲池中,这样就可以在内存中进行读写访问了。对于数据
库中页的修改操作,也是先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。有了
缓冲池,就可以省去很多磁盘IO的开销了,从而提升数据库性能。

Buffer Pool的数据结构类似队列,采用变种LRU算法(最近最少使用),将LRU队列分成
新生代(new sublist)和老生代(old sublist)两个区域,新生代用来存热点数据页,
老生代用来存使用频率较低的数据页,一条缓存会先进老生代(老生代满了会把使用频率最
低的缓存移除),待够一定时间之后再挪到新生代

InnoDB在Buffer Pool中开辟了一块内存,用来存储变更记录(Change Buffer),默认占
BufferPool的25%,最大设置占用50%。为了防止异常宕机丢失缓存,会将变更记录记录到
redo log里面(当宕机时, `change buffer` 丢失,`redo log` 记录了数据的完整修
改记录,恢复时根据 `redo log` 重建 `change buffer`),等待时机更新磁盘的数据文
件(刷脏)
`change buffer` 虽然名上是 `buffer`。但其实它是可以持久化的,它持久化的地方默
认是 `ibdata1` 共享空间表中

注意:
通过索引只能定位到磁盘中的页,而不能定位到页中的一条记录。将页加载到内存后,就可
以通过页目录(Page Directory)去定位到某条具体的记录

change buffer和redo log的联系

一个事务要修改多张表的多条记录,多条记录分布在不同的Page里面,对应到磁盘的不同
位置。如果每个事务都直接写磁盘,一次事务提交就要多次磁盘的随机I/O,性能达不到要
求。
怎么办呢?
不写磁盘,在内存中进行事务提交。然后再通过后台线程,异步地把内存中的数据写入到磁盘中。
但有个问题:机器宕机,内存中的数据还没来得及刷盘,数据就丢失了。
为此,就有了Write-ahead Log的思路:
先在内存中提交事务,然后写日志(所谓的Write-ahead Log),然后后台任务把内存中
的数据异步刷到磁盘。日志是顺序地在尾部Append,从而也就避免了一个事务发生多次磁
盘随机 I/O 的问题。明明是先在内存中提交事务,后写的日志,为什么叫作Write-Ahead
呢?这里的Ahead,其实是指相对于真正的数据刷到磁盘,因为是先写的日志,后把内存数
据刷到磁盘,所以叫Write-Ahead Log。
具体到InnoDB中,`Write-Ahead Log`是Redo Log。`在InnoDB中,不光事务修改的数据
库表数据是异步刷盘的,连Redo Log的写入本身也是异步的`。

-   InnoDB的WAL 机制就是redo log
-   为何要WAL?避免随机写入 提高速度,log是顺序写入的

`注意:`(分别操作page1和page2):
1. Page 1在内存中,直接更新内存;
2. Page 2没有在内存中,就在内存的change buffer区域,记录下“我要往Page 2插入一
行”这个信息
3. 将上述两个动作记入redo log4. redo log会记录数据的变更和change buffer的变更,Page 1记录的是数据的变更,
Page 2记录的是change buffer的变更,Page 2数据的变更是发生在merge的时候
将change buffer中的操作应用到原数据页,得到最新结果的过程称为merge。除了访问这
个数据页会触发merge外,系统有后台线程会定期merge。在数据库正常关闭(shutdown)
的过程中,也会执行merge操作,merge的时候是真正进行数据更新的时刻

merge的执行流程是这样的:
1. 从磁盘读入数据页到内存(老版本的数据页);
2. 从change buffer里找出这个数据页的change buffer 记录(可能有多个),依次应
用,得到新版数据页;
3. 写redo log。这个`redo log包含了数据的变更和change buffer的变更`。
到这里merge过程就结束了。这时候,数据页和内存中change buffer对应的磁盘位置都还
没有修改,属于脏页,之后各自刷回自己的物理数据,就是另外一个过程了[change 
buffer会刷到ibdata1中,数据叶会刷到data对应数据叶中]。

在崩溃恢复场景中,InnoDB如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就
会将它读到内存,然后让redo log更新内存内容。更新完成后,内存页变成脏页,等待刷
盘。

image.png

image.png

ReadView 机制

什么是 Read View,说白了 Read View 就是事务进行快照读操作的时候生产的读视图 
(Read View),
在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前
活跃事务的 ID (当每个事务开启时,都会被分配一个 ID , 这个 ID 是递增的,所以最
新的事务,ID 值越大)
在当前事务开启的一瞬间系统会创建一个数组,数组中保存了目前所有的活跃事务 id,所
谓的活跃事务就是指已开启但是还没有提交的事务。

这个ReadView 会记录 4 个非常重要的属性:

  1. creator_trx_id: 当前事务的 id;
  2. m_ids: 当前系统中所有的活跃事务的 id,活跃事务指的是当前系统中开启了事务,但是还没有提交的事务;
  3. min_trx_id: 当前系统中,所有活跃事务中事务 id 最小的那个事务,也就是 m_id 数组中最小的事务 id;
  4. max_trx_id: 当前系统里面已经创建过的事务ID的最大值加1,也就是系统中下一个要生成的事务 id。

是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view, ReadView 会根据这 4 个属性,再结合 undo log 版本链,来实现 MVCC 机制,决定让一个事务能读取到哪些数据,不能读取到哪些数据。

Read View遵循一个可见性算法,主要是将记录中的 DB_TRX_ID(即当前行事务ID )取出
来,与系统当前其他活跃事务的 ID 去对比(由 Read View 维护),如果 DB_TRX_ID 
跟 Read View 的属性做了某些比较,不符合可见性,那就通过 DB_ROLL_PTR 回滚指针
去取出 Undo Log 中的 DB_TRX_ID再比较,即遍历链表的 DB_TRX_ID(从链首到链尾,
即从最近的一次修改查起),直到找到满足特定条件的 DB_TRX_ID , 那么这个 
DB_TRX_ID 所在的旧记录就是当前事务能看见的最新老版本

REPEATABLE READ隔离级别下:
当前事务想要去查看某一行数据的时候,会先去查看该行数据的DB_TRX_ID

  • [1] 如果这个值等于当前事务 id,说明这就是当前事务修改的,那么数据可见。
  • [2] 如果当前数据的 row_trx_id 小于 min_trx_id,那么表示这条数据是在当前事务开启之前,其他的事务就已经将该条数据修改了并提交了事务(事务的 id 值是递增的),所以当前事务能读取到。
  • [3] 如果这个值大于等于数组中的最大值,说明这行数据是我们在开启事务之后,还没有提交的时候,有另外一个会话也开启了事务,并且修改了这行数据,那么此时这行数据就是不可见的。
  • [4] 如果这个值的大小介于数组中最大值最小值之间(闭区间),且该值不在数组中,说明这也是一个已经提交的事务修改的数据,这是可见的。
  • [5] 如果这个值的大小介于数组中最大值最小值之间(闭区间),且该值在数组中(不等于当前事务 id),说明这是一个未提交的事务修改的数据,不可见。

READ COMMITTED隔离级别下:
READ COMMITTED 和 REPEATABLE READ 类似,区别主要是后者是同一个事务中的第一个快照读才会创建 Read View, 之后的快照读获取的都是同一个 Read View(创建数组列出活跃事务 id),而前者则每一个语句执行快照读都会生成并获取最新的 Read View

所以 READ COMMITTED 这种隔离级别会看到别的会话已经提交的数据(即使别的会话比当前会话开启的晚)。

注意:RR隔离级别下,如果一个事务的两个快照读之间插入了当前读,那么第二个快照读是不会复用之前的Read View的,会重新生成一个Read View

另外还有一个需要注意的地方,就是如果当前事务中涉及到数据的更新操作,
那么更新操作是在当前读的基础上更新的,而不是快照读的基础上更新的,
如果是后者则有可能导致数据丢失。
其实 MySQL 中的 update 就是先读再更新,读的时候默认就是当前读,即会加锁

这就是 MVCC,一行记录存在多个版本。实现了读写并发控制,读写互不阻塞;同时 MVCC 中采用了乐观锁,读数据不加锁,写数据只锁行,降低了死锁的概率;并且还能据此实现快照读。

注意:
MVCC 在一定程度上实现了读写并发(**为了实现读-写冲突不加锁,提高并发读写性能
**),不过它只在READ COMMITTED 和 REPEATABLE READ 两个隔离级别下有效。
而READ UNCOMMITTED 总是直接返回记录上的最新值,没有Read View概念,
SERIALIZABLE 则会对所有读取的行都加锁来避免并行访问,这两个都和 MVCC 不兼容。

问题

一个没有提交事务的 redo log 记录,也可能会刷盘 ps:(prepare)状态,所以不会有影响?为什么呢?
主要有三种可能的原因:

  • 第一种情况:InnoDB 有一个后台线程,每隔 1 秒轮询一次,具体的操作是这样的:调用 write 将 redolog buffer 中的日志写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。而在事务执行中间过程的 redolog 都是直接写在 redolog buffer 中的,也就是说,一个没有提交的事务的 redolog,也是有可能会被后台线程一起持久化到磁盘的。
  • 第二种情况:innodb_flush_log_at_trx_commit 设置是 1,这个参数的意思就是,每次事务提交的时候,都执行 fsync 将 redolog 直接持久化到磁盘(还有 0 和 2 的选择,0 表示每次事务提交的时候,都只是把 redolog 留在 redolog buffer 中;2 表示每次事务提交的时候,都只执行 write 将 redolog 写到文件系统的 page cache 中)。举个例子,假设事务 A 执行到一半,已经写了一些 redolog 到 redolog buffer 中,这时候有另外一个事务 B 提交,按照 innodb_flush_log_at_trx_commit = 1 的逻辑,事务 B 要把 redolog buffer 里的日志全部持久化到磁盘,这时候,就会带上事务 A 在 redolog buffer 里的日志一起持久化到磁盘
  • 第三种情况:redo log buffer 占用的空间达到 redolo buffer 大小(由参数 innodb_log_buffer_size控制,默认是 16MB)一半的时候,后台线程会主动写盘。不过由于这个事务并没有提交,所以这个写盘动作只是 write 到了文件系统的 page cache,仍然是在内存中,并没有调用 fsync 真正落盘

隔离级别

读未提交

一个事务还没提交时,它做的变更就能被别的事务看到(存在脏读、不可重复读以及幻象读问题)  

读提交

一个事务提交之后,它做的变更才会被其他事务看到(主要解决了脏读的问题,不可重复读以
及幻象读问题未解决)

可重复读

一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的(不管其他
事务是不是修改这个数据并提交了事务)
(解决了脏读的问题以及不可重复读的问题,没有完全解决幻读问题[快照读解决了幻读问
题,当前读没有,当前读依靠引入的间隙锁解决幻读问题])  

串行化

顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突
的时候,后访问的事务必须等前一个事务执行完成,才能继续执行(解决了脏读、不可重复
读、幻读)  
如果设置当前事务隔离级别为 SERIALIZABLE,那么此时开启其他事务时,就会阻塞,
必须等当前事务提交了,其他事务才能开启成功,因此前面的脏读、不可重复读以及幻象读
问题这里都不会发生。 

脏读

一个事务读到另外一个事务还没有提交的数据  

不可重复读

和脏读的区别在于,脏读是看到了其他事务未提交的数据,而不可重复读是看到了其他事务
已经提交的数据(由于当前 SQL 也是在事务中,因此有可能并不想看到其他事务已经提交
的数据)

幻象读

幻读:官方定义幻读是**两次读取的结果数量不一样**,也即在第一次查询后有其他事务在
第一次查询范围内insertdelete了数据,然后第二次查询(与第一次查询相同)后得到
的数据数量和第一次不一样,这就是幻读在读未提交和读已提交的隔离级别下会出现,在可
重复读的隔离级别下,快照读不会出现幻读,当前读时,innodb 通过间隙锁和行锁共同作
用控制,从而不会出现幻读现象。

快照读与当前读

快照读

`不加锁`select 操作就是快照读,即不加锁的非阻塞读,之所以出现快照读的情
况,是基于提高并发性能的考虑(为了实现读-写冲突不加锁,而这个读指的就是`快照读`,
而非当前读),快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的
一个种,但它在很多情况下,避免了加锁操作,降低了开销;
既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历
史版本
在事务开启的时候,会基于当前系统中数据库的数据,为每个事务生成一个快照,也叫做 
ReadView,后面这个事务所有的读操作都是基于这个 ReadView 来读取数据,这种读称之
为快照读。
[我们在实际的工作中,所使用的 SQL 查询语句基本都是快照读]  

快照读主要体现在select时,而不同隔离级别下,select 的行为不同
在 Serializable 隔离级别下 - 普通 select 也变成当前读,即加共享读锁。
在 RC 隔离级别下 - 每次 select 都会建立新的快照。  
在 RR 隔离级别下
  - 事务启动后,首次 select 会建立快照。
  - 如果事务启动选择了 with consistent snapshot,事务启动时就建立快照。
  - 基于旧数据的修改操作,会重新建立快照。
快照读本质上读取的是历史数据(原理是回滚段),属于无锁查询  

当前读

通过undo log 版本链,我们知道,每行数据可能会有多个版本,如果每次读取时,
「我们都强制性的读取最新版本的数据,这种读称之为当前读,也就是读取最新的数据,
读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁」。
什么样的 SQL 查询语句叫做当前读呢?例如在 select语句后面加上
**for update 或者 lock in share mode」**等。
另外 INSERT/DELETE/UPDATE 都属于当前读
**当前读实际上是一种加锁的操作,是悲观锁的实现**

mysql innodb中的锁

表锁

MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
第一种表锁的语法是 lock tables …read/write
另一类表级的锁是 MDL(metadata lock) 中文叫元数据锁,是从MySQL5.5开始引入的
锁,是为了解决DDL操作和DML操作之间操作一致性。从锁的作用范围上来说,MDL算是一种
表级锁,是一个server层的锁。
ps:
MDL操作 是对一个表做增删改查操作(MDL读锁)
DDL操作 是加字段等修改表结构的操作(MDL写锁)
MDL操作和DDL操作 统一都叫MDL锁,当我们对一个表做增删改查操作的时候,会自动加MDL读锁;
当我们要更新表结构的时候,加MDL写锁。
其实MDL加锁过程是系统自动控制,无法直接干预,也不需要直接干预,加读锁则所有线程
可正常读表的元数据,并且读锁不影响表的增删改查操作,只是不能修改表结构;
而加写锁只有拥有锁的线程可以读写元数据,即只拥有锁的线程才能更新表结构,其他线程
不能修改结构也不能执行相应的增删改查。

行锁

mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数
据加上排他锁,
select语句默认不会加任何锁类型,如果加排他锁可以使用selectfor update语句,
加共享锁可以使用select … lock in share mode语句。
所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和
lock in share mode锁的方式查询数据,但可以直接通过selectfrom…查询数据,因为
普通查询没有任何锁机制。
InnoDB行锁是通过给索引上的索引项加锁来实现的 只有通过索引条件检索数据,InnoDB
才使用行级锁,否则,InnoDB将使用表锁[InnoDB默认最小加锁粒度为行级锁,并且锁是加
在索引上,如果SQL语句未命中索引,即没有走索引树,使则走聚簇索引的全表扫描,表上
每条记录都会上锁,相当于是表锁,如果sql语句走了索引树,会把扫描过程中碰到的行,
也都加上写锁(也叫行排它锁),还有一种特殊情况,就是如果where之后的条件是索引,但
数据是不存在的,这种情况依旧会走这个索引树,到叶子节点的时候发现没有该数据,返回
结果为空,这不是全表扫描,不会锁表的,同理扫描索引过程中没有碰到行,所以也不会锁行]

意象锁

除了上面所说的两种表锁外,意象锁也属于表锁
意向锁分为两类:
意向共享锁:ISselect ... lock in share mode
意向排他锁:IX,insertupdatedeleteselect ... for update
从上面的语句可以看出意向共享锁主要对应查询操作,意向排他锁对应更新操作。
行锁之间的竞争关系是行锁与行锁的竞争,意向锁并不会参与其中,任意两个意向锁之间也
不会产生冲突
意向锁与表锁的互斥情况:
意向共享锁 x 表共享锁(也叫读锁、read、S) 兼容
意向共享锁 x 表排它锁(也叫写锁、write、X) 互斥
意向排他锁 x 表共享锁 互斥
意向排他锁 x 表共享锁 互斥

表锁与行锁是有互斥关系的:
行共享锁 x 表共享锁 兼容
行共享锁 x 表排他锁 互斥
行排他锁 x 表共享锁 互斥
行排他锁 x 表排他锁 互斥
所以当要给表加写锁的时候,理论上必须得先确定这张表上面的记录有没有被其他事务加
锁,如果有的话,当前事务应该加锁失败,判断是否存在行锁需要一行一行遍历每条数据,
分别去判断每行有没有被锁住效率太低,所以增加了意象锁

间隙锁(Gap Lock)

仅在RR隔离级别下支持。
跟间隙锁存在冲突关系的,是 "往这个间隙中插入一个记录" 这个操作 间隙锁之间都不存
在冲突关系(两个事务可以同时获取一个间隙锁,但都不能往这个间隙插入记录)。

临键锁(Next-Key Lock)

行锁和间隙锁组合,同时锁住数据,并锁住数据'前面'的间隙Gap。仅在RR隔离级别下支持。

关于读未提交事务隔离级别下的一些问题

说在前面 **写锁仅仅是阻止其他事务施加读锁,而不是禁止事务读取数据**
MySQL在读未提交隔离级别下的写操作是加了排它锁的但查询不会加MDL读锁。(写锁是必须
的,否则事务连原子性都不能保证。)一个事务会脏读,即读取到另一个事务加了写锁的数
据修改,这是因为读未提交下,事务在读取数据时,是不会加读锁的,所以读取数据不会被
另一个事务的写锁阻止。
但当隔离级别提高到读已提交,一个事务就不能读取到另一个事务加了写锁的数据了。
因为读已提交会在读取数据前,先加读锁,这会被另一个事务的写锁阻止,导致读取失败。
所以,"加了写锁的数据就无法被其他事务读取,这一结论实现的前提是隔离级别至少达到
了读已提交"

间隙锁和临键锁的加锁规则

原则1:加锁的基本单位是next-key lock。希望你还记得,next-key lock是前开后闭区
间(理解:左开右闭。(5,10],其中(5,10)是间隙锁,10是行锁。在特定条件下next-
key lock会退化成间隙锁或者行锁)
原则2:查找过程中访问到的对象才会加锁。(如果某个查询使用覆盖索引,并不需要访问主
键索引,那主键索引上不会加任何锁 注意:lock in share mode情况下只锁覆盖索引,但
是如果是for update就不一样了。 执行 for update时,系统会认为你接下来要更新数
据,因此会顺便给主键索引上满足条件的行加上行锁) 
优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。(若[唯
一索引]由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么如果查询其实
是范围类型查询,而不是精准类型查询,InnoDB存储引擎会使用Next-Key Lock进行锁定。)
优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key
lock退化为间隙锁。

当扫描表索引时,会在遇到的索引记录上设置共享或[排他锁]所以行锁实际上是索引记录
锁,索引记录锁上的行锁会影响索引记录之前的间隙。(而行锁可能会根据优化2变为间隙
锁)

next-key lock实际上是由间隙锁加行锁实现的。如果切换到读提交隔离级别(read-
committed)的话,就好理解了,过程中去掉间隙锁的部分,也就是只剩下行锁的部分。

可重复读隔离级别遵守两阶段锁协议,所有加锁的资源,都是在事务提交或者回滚的
时候才释放的。

在读提交隔离级别下还有一个优化,即:语句执行过程中加上的行锁,在语句执行完成
后,就要把“不满足条件的行”上的行锁直接释放了,不需要等到事务提交。
也就是说,读提交隔离级别下,锁的范围更小,锁的时间更短,这也是不少业务都默认使用
读提交隔离级别的原因

检索条件必须有索引(没有索引的话,mysql会全表扫描,那样会锁定整张表所有的记录,
包括不存在的记录,此时其他事务不能修改不能删除不能添加)

插入意象锁

插入意向锁是间隙锁的一种,专门针对 insert 操作,多个事务在同一个索引同一个范围
区间插入记录时候,如果插入位置不冲突,不会彼此阻塞,隔离级别还是 RR

但是需要注意:需要强调的是,虽然`插入意向锁`中含有`意向锁`三个字,但是它并不属
于`意向锁`而属于`间隙锁`,因为`意向锁`**表锁**`插入意向锁`**行锁**。

普通的间隙锁不允许 在(上一条记录,本记录)范围内插入数据
插入意向锁允许在(上一条记录,本记录)范围内插入数据

插入意向锁的作用是为了提高并发插入的性能,多个事务同时写入不同数据至同一索
引范围(区间)内,并不需要等待其他事务完成,不会发生锁等待。

插入过程:
假设现在有记录 10, 30, 50, 70 ;且为主键 ,需要插入记录 25 。
1. 找到 小于等于25的记录 ,这里是 10
2. 找到 记录10的下一条记录 ,这里是 30
3. 判断 下一条记录30 上是否有锁
   3.1 判断 30 上面如果没有锁 ,则可以插入
   3.2 判断 30 上面如果有(行锁)`Record Lock`,则可以插入
   3.3 判断 30 上面如果有`(间隙锁)Gap Lock / (临建锁)Next-Key Lock`,则无法
   插入,因为锁的范围是 (10, 30) /(10, 30] ;在30上增加`(插入意象锁)insert 
   intention lock`( 此时处于waiting状态),当 Gap Lock / Next-Key Lock 释
   放时,等待的事物将被唤醒 ,此时记录30 上才能获得 insert intention lock , 
   然后再插入记录25
注意:一个事物 insert 25 且没有提交,另一个事物 delete 25 时,记录25上会有
Record Lock

假如没有插入意向锁,而是用普通的间隙锁。插入数据时会获取这条记录所在区间的间隙锁
及这条记录的排它锁,其他事务是不可能在这个区间内插入数据的,因为当前事务已经获取
了这个区间内的间隙锁,其他事务无法获取对应记录的排它锁,只能等待其他事务完成;

用插入意向锁后,数据库设计插入意向锁与排它锁不互斥。多个事务既可以获取对应区间的
插入意向锁也可以获取对应记录的排它锁,各个事务互不影响,不需要等待其他事务完成后
才能进行插入。