用update语句贯穿MySQL的重要概念,执行流程、表锁、行锁、事务隔离、存储引擎、redo log、bin log、undo log、版本控制(MVCC)。理解MySQL的事务实现。

一、前置准备

1、MySQL的执行流程

从图中看,MySQL可以分为Server层存储引擎层两部分。

  1. Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能。
  2. 存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。

模块信息:

(1)连接器

连接器负责跟客户端建立连接、获取权限、维持和管理连接。连接命令一般是这么写的:

mysql -h$ip -P$port -u$user -p

连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在show processlist命令中看到它。文本中这个图是show processlist的结果,其中的Command列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接。

客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数wait_timeout控制的,默认值是8小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执行请求了。

(2)查询缓存

连接建立完成后,你就可以执行select语句了。执行逻辑就会来到第二步:查询缓存。MySQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value对的形式,被直接缓存在内存中。key是查询的语句,value是查询的结果。如果你的查询能够直接在这个缓存中找到key,那么这个value就会被直接返回给客户端。

(3)分析器

如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL需要知道你要做什么,因此需要对SQL语句做解析。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。

(4)执行器

MySQL通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。开始执行的时候,要先判断一下你对这个表T有没有执行查询的权限,如果没有,就会返回没有权限的错误。

存储引擎:

MySQL中的索引InnoDB和MyISAM:

  1. InnoDB是MySQL的默认存储引擎,它支持事务处理和行级锁定。它的数据结构是基于聚集索引(clustered index)的B+树。InnoDB适用于需要高并发、数据一致性和事务支持的应用场景,比如电子商务网站、银行系统等。 
  2. MyISAM是MySQL的旧有存储引擎,它不支持事务处理,只支持表级锁定。它的数据结构是基于堆表(heap table)和非聚集索引的B+树。MyISAM适用于读写分离明显、对事务一致性要求不高的应用场景,比如博客、论坛等。 

二、执行update语句

前文中有交代,MySQL可以分为Server层和存储引擎层两部分,Server层和存储引擎层。其中,更新流程涉及两个重要的日志模块:redo log(重做日志) binlog(归档日志)。

为什么会有两份日志呢?
因为最开始MySQL里并没有InnoDB引擎。MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档。而InnoDB是另一个公司以插件形式引入MySQL的,既然只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统——也就是redo log来实现crash-safe能力。
注:有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。

其中:
1、redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。
2、redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=1这一行的c字段加1 ”。
3、redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

注:redo log是循环写的,空间固定会用完;意思是说InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么这块redo log日志总共就可以记录4GB的操作。从头开始写,写到末尾就又回到开头循环写。如果日志文件满了,这时候不能再执行新的更新,会停下来先清除一些记录。

数据更新:

MySQL里经常说到的WAL技术,WAL的全称是Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,用这样的思路来提升更新效率。具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。

有了对这两个日志的概念性理解,我们再来看执行器和InnoDB引擎在执行这个简单的update语句时的内部流程:

1、执行器先找引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
2、执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
3、引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
4、执行器生成这个操作的binlog,并把binlog写入磁盘。
5、执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。

这里我给出这个update语句的执行流程图,图中红褐色框表示是在InnoDB内部执行的,蓝色框表示是在执行器中执行的。

将redo log的写入拆成了两个步骤:prepare和commit,这就是"两阶段提交"。这样做的目的就是让两个状态保持逻辑上的一致。

三、MySQL的锁在update中的使用

MyISAM是MySQL的旧有存储引擎,它不支持事务处理,只支持表级锁定。
MySQL里面表级别的锁有两种:

  • 一种是表锁,
  • 一种是元数据锁(meta data lock,MDL)。

1. 表锁: 
表锁是对整个表进行锁定。当获取表锁时,其他会话无法对该表执行任何操作,直到锁被释放。表锁有两种模式:共享锁(读锁)和排他锁(写锁)。 
  
使用SELECT语句获取一个表的共享锁:

