五.锁

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null

5.1 概述

        锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源CPU、 RAM、I/O的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。

MySQL中锁的分类:

1.按照锁的使用方式分类:

共享锁、排它锁

2.按照锁的粒度分:

全局锁:锁定数据库中的所有表。

表级锁:每次操作锁住整张表。

行级锁:每次操作锁住对应的行数据。

3.从思想层面上分:

悲观锁、乐观锁

5.2 共享锁&排他锁

5.2.1 共享锁

共享锁,Share lock,也叫读锁。它是指当对象被锁定时,允许其它事务读取该对象,也允许其它事务从该对象上再次获取共享锁,但不能对该对象进行写入。 加锁方式是:

# 方式1
select ... lock in share mode;
# 方式2
select ... for share;

如果事务T1 在某对象持有共享(S)锁,则事务T2 需要再次获取该对象的锁时,会出现下面两种情况: - 如果T2 获取该对象的共享(S)锁,则可以立即获取锁; - 如果T2 获取该对象的排他(X)锁,则无法获取锁;

例子:

为了更好的理解上述两种情况,可以参照下面的执行顺序流和实例图

给user表加共享锁

mysql 将字段制成null mysql将空数据显示为0_java_02

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_03

给user表id=3的行加共享锁

mysql 将字段制成null mysql将空数据显示为0_java_04

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_05

通过上述两个实例可以看出: - 当共享锁加在user表上,则其它事务可以再次获取user表的共享锁,其它事务再次获取user表的排他锁失败,操作被堵塞; - 当共享锁加在user表id=3的行上,则其它事务可以再次获取user表id=3行上的共享锁,其它事务再次获取user表id=3行上的排他锁失败,操作被堵塞,但是事务可以再次获取user表id!=3行上的排他锁;

5.2.2 排它锁

排它锁,Exclusive Lock,也叫写锁或者独占锁,主要是防止其它事务和当前加锁事务锁定同一对象。同一对象主要有两层含义: - 当排他锁加在表上,则其它事务无法对该表进行insert,update,delete,alter,drop等更新操作; - 当排他锁加在表的行上,则其它事务无法对该行进行insert,update,delete,alter,drop等更新操作;

排它锁加锁方式为:

select ... for update;

例子:

为了更好的说明排他锁,可以参照下面的执行顺序流和实例图:

给user表对象加排他锁

mysql 将字段制成null mysql将空数据显示为0_java_06

mysql 将字段制成null mysql将空数据显示为0_数据库_07

给user表id=3的行对象加排他锁

mysql 将字段制成null mysql将空数据显示为0_数据库_08

mysql 将字段制成null mysql将空数据显示为0_mysql_09

5.3 全局锁

5.3.1 介绍

        全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新操作的事务提交语句都将被阻塞。 其典型的使用场景是做全库的逻辑备份,在进行逻辑备份的过程中,通过加全局锁,数据库中的数据就不会发生变化,这样就保证了数据的一致性和完整性。。

5.3.2 语法

1.加全局锁

flush tables with read lock ;

2.数据备份

mysqldump -uroot –p1234 itcast > itcast.sql

3.释放锁

unlock tables ;

5.3.3 特点

数据库中加全局锁,是一个比较重的操作,存在以下问题:

1.如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。

2.如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导 致主从延迟。

注意:在InnoDB引擎中,我们也可以在备份时加上参数 --single-transaction 参数来完成不加锁的一致性数据备份。

mysqldump --single-transaction -uroot –p123456 itcast > itcast.sql

5.4 表级锁

5.4.1 介绍

表级锁,每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、 InnoDB、BDB等存储引擎中。

对于表级锁,主要分为以下三类:表锁、元数据锁(meta data lock,MDL)、意向锁

5.4.2 表锁

1.什么是表锁

        表锁就是对整张表加锁,包含读锁和写锁,由MySQL Server实现,表锁需要显示加锁或释放锁,具体指令如下:

# 给表加写锁
lock tables tablename write;

# 给表加读锁
lock tables tablename read;

# 释放锁
unlock tables;

