日志
InnoDB引擎在执行给ID=2这一行的c字段加1这个简单的update语句时的内部大致流程:
-
- 执行器先找引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中(Buffer Pool),就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
-
- 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据
-
- 引擎将这行新数据更新到内存中[在内存中的前提下],同时将这个更新操作记录到redo log里面
-
- 此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务
-
- 执行器生成这个操作的binlog,并把binlog写入磁盘
-
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成
-
- 使用两阶段提交后,写入binlog时发生异常也不会有影响,因为MySQL根据redo log日志恢复数据时,发现redo log还处于prepare阶段,并且没有对应binlog日志,就会回滚该事务。
redo log(重做日志)
-
- 引擎层[InnoDB引擎特有的日志]
-
- 物理日志(记录修改后的值)
-
- redo log是循环写的,空间固定会用完,会覆盖以前的日志
-
- 顺序io(磁盘顺序写)[写redo log 跟写数据到磁盘有一个很大的差异,那就是redo log 是顺序IO,而写数据到磁盘涉及到随机IO,写磁盘需要寻址,找到对应的位置,然后更新/添加/删除,而写 redo log 则是在一个固定的位置循环写入,是顺序IO,所以速度要高于写磁盘数据]
-
- 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 才能够实现真正的落盘。
-
- 数据库中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秒刷盘一
次,所以如果宕机了,可能丢失一秒内的数据
-
- 除了通过参数控制事务提交时的fsync时机外,InnoDB有一个后台线程,每隔1s就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘
-
- 除了以上两种情况外,还有一种情况也会刷盘,就是当buffer中的记录数据大小达到最大值(
16M)的一半的时候,也会自动刷盘(不过由于这个事务并没有提交,所以这个写盘动作只是 write 到了文件系统的 page cache,仍然是在内存中,并没有调用 fsync 真正落盘)
- 除了以上两种情况外,还有一种情况也会刷盘,就是当buffer中的记录数据大小达到最大值(
redo log file(日志文件)
MySQL默认目录中,有两个文件ib_logfile0和ib_logfile1,每次刷盘都是将数据刷新到
这两个文件内Redo日志文件有很多个,一般以日志文件组的形式出现,文件统一命名,格
式是ib_logfile+数字,从0开始。日志文件组中每个文件大小相同。
每次写入从0开始,然后是1,2,3…
binlog(归档日志)
-
- Server层
-
- 逻辑日志(用于记录用户对数据库更新的SQL语句信息,例如更改数据库表和更改内容的SQL语句都会记录到binlog里,但是对库表等内容的查询不会记录),binlog会记录所有涉及更新数据的逻辑操作,并且是顺序写
-
- binlog是可以追加写入的。“追加写”是指binlog文件,写到一定大小后会切换到下一个,并不会覆盖以前的日志。
-
- MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性
-
- binlog的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中
-
- 因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。
-
- 我们可以通过binlog_cache_size参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap)。
-
- 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)
-
- binlog cache 其实就是一片内存区域,充当缓存的作用,
-
- 每个线程都有自己 binlog cache 区域,在事务运行的过程中,MySQL 会先把日志写到 binlog cache 中,等到事务真正提交的时候,再统一把 binlog cache 中的数据写到 binlog 文件中。(binlog cache 有很多个,binlog 文件只有一个!)
-
- 事实上,这个从 binlog cache 写到 binlog 文件中的操作,并不一定就是落盘操作了(根据参数sync_binlog配置),这里仅仅是把 binlog 写到了文件系统的 page cache 上(write操作)
-
- 最后需要把 page cache 中的数据同步到磁盘上,才算真正完成了binlog的持久化(fsync操作)
-
- 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。 如下图:
举个例子,现在有一个事务 A,它的事务 id 为 10,向表中新插入了一条数据,数据记为 data_A,那么此时对应的 undo log 应该如下图所示:
接着事务 B(trx_id=20),将这行数据的值修改为 data_B,同样也会记录一条 undo log,如下图所示,这条 undo log 的 roll_pointer 指针会指向上一个数据版本的 undo log,也就是指向事务 A 写入的那一行 undo log。
只要有事务修改了这一行的数据,那么就会记录一条对应的 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日志。
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 log中
4. 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更新内存内容。更新完成后,内存页变成脏页,等待刷
盘。
ReadView 机制
什么是 Read View,说白了 Read View 就是事务进行快照读操作的时候生产的读视图
(Read View),
在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前
活跃事务的 ID (当每个事务开启时,都会被分配一个 ID , 这个 ID 是递增的,所以最
新的事务,ID 值越大)
在当前事务开启的一瞬间系统会创建一个数组,数组中保存了目前所有的活跃事务 id,所
谓的活跃事务就是指已开启但是还没有提交的事务。
这个ReadView 会记录 4 个非常重要的属性:
- creator_trx_id: 当前事务的 id;
- m_ids: 当前系统中所有的活跃事务的 id,活跃事务指的是当前系统中开启了事务,但是还没有提交的事务;
- min_trx_id: 当前系统中,所有活跃事务中事务 id 最小的那个事务,也就是 m_id 数组中最小的事务 id;
- 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 也是在事务中,因此有可能并不想看到其他事务已经提交
的数据)
幻象读
幻读:官方定义幻读是**两次读取的结果数量不一样**,也即在第一次查询后有其他事务在
第一次查询范围内insert或delete了数据,然后第二次查询(与第一次查询相同)后得到
的数据数量和第一次不一样,这就是幻读在读未提交和读已提交的隔离级别下会出现,在可
重复读的隔离级别下,快照读不会出现幻读,当前读时,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语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,
加共享锁可以使用select … lock in share mode语句。
所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和
lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为
普通查询没有任何锁机制。
InnoDB行锁是通过给索引上的索引项加锁来实现的 只有通过索引条件检索数据,InnoDB
才使用行级锁,否则,InnoDB将使用表锁[InnoDB默认最小加锁粒度为行级锁,并且锁是加
在索引上,如果SQL语句未命中索引,即没有走索引树,使则走聚簇索引的全表扫描,表上
每条记录都会上锁,相当于是表锁,如果sql语句走了索引树,会把扫描过程中碰到的行,
也都加上写锁(也叫行排它锁),还有一种特殊情况,就是如果where之后的条件是索引,但
数据是不存在的,这种情况依旧会走这个索引树,到叶子节点的时候发现没有该数据,返回
结果为空,这不是全表扫描,不会锁表的,同理扫描索引过程中没有碰到行,所以也不会锁行]
意象锁
除了上面所说的两种表锁外,意象锁也属于表锁
意向锁分为两类:
意向共享锁:IS,select ... lock in share mode
意向排他锁:IX,insert 、update、delete、select ... 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
假如没有插入意向锁,而是用普通的间隙锁。插入数据时会获取这条记录所在区间的间隙锁
及这条记录的排它锁,其他事务是不可能在这个区间内插入数据的,因为当前事务已经获取
了这个区间内的间隙锁,其他事务无法获取对应记录的排它锁,只能等待其他事务完成;
用插入意向锁后,数据库设计插入意向锁与排它锁不互斥。多个事务既可以获取对应区间的
插入意向锁也可以获取对应记录的排它锁,各个事务互不影响,不需要等待其他事务完成后
才能进行插入。