LOCK TABLES 表名 READ;
SELECT * FROM 表名;
UNLOCK TABLES;

使用UPDATE语句获取一个表的排他锁:

LOCK TABLES 表名 WRITE;
UPDATE 表名 SET 列名 = 值 WHERE 条件;
UNLOCK TABLES;

2. 元数据锁(MDL): 
元数据锁用于管理对数据库对象(如表或存储过程)的并发访问。这些锁会在需要时由服务器自动获取和释放。元数据锁确保数据一致性,并防止对同一对象的冲突操作。 
 
创建一个名为"表名"的表并获取元数据锁:

CREATE TABLE 表名 (id INT, name VARCHAR(50));

修改表结构并获取元数据锁:

ALTER TABLE 表名 ADD email VARCHAR(100);

无需手动释放元数据锁,它会在操作完成后自动释放。 

MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是,保证读写的正确性。你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。


因此,在MySQL 5.5版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。

  1. 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
  2. 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

虽然MDL锁是系统默认会加的,但却是你不能忽略的一个机制。我经常看到有人掉到这个坑里:给一个小表加个字段,导致整个库挂了。就是因为,当访问量较大时,这时候执行update语句,因为读写锁之间、写锁之间是互斥的,就导致大量的请求全部阻塞。
 
需要注意的是,表锁和元数据锁都由MySQL服务器自动管理。不需要显式地获取或释放元数据锁,但在必要时可以使用表锁。

MySQL的默认存储引擎InnoDB,它支持事务处理和行级锁定。