2.表锁的特点

A.读锁

        代表当前表为只读状态,读锁是一种共享锁。需要注意的是,读锁除了会限制其它线程的操作外,也会限制加锁线程的行为,具体限制如下: 1. 加锁线程只能对当前表进行读操作,不能对当前表进行更新操作,不能对其它表进行所有操作; 2. 其它线程只能对当前表进行读操作,不能对当前表进行更新操作,可以对其它表进行所有操作;

例子

mysql 将字段制成null mysql将空数据显示为0_mysql_10

mysql 将字段制成null mysql将空数据显示为0_mysql_11

B.写锁 

写锁:写锁是一种独占锁,需要注意的是,写锁除了会限制其它线程的操作外,也会限制加锁线程的行为,具体限制如下: 1. 加锁线程对当前表能进行所有操作,不能对其它表进行任何操作; 2. 其它线程不能对当前表进行任何操作,可以对其它表进行任何操作

mysql 将字段制成null mysql将空数据显示为0_java_12

mysql 将字段制成null mysql将空数据显示为0_mysql_13

3.结论: 读锁不会阻塞其他客户端的读,但是会阻塞写。写锁既会阻塞其他客户端的读,又会阻塞其他客户端的写。 并且加读锁和写锁的线程不能再对其它表做任何操作

5.4.3 元数据锁

1.什么是元数据锁

元数据锁:metadata lock,简称MDL,它是在MySQL 5.5版本引进的。元数据锁不用像表锁那样显式的加锁和释放锁,而是在访问表时被自动加上,以保证读写的正确性。加锁和释放锁规则如下:

  • MDL读锁之间不互斥,也就是说,允许多个线程同时对加了 MDL读锁的表进行CRUD(增删改查)操作;
  • MDL写锁,它和读锁、写锁都是互斥的,目的是用来保证变更表结构操作的安全性。也就是说,当对表结构进行变更时,会被默认加 MDL写锁,因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
  • MDL读写锁是在事务commit之后才会被释放;

2.例子

为了更好的说明 MDL读锁规则,可以参照下面的顺序执行流和实例图:

mysql 将字段制成null mysql将空数据显示为0_java_14

mysql 将字段制成null mysql将空数据显示为0_数据_15

为了更好的说明 MDL写锁规则,可以参照下面的顺序执行流和实例图:

mysql 将字段制成null mysql将空数据显示为0_数据_16

mysql 将字段制成null mysql将空数据显示为0_数据_17

5.4.4 意向锁

1.介绍

为了避免DML在执行时,加的行锁与表锁的冲突,在InnoDB中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。

有了意向锁之后:

客户端一:在执行DML操作时,会对涉及的行加行锁,同时也会对该表加上意向锁。

其他客户端:在对这张表加表锁的时候,会根据该表上所加的意向锁来判定是否可以成功加表锁,而不用逐行判断表中每行是否有行锁。

mysql 将字段制成null mysql将空数据显示为0_java_18

2.分类

意向共享锁(IS): 由语句select ... lock in share mode添加 。 与 表锁共享锁 (read)兼容,与表锁排他锁(write)互斥。

意向排他锁(IX): 由insert、update、delete、select...for update添加 。与表锁共享锁(read)及排他锁(write)都互斥,意向锁之间不会互斥。

3.注意

一旦事务提交了,意向共享锁、意向排他锁,都会自动释放。

可以通过以下SQL,查看意向锁及行锁的加锁情况:

select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from
performance_schema.data_locks;

5.4.5 锁的兼容性

下面的图表总结了表级锁类型的兼容性

mysql 将字段制成null mysql将空数据显示为0_数据_19

5.5 行级锁

5.5.1 介绍

行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。并不是所有的引擎都支持行锁,比如,InnoDB引擎支持行锁而 MyISAM引擎不支持。

InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的 锁。对于行级锁,主要分为以下三类:

行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在 RC、RR隔离级别下都支持。

mysql 将字段制成null mysql将空数据显示为0_java_20

间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事 务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。 

mysql 将字段制成null mysql将空数据显示为0_数据库_21

临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。 在RR隔离级别下支持。 

mysql 将字段制成null mysql将空数据显示为0_java_22

5.5.2 行锁

1.介绍

InnoDB实现了以下两种类型的行锁:

共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。

排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他 锁。

两种行锁的兼容情况如下:

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_23

常见的SQL语句,在执行时,所加的行锁如下:

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_24

5.5.3 间隙锁&临键锁

默认情况下,InnoDB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 临键锁(next-key )进行搜索和索引扫描,以防止幻读。

索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁 。

索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁。

索引上的范围查询(唯一索引)--会访问到不满足条件的第一个值为止。

注意:间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会 阻止另一个事务在同一间隙上采用间隙锁。

5.5.4 示例

A. 索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁 。

mysql 将字段制成null mysql将空数据显示为0_mysql_25

B. 索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁。 

介绍分析一下:我们知道InnoDB的B+树索引,叶子节点是有序的双向链表。 假如,我们要根据这个二级索引查询值为18的数据,并加上共享锁,我们是只锁定18这一行就可以了吗? 并不是,因为是非唯一索引,这个结构中可能有多个18的存在,所以,在加锁时会继续往后找,找到一个不满足条件的值(当前案例中也 就是29)。此时会对18加临键锁,并对29之前的间隙加锁。

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_26

mysql 将字段制成null mysql将空数据显示为0_java_27

C.索引上的范围查询(唯一索引)--会访问到不满足条件的第一个值为止。 

mysql 将字段制成null mysql将空数据显示为0_数据库_28

查询的条件为id>=19,并添加共享锁。 此时我们可以根据数据库表中现有的数据,将数据分为三个部 分:

[19]

(19,25]

(25,+∞] 

所以数据库数据在加锁是,就是将19加了行锁,25的临键锁(包含25及25之前的间隙),正无穷的临键锁(正无穷及之前的间隙)。

5.6 乐观锁&悲观锁

在MySQL中,无论是悲观锁还是乐观锁,都是人们对概念的一种思想抽象,它们本身还是利用 MySQL提供的锁机制来实现的。其实,除了在MySQL数据,像 Java语言里面也有乐观锁和悲观锁的概念。

  • 悲观锁,可以理解成:在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking),采用的是先获取锁再操作数据的策略,可能会产生死锁;
  • 乐观锁,是相对悲观锁而言,一般不会利用数据库的锁机制,而是采用类似版本号比较之类的操作,因此乐观锁不会产生死锁的问题;

5.7 死锁和死锁检测

当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。可以通过下面的指令查看死锁

show engine innodb status\G

当出现死锁以后,有两种策略:

  • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置,InnoDB 中 innodb_lock_wait_timeout 的默认值是 50s。
  • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其它事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启死锁检测。

六.InnoDB引擎

6.1 逻辑存储结构

InnoDB的逻辑存储结构如下图所示:

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_29

1.表空间

表空间是InnoDB存储引擎逻辑结构的最高层, 如果用户启用了参数 innodb_file_per_table(在 8.0版本中默认开启) ,则每张表都会有一个表空间(xxx.ibd),一个mysql实例可以对应多个表空 间,用于存储记录、索引等数据。

2.段

段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段 (Rollback segment),InnoDB是索引组织表,数据段就是B+树的叶子节点, 索引段即为B+树的非叶子节点。段用来管理多个Extent(区)。

3.区

区,表空间的单元结构,每个区的大小为1M。 默认情况下, InnoDB存储引擎页大小为16K, 即一 个区中一共有64个连续的页。

4.页

是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。为了保证页的连续性, InnoDB 存储引擎每次从磁盘申请 4-5 个区。

5.行

行,InnoDB 存储引擎数据是按行进行存放的。

在行中,默认有两个隐藏字段:

  • Trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
  • Roll_pointer:每次对某条记录进行改动时,都会把旧的版本写入到undo日志中,然后这个 隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

6.2 架构

6.2.1 概述

MySQL5.5 版本开始,默认使用InnoDB存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发 中使用非常广泛。下面是InnoDB架构图,左侧为内存结构,右侧为磁盘结构。

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_30