事务的特性:即原子性、一致性、隔离性、持久性),今天我们就来说说其中I,也就是“隔离性”。
MySQL支持四种事务隔离级别,包括:

  • 读未提交 (Read Uncommitted):允许一个事务读取另一个未提交的事务的数据,这可能导致脏读、不可重复读和幻读的情况。
  • 读已提交 (Read Committed):只允许一个事务读取另一个已经提交的事务的数据,这是大多数数据库系统的默认隔离级别。
  • 可重复读 (Repeatable Read):保证在同一个事务中的查询都是事务开始时刻一致的,这是InnoDB的默认级别。
  • 串行化 (Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。

下面我们通过两个事务来说明这些隔离级别:

我们来看看在不同的隔离级别下,事务A会有哪些不同的返回结果,也就是图里面V1、V2、V3的返回值分别是什么。

  1. 若隔离级别是“读未提交”, 则V1的值就是2。这时候事务B虽然还没有提交,但是结果已经被A看到了。因此,V2、V3也都是2。
  2. 若隔离级别是“读提交”,则V1是1,V2的值是2。事务B的更新在提交后才能被A看到。所以, V3的值也是2。
  3. 若隔离级别是“可重复读”,则V1、V2是1,V3是2。之所以V2还是1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
  4. 若隔离级别是“串行化”,则在事务B执行“将1改成2”的时候,会被锁住。直到事务A提交后,事务B才可以继续执行。所以从A的角度看, V1、V2值是1,V3的值是2。

在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的。

这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。

事务启动方式有以下几种:
1、显式启动事务语句, begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback。

2、set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个select语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行commit 或 rollback 语句,或者断开连接。

四、说说MySQL的InnoDB的行锁

首先,行锁就是针对数据表中行记录的锁。这很好理解,比如事务A更新了一行,而这时候事务B也要更新同一行,则必须等事务A的操作完成后才能进行更新。 

在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。知道了这个设定,那如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。


比如,下面这个死锁的产生,就是当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,几个线程都进入无限等待的状态。

如图,事务A在等待事务B释放id=2的行锁,而事务B在等待事务A释放id=1的行锁。 事务A和事务B在互相等待对方的资源释放,就是进入了死锁状态。

当出现死锁以后,有两种策略:
一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。
另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。
在InnoDB中,innodb_lock_wait_timeout的默认值是50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过50s才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。

所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且innodb_deadlock_detect的默认值本身就是on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。
其实,减少死锁的主要方向,就是控制访问相同资源的并发事务量。

回滚操作:

在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
如图,图中蓝色的线就是undo log回滚日志,假设一个值从1被按顺序改成了2、3、4,在回滚日志里面就会有类似下面的记录。

当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如图中看到的,在视图A、B、C里面,这一个记录的值分别是1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于read-view A,要得到1,就必须将当前值依次执行图中所有的回滚操作得到。
同时你会发现,即使现在有另外一个事务正在将4改成5,这个事务跟read-view A、B、C对应的事务是不会冲突的。你一定会问,回滚日志总不能一直保留吧,什么时候删除呢?答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的read-view的时候。

MySQL数据库的多版本并发控制(MVCC)是一种用于处理并发访问的机制。它允许多个事务同时读取和修改数据库中的数据,而不会相互干扰或产生冲突。

在MVCC中,每个事务在开始时会创建一个独立的快照,该快照包含了事务开始时数据库中的所有数据。当其他事务对数据库进行修改时,它们不会直接修改原始数据,而是创建新的数据版本,并将其与事务关联起来。

读取操作会根据事务开始时的快照来获取数据,而不受其他事务修改的影响。这意味着读取操作可以获得一致性的数据视图,即使其他事务正在修改数据。

写入操作会创建新的数据版本,并将其与当前事务关联。其他事务仍然可以使用旧版本的数据进行读取操作,而不会受到正在进行的写入操作的影响。只有在事务提交时,新版本的数据才会成为其他事务可见的。

如图所示:


结果返回:事务B查到的k的值是3,而事务A查到的k的值是1

这里,我们需要注意的是事务的启动时机。

begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句(第一个快照读语句),事务才真正启动。
如果你想要马上启动一个事务,可以使用start transaction with consistent snapshot 这个命令。

在这个例子中,事务C没有显式地使用begin/commit,表示这个update语句本身就是一个事务,语句完成的时候会自动提交。
在事务A查询的时候,其实事务B还没有提交,但是它生成的(1,3)这个版本已经变成当前版本了。但这个版本对事务A必须是不可见的,否则就变成脏读了。
事务B在更新了行之后查询; 事务A在一个只读事务中查询,并且时间顺序上是在事务B的查询之后。如果事务B在更新之前查询一次数据,这个查询返回的k的值确实是1。
但是,当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务C的更新就丢失了。因此,事务B此时的set k=k+1是在(1,2)的基础上进行的操作。
所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。

五、MVCC的工作原理:

1、每行数据都有一个隐藏的版本号或时间戳。这个版本号标识了数据的创建或修改时间。

2、当一个事务开始时,它会创建一个独立的快照,该快照包含了事务开始时数据库中的所有数据。这个快照基于事务开始时的时间戳。

3、在读取操作中,事务只能看到在其开始时间之前已经提交的数据版本。如果某行数据的版本号大于事务开始时间戳,则该行数据对事务不可见。

4、在写入操作中,事务会创建新的数据版本,并将其与当前事务关联。这个新版本的数据会被分配一个新的版本号。

InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。而每行数据也都是有多个版本的。

每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id。

同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id。

5、当其他事务正在读取数据时,它们仍然可以访问旧版本的数据,而不会受到正在进行的写入操作的影响。这种读取不会阻塞写入操作,也不会导致数据冲突。

在实现上, InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的就是,启动了但还没提交。

数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。而数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果得到的。

6、当事务提交时,它所做的修改会成为其他事务可见的。这意味着其他事务可以看到该事务所提交的新版本数据。

通过使用快照和版本号,MVCC实现了并发事务的隔离性。每个事务都有自己的一致性视图,可以读取一致的数据,并且写入操作不会阻塞其他事务的读取操作。这种机制提高了并发性能,减少了锁冲突,同时保持了数据的一致性。

猜你喜欢

转载自blog.csdn.net/amosjob/article/details/133034613