6.2.2 内存结构

在左侧的内存结构中,主要分为这么四大块儿: Buffer Pool、Change Buffer、Adaptive Hash Index、Log Buffer。 接下来介绍一下这四个部分。

1.Buffer Pool

        InnoDB存储引擎基于磁盘文件存储,访问物理硬盘和在内存中进行访问,速度相差很大,为了尽可能 弥补这两者之间的I/O效率的差值,就需要把经常使用的数据加载到缓冲池中,避免每次访问都进行磁盘I/O。 在InnoDB的缓冲池中不仅缓存了索引页和数据页,还包含了undo页、插入缓存、自适应哈希索引以及 InnoDB的锁信息等等。

        缓冲池 Buffer Pool,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增 删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。

缓冲池以Page页为单位,底层采用链表数据结构管理Page。根据状态,将Page分为三种类型:

  • free page:空闲page,未被使用。
  • clean page:被使用page,数据没有被修改过。
  • dirty page:脏页,被使用page,数据被修改过,也中数据与磁盘的数据产生了不一致。

在专用服务器上,通常将多达80%的物理内存分配给缓冲池 。查看缓冲池参数对应的值大小: show variables like 'innodb_buffer_pool_size';

mysql 将字段制成null mysql将空数据显示为0_java_31

2.Change Buffer

Change Buffer,更改缓冲区(针对于非唯一二级索引页),在执行DML语句时,如果这些数据Page 没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer 中,在未来数据被读取时,再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中。

Change Buffer的意义是什么呢? 先来看一幅图,这个是二级索引的结构图:

mysql 将字段制成null mysql将空数据显示为0_数据_32

与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了 ChangeBuffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO。 

3.Adaptive Hash Index

自适应hash索引,用于优化对Buffer Pool数据的查询。MySQL的innoDB引擎中虽然没有直接支持 hash索引,但是给我们提供了一个功能就是这个自适应hash索引。因为前面我们讲到过,hash索引在 进行等值匹配时,一般性能是要高于B+树的,因为hash索引一般只需要一次IO即可,而B+树,可能需 要几次匹配,所以hash索引的效率要高,但是hash索引又不适合做范围查询、模糊匹配等。 InnoDB存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下hash索引可以提升速度, 则建立hash索引,称之为自适应hash索引。

自适应哈希索引,无需人工干预,是系统根据情况自动完成。

参数: adaptive_hash_index

4.Log Buffer

Log Buffer:日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log 、undo log), 默认大小为 16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事 务,增加日志缓冲区的大小可以节省磁盘 I/O。

参数:

innodb_log_buffer_size:日志缓冲区大小

innodb_flush_log_at_trx_commit:日志刷新到磁盘时机,取值主要包含以下三个:

  • 0: 每秒将日志写入并刷新到磁盘一次。
  • 1: 日志在每次事务提交时写入并刷新到磁盘,默认值。
  • 2: 日志在每次事务提交后写入,并每秒刷新到磁盘一次。

mysql 将字段制成null mysql将空数据显示为0_java_33

6.2.3 磁盘结构

mysql 将字段制成null mysql将空数据显示为0_mysql_34

接下来,再来看看InnoDB体系结构的右边部分,也就是磁盘结构:

1.System Tablespace

系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)

参数:innodb_data_file_path

mysql 将字段制成null mysql将空数据显示为0_java_35

系统表空间,默认的文件名叫 ibdata1。

2.File-Per-Table Tablespaces

如果开启了innodb_file_per_table开关 ,则每个表的文件表空间包含单个InnoDB表的数据和索引 ,并存储在文件系统上的单个数据文件中。

开关参数:innodb_file_per_table ,该参数默认开启。

mysql 将字段制成null mysql将空数据显示为0_数据库_36

那也就是说,我们没创建一个表,都会产生一个表空间文件,如图:

mysql 将字段制成null mysql将空数据显示为0_java_37

3.General Tablespaces

通用表空间,需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空间。

A.创建表空间

CREATE TABLESPACE ts_name ADD DATAFILE 'file_name' ENGINE = engine_name;

mysql 将字段制成null mysql将空数据显示为0_mysql_38

B.创建表时指定表空间

CREATE TABLE xxx ... TABLESPACE ts_name;

mysql 将字段制成null mysql将空数据显示为0_数据库_39

4.Undo Tablespaces

撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储 undo log日志。

5.Temporary Tablespaces

InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。

6.Doublewrite Buffer Files

双写缓冲区,innoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件 中,便于系统异常时恢复数据。

mysql 将字段制成null mysql将空数据显示为0_mysql_40

7.Redo Log

重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所 有修改信息都会存到该日志中, 用于在刷新脏页到磁盘时,发生错误时, 进行数据恢复使用。

以循环方式写入重做日志文件,涉及两个文件:

mysql 将字段制成null mysql将空数据显示为0_java_41

我们已经介绍完了InnoDB的内存结构,以及磁盘结构,那么内存中我们所更新的数据,又是如何到磁盘 中的呢? 此时,就涉及到一组后台线程,接下来,就来介绍一些InnoDB中涉及到的后台线程。

mysql 将字段制成null mysql将空数据显示为0_java_42

6.2.4 后台线程

mysql 将字段制成null mysql将空数据显示为0_数据库_43

在InnoDB的后台线程中,分为4类,分别是:Master Thread 、IO Thread、Purge Thread、 Page Cleaner Thread。

1.Master Thread

核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中, 保持数据的一致性, 还包括脏页的刷新、合并插入缓存、undo页的回收 。

2.IO Thread

在InnoDB存储引擎中大量使用了AIO来处理IO请求, 这样可以极大地提高数据库的性能,而IO Thread主要负责这些IO请求的回调。

mysql 将字段制成null mysql将空数据显示为0_mysql_44

我们可以通过以下的这条指令,查看到InnoDB的状态信息,其中就包含IO Thread信息。 

show engine innodb status \G;

mysql 将字段制成null mysql将空数据显示为0_数据_45

3.Purge Thread

主要用于回收事务已经提交了的undo log,在事务提交之后,undo log可能不用了,就用它来回 收。

4.Page Cleaner Thread

协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master Thread 的工作压力,减少阻 塞。

6.3 事务原理

6.3.1 事务基础

1.事务

事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系 统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

2.特性

  • 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。  
  • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环 境下运行。
  • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

实际上,我们研究事务的原理,就是研究MySQL的InnoDB引擎是如何保证事务的这四大特性的。

mysql 将字段制成null mysql将空数据显示为0_数据_46

而对于这四大特性,实际上分为两个部分。 其中的原子性、一致性、持久性,实际上是由InnoDB中的 两份日志来保证的,一份是redo log日志,一份是undo log日志。 而隔离性是通过数据库的锁, 加上MVCC来保证的。

mysql 将字段制成null mysql将空数据显示为0_数据库_47

我们在讲解事务原理的时候,主要就是来研究一下redolog,undolog以及MVCC。

6.3.2 redo log

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。 该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用 于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。

如果没有redolog,可能会存在什么问题的? 我们一起来分析一下。

我们知道,在InnoDB引擎中的内存结构中,主要的内存区域就是缓冲池,在缓冲池中缓存了很多的数 据页。 当我们在一个事务中,执行多个增删改的操作时,InnoDB引擎会先操作缓冲池中的数据,如果 缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中 的数据修改,修改后的数据页我们称为脏页。 而脏页则会在一定的时机,通过后台线程刷新到磁盘 中,从而保证缓冲区与磁盘的数据一致。 而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后 将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却 没有持久化下来,这就出现问题了,没有保证事务的持久性。

mysql 将字段制成null mysql将空数据显示为0_数据库_48

那么,如何解决上述的问题呢? 在InnoDB中提供了一份日志 redo log,接下来我们再来分析一 下,通过redolog如何解决这个问题。

mysql 将字段制成null mysql将空数据显示为0_数据_49

有了redolog之后,当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在redo log buffer中。在事务提交时,会将redo log buffer中的数据刷新到redo log磁盘文件中。 过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于redo log进行数据 恢复,这样就保证了事务的持久性。 而如果脏页成功刷新到磁盘 或 或者涉及到的数据已经落盘,此 时redolog就没有作用了,就可以删除了,所以存在的两个redolog文件是循环写的。

那为什么每一次提交事务,要刷新redo log 到磁盘中呢,而不是直接将buffer pool中的脏页刷新 到磁盘呢 ?
因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。 而redo log在 往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。

这 种先写日志的方式,称之为 WAL(Write-Ahead Logging)。

6.3.3 undo log 

回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚(保证事务的原子性) 和 MVCC(多版本并发控制) 。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的 update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。

Undo log销毁:undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些 日志可能还用于MVCC。

Undo log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的 rollback segment 回滚段中,内部包含1024个undo log segment。

6.4 MVCC

6.4.1 基本概念

1.当前读

当前读是指读取的记录是最新版本,读取时会通过加共享锁或排它锁,来保证其他并发事务不能修改当前记录。当前读的一些常见 SQL 语句类型如下:

# 对读的记录加一个X锁
SELECT...FOR UPDATE
# 对读的记录加一个S锁
SELECT...LOCK IN SHARE MODE
# 对读的记录加一个S锁
SELECT...FOR SHARE
# 对修改的记录加一个X锁
INSERT...
UPDATE...
DELETE...

测试:

mysql 将字段制成null mysql将空数据显示为0_数据_50

在测试中我们可以看到,即使是在默认的RR隔离级别下,事务 A 中依然可以读取到事务 B 最新提交的内容,因为在查询语句后面加上了 lock in share mode 共享锁,此时是当前读操作。当然,当我们加排他锁的时候,也是当前读操作。

2.快照读

快照读是指如果读取的记录正在执行 UPDATE/DELETE 操作,读取操作不会因此去等待记录上 X 锁的释放,而是去读取记录的一个快照。快照即记录的历史版本,每行记录可能存在多个历史版本。

  • Read Committed:每次 select,都生成一个快照。
  • Repeatable Read:开启事务后第一个 select 语句会生成一个快照,后续的 select 语句都使用该快照进行读取。
  • Serializable:快照读会退化为当前读。

测试:

mysql 将字段制成null mysql将空数据显示为0_数据库_51

在测试中,我们看到即使事务B提交了数据,事务 A 中也查询不到。原因就是因为普通的 select 是快照读,而在当前默认的 RR 隔离级别下,开启事务后第一个 select 语句才是快照读的地方,后面执行相同的 select 语句都是从快照中获取数据,可能不是当前的最新数据,这样也就保证了可重复读。

3.MVCC

全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本, 使得读写操作没有冲突,快照读为 MySQL 实现 MVCC 提供了一个非阻塞读功能。MVCC 的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log 日志、readView。 

6.4.2 隐藏字段

1.介绍

mysql 将字段制成null mysql将空数据显示为0_java_52

当我们创建了上面的这张表,我们在查看表结构的时候,就可以显式的看到这三个字段。 实际上除了这三个字段以外,InnoDB还会自动的给我们添加三个隐藏字段,这三个隐藏字段及其含义分别是:

mysql 将字段制成null mysql将空数据显示为0_数据库_53

而上述的前两个字段是肯定会添加的, 是否添加最后一个字段DB_ROW_ID,得看当前表有没有主键, 如果有主键,则不会添加该隐藏字段。

6.4.3 undo log

1.介绍

回滚日志是在 insert、update、delete 的时候产生的便于数据回滚的日志。

当 insert 的时候,产生的 undo log 日志只在回滚时需要,在事务提交后,可被立即删除。

而 update、delete 的时候,产生的 undo log 日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

2.版本链

多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。如下:

mysql 将字段制成null mysql将空数据显示为0_数据_54

3.readview

ReadView是读视图,读视图是快照读 SQL 执行时 MVCC 提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。 

如果是 RC 隔离级别,每一次执行快照读时都会生成 ReadView,如果是 RR 隔离级别仅在事务中第一次执行快照读时生成 ReadView,后续复用。

ReadView 中包含了四个核心字段:

mysql 将字段制成null mysql将空数据显示为0_数据库_55

而在 readview 中就规定了版本链数据的访问规则:

trx_id 代表 undo log 版本链中记录对应的事务ID。

mysql 将字段制成null mysql将空数据显示为0_数据_56

不同的隔离级别,生成 ReadView 的时机不同:

  • READ COMMITTED :在事务中每一次执行快照读时生成 ReadView。
  • REPEATABLE READ:仅在事务中第一次执行快照读时生成 ReadView,后续复用该ReadView。

6.4.5 原理分析

1.RC隔离级别

RC隔离级别下,在事务中每一次执行快照读时生成 ReadView。我们来分析事务 5 中,两次快照读读取数据,是如何获取数据的?

在事务5中,查询了两次 id 为 30 的记录,由于隔离级别为 Read Committed,所以每一次进行快照读都会生成一个 ReadView,那么两次生成的 ReadView 如下。

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_57

那么这两次快照读在获取数据时,就需要根据所生成的 ReadView 以及 ReadView 的版本链访问规则, 到 undolog 版本链中匹配数据,最终决定此次快照读返回的数据。

A.先来看第一次快照读具体的读取过程:

mysql 将字段制成null mysql将空数据显示为0_mysql_58

在进行匹配时,会从 undo log 的版本链,从上到下进行挨个匹配:

先匹配第一条记录

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_59

这条记录对应的 trx_id(也就是记录中的DB_TRX_ID)为 4,将 4 带入右侧的匹配规则中。

①不满足 ②不满足 ③不满足 ④也不满足,都不满足,则继续匹配 undo log 版本链的下一条。

再匹配第二条记录

mysql 将字段制成null mysql将空数据显示为0_mysql_60

这条记录对应的 trx_id 为3,也就是将 3 带入右侧的匹配规则中。

①不满足 ②不满足 ③不满足 ④也不满足 ,都不满足,则继续匹配 undo log 版本链的下一条。 

再匹配第三条

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_61

这条记录对应的 trx_id 为 2,也就是将 2 带入右侧的匹配规则中。

①不满足 ②满足,终止匹配,此次快照读,返回的数据就是版本链中记录的这条数据。 

B.再来看第二次快照读具体的读取过程

mysql 将字段制成null mysql将空数据显示为0_数据_62

在进行匹配时,会从undo log 的版本链,从上到下进行挨个匹配:

先匹配第一条记录,这条记录对应的 trx_id 为 4,也就是将 4 带入右侧的匹配规则中。

①不满足 ②不满足 ③不满足 ④也不满足,都不满足,则继续匹配 undo log 版本链的下一条。

mysql 将字段制成null mysql将空数据显示为0_mysql_63

再匹配第二条记录,这条记录对应的 trx_id 为 3,也就是将 3 带入右侧的匹配规则中。

①不满足 ②满足,终止匹配。此次快照读,返回的数据就是版本链中记录的这条数据。

mysql 将字段制成null mysql将空数据显示为0_java_64

2.RR隔离级别

RR 隔离级别下,仅在事务中第一次执行快照读时生成 ReadView,后续复用该 ReadView

RR 是可重复读,在一个事务中,执行两次相同的 select 语句,查询到的结果是一样的。那 MySQL 是如何做到可重复读的呢? 我们简单分析一下就知道了

我们看到,在 RR 隔离级别下,只是在事务中第一次快照读时生成 ReadView,后续都是复用该 ReadView,那么既然 ReadView 都一样, ReadView 的版本链匹配规则也一样, 那么最终快照读返回的结果也是一样的。

3.总结

所以,MVCC 的实现原理就是通过 InnoDB 表的隐藏字段、Undo Log 版本链、ReadView 来实现的。 而 MVCC + 锁,则实现了事务的隔离性。 而一致性则是由 redo log 与 undo log 保证。

mysql 将字段制成null mysql将空数据显示为0_mysql 将字段制成null_65