04事务

关闭自动提交#

在MySQL命令行的默认设置下,事务都是自动提交的,即执行SQL语句后就会马上执行COMMIT操作。因此开始一个事务,必须使用BEGIN、START TRANSACTION,(显示开启事务)或者执行SET AUTOCOMMIT=0,以禁用当前会话的自动提交。

  1. select语句一般用来输出用户变量,比如select @变量名,用于输出数据源不是表格的数据。
  2. 系统变量在变量名前面有两个@
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
|            1 |
+--------------+
1 row in set (0.00 sec)
# 修改变量
set autocommit=0;

本文中autocommit=0,开始事务需要使用begin显式开启

事务并发存在的问题#

没有脏读问题,如果两个事务同时修改一条数据,那么另一个事务的修改操作会被阻塞
【隔离性】脏读(读到了还没有提交的数据)

【隔离性】不可重复读(同一事务中,第1次读的数据和第2次读的数据不一样。读到了事务操作过程中其他事务提交的数据,也不应该,这个没法保证一致性。比如我第一次根据某个事务做了一个操作,第二次想同样的逻辑做一个判断操作,但是数据变了,导致同一个事务中对同一个(其实不是了)数据做的操作居然不一致

【隔离性】虚读、幻读(同一事务中:我第一次读到某一条数据,但是第二次没读到;或者第一次没读到,第二次居然读到了。条数问题)

事务的隔离级别#

READ_UNCOMMITTED 读未提交
READ_COMMITTED 读已提交
REPEATABLE_READ 重复读 SERIALIZABLE 串行化(行锁) 修改事务隔离级别:

set tx_isolation='SERIALIZABLE'; (可以加global,让新开的session也使用该事务隔离级别)

经测试,如果有三个session(mysql客户端),其中一个(a)是SERIALIZABLE,而其他两个(b,c)是READ_UNCOMMITTED,那么客户端b,c之间不会因为锁挂起,而ab或ac会导致(行)锁挂起

REPEATABLE_READ重复读#

REPEATABLE_READ重复读的隔离级别只能在一定程度上防止幻读 如果事务a(插入一条新数据,或者删除一条数据)后提交。那么变动的数据不会在事务b被查出来。但是如果插入的这条数据(在事务b,注意 不是事务a)被修改了,那么之后它会在事务b被查出来

新增后修改#

事务a新增数据后提交,在事务b执行中修改了该数据,则该数据会显示出来(影响了)

#=======事务b=======
mysql> begin;
mysql> select * from user;
+----+-----+
| id | age |
+----+-----+
|  1 |  10 |
|  2 |  20 |
|  3 |  33 |
+----+-----+
#=======事务a=======
mysql> insert into user(age) values(44);
#=======事务b=======
mysql> select * from user;
+----+-----+
| id | age |
+----+-----+
|  1 |  10 |
|  2 |  20 |
|  3 |  33 |
+----+-----+
#=======事务a=======
mysql> update user set age=55 where id = 4;
#=======事务b=======
mysql> select * from user;
+----+-----+
| id | age |
+----+-----+
|  1 |  10 |
|  2 |  20 |
|  3 |  33 |
+----+-----+
#=======事务b=======
mysql> update user set age=55 where id = 4;
mysql> select * from user;
+----+-----+
| id | age |
+----+-----+
|  1 |  10 |
|  2 |  20 |
|  3 |  33 |
|  4 |  55 |
+----+-----+
##此时如果事务a进行 delete from user where id = 4; 那么事务a会阻塞,直到事务b commit

删除后修改#

事务a删除数据后提交,在事务b执行中修改了该数据,该数据也照样存在(不影响)

03存储引擎

表结构、数据、索引

文件目录#

 root@db211:/var/lib/mysql/mysql# ls | tail -16
tables_priv.frm
tables_priv.MYD
tables_priv.MYI
time_zone.frm
time_zone.ibd
time_zone_leap_second.frm
time_zone_leap_second.ibd
time_zone_name.frm
time_zone_name.ibd
time_zone_transition.frm
time_zone_transition.ibd
time_zone_transition_type.frm   #INNODB存储引擎-表结构
time_zone_transition_type.ibd   #INNODB存储引擎-表数据+表索引(包括所有索引)
user.frm   #MyISAM存储引擎-表结构
user.MYD   #MyISAM存储引擎-表数据
user.MYI   #MyISAM存储引擎-表索引

INNODE存储引擎的表主键聚簇索引和数据在同一个文件(所以即使没有设置主键,innodb也会为每一行自动生成一个默认的隐藏主键列,用来形成B+树)

索引#

索引创建#

建表时创建#

create table index1( id int, name varchar(20), index idx_id_name (id,name));

后续添加#

create index idx_name on bank_bill (name);

B树#

m阶B树 ,m个分支(或者说m个区间)
x个元素就会有x+1个区间

B+树和B树区别#

从B树考虑#

log2 2000万≈24.3 log500 2000万≈3 (m阶平衡树) 如果是500阶B树,那么2000万个节点最多只需要3层

从B+树考虑#

MySQL中,一个节点1页,也就是16KB(可以改),而一个节点可以放(主键bigint(8字节)+页号地址 6字节)单元1170个,假设是1000,那两层可以放100 0000万个节点。由于1个节点16KB,如果一条数据用1KB的话,那么最底层每个叶子节点就是放16条数据。那就是3层可以放1600万条数据。这是估算,细算的话应该是2000万左右

其他#

操作系统内存管理按页面4K为单位。MySQL默认使用InnoDB存储引擎,其数据块大小(block size)一般默认为16KB

using index#

mysql> explain select name from student where name = 'liuxiang' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: student
   partitions: NULL
         type: ref
possible_keys: idx_name
          key: idx_name
      key_len: 152
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using index (直接在二级索引搜索就结束了)

下面这个还需要回表

02增删改查

分类#

  • DDL 数据定义语言(create,drop,alter)
  • DML 数据操纵语言(insert,delete,update,select)
  • DCL 数据控制语言(grant,revoke)

删除#

mysql> select * from user;
+----+----------+-----+-----+
| id | name     | age | sex |
+----+----------+-----+-----+
|  1 | zhangsan |  20 | M   |
|  2 | wuqi     |  34 | M   |
|  5 | baobaoi  |  45 | W   |
|  6 | lalal    |  44 | W   |
+----+----------+-----+-----+
4 rows in set (0.00 sec)
mysql> delete from user where id = 6
#如果之后重启了mysql(且id最大为5,则再插入的数据id为6),(如果id最大为10,则插入的数据id为11);如果没有重启,则再插入的数据id为7
mysql> nsert into user(name,age,sex) values('hah',35,'W');

新增#

一次新增多条数据和多次新增单条数据区别

搜索#

limit可以优化sql语句,不过要看数据的位置

mysql> select * from t_user  limit 1999999 ,1;
+---------+------------------+------------+
| id      | email            | password   |
+---------+------------------+------------+
| 2000000 | cwng78@yahoo.com | h4HcxZKBNQ |
+---------+------------------+------------+
1 row in set (0.52 sec)
#这条数据在最后一条,所以优化效果不怎么样
mysql> select * from t_user where email='cwng78@yahoo.com';
+---------+------------------+------------+
| id      | email            | password   |
+---------+------------------+------------+
| 2000000 | cwng78@yahoo.com | h4HcxZKBNQ |
+---------+------------------+------------+
1 row in set (0.47 sec)

在第一条或者前面,则优化效果显著

01基础知识

本课程大纲#

部分扩展知识#

MySQL分为服务端和客户端(两进程),SQLite是进程程序,客户端和服务端都在同一个进程操作数据

安装(windows)#

安装目录下包括数据目录(Data/)和配置文件(my.ini)

安装(debian)#

数据目录#

root@db211:/etc/mysql# ls -l /var/lib/mysql | awk '{print $1,$3,$4,$9}'
total   
-rw-r----- mysql mysql auto.cnf
-rw------- mysql mysql ca-key.pem
-rw-r--r-- mysql mysql ca.pem
-rw-r--r-- mysql mysql client-cert.pem
-rw------- mysql mysql client-key.pem
-rw-r----- mysql mysql ib_buffer_pool
-rw-r----- mysql mysql ibdata1
-rw-r----- mysql mysql ib_logfile0
-rw-r----- mysql mysql ib_logfile1
-rw-r----- mysql mysql ibtmp1
drwxr-x--- mysql mysql mysql
drwxr-x--- mysql mysql performance_schema
-rw------- mysql mysql private_key.pem
-rw-r--r-- mysql mysql public_key.pem
-rw-r--r-- mysql mysql server-cert.pem
-rw------- mysql mysql server-key.pem
drwxr-x--- mysql mysql sys
drwxr-x--- mysql mysql xx

配置目录#

root@db211:/etc/mysql# ls -l | awk '{print $1,$9,$10,$11}'
total   
drwxr-xr-x conf.d  
lrwxrwxrwx my.cnf -> /etc/alternatives/my.cnf
-rw-r--r-- my.cnf.fallback  
-rw-r--r-- mysql.cnf  
drwxr-xr-x mysql.conf.d

再进一步查看

05MachineLevelProgramming01Basic

概述#

计算机为了运行程序,会执行一串独立的指令
两种形式的机器语言:

  1. 在计算机上运行的实际目标代码(字节,二进制01)
  2. 汇编代码(编译器的目标,文本格式)

不写汇编,但需要学习:

  1. 编译器产生的结果(汇编及目标代码)与文本代码关联
  2. 低级代码(汇编、二进制)如何实现高级别程序构造
  3. 过程函数、结构体、数组在机器语言中的实现

课程选择64位版本的Intel指令集
为什么叫x86处理器16位微处理器(也称CISC复杂,与之对应的是RISC精简),因为一开始的处理器是8086,接着8286,8386…
需要更加关注–gcc编译生成的代码长什么样

x86进化#

多核处理器#

另一个公司:AMD

教学范围#

哪些处理器#

  • ARM(AcornRISCMachine),比x86机器功耗更低
  • x86

定义#

  • 指令集
  • 寄存器:非常小的内存位置
  • 机器指令

(无法直接操作缓存(没有这个概念),是机器级别的概念)

将c源代码转换成目标文件#

汇编简介#

gcc -Og -S sum.c #-Og 调试级别的优化过的(英文字母大写O); -S 生成汇编 


带点的这些,是给调试器用的(用来定位);也有一些是给链接器用(告诉它这是个全局定义的函数)

数据类型#

  1. 多种整数数据类型(不区分符号与无符号,都以数字形式存储在计算机)
  2. 浮点(使用和整型不一样的寄存器组)
  3. 程序本身在x86中,是一系列字节
  4. 没有数组或结构,由编译器人工处理

指令#

每条指令做的事很有限(只做一件小事)

反汇编#

举例#

  • 变量的概念在汇编级别代码中完全消失(变成了寄存器或内存中某个东西)

使用gdb反汇编#

第26章_写作本书时用到的一些重要的参考资料

第26章 写作本书时用到的一些重要的参考资料

感谢#

我不生产知识,只是知识的搬运工。写作本小册的时间主要用在了两个方面:

搞清楚事情的本质是什么。

这个过程就是研究源码、书籍和资料。

如何把我已经知道的知识表达出来。

这个过程就是我不停的在地上走过来走过去,梳理知识结构,斟酌用词用句,不停的将已经写好的文章推倒重来,只是想给大家一个不错的用户体验。

这两个方面用的时间基本上是一半一半吧,在搞清楚事情的本质是什么阶段,除了直接阅读MySQL的源码之外,查看参考资料也是一种比较偷懒的学习方式。本书只是MySQL进阶的一个入门,想了解更多关于MySQL的知识,大家可以从下面这些资料里找点灵感。

一些链接#

MySQL官方文档: https://dev.mysql.com/doc/refman/5.7/en/

MySQL官方文档是写作本书时参考最多的一个资料。说实话,文档写的非常通俗易懂,唯一的缺点就是太长了,导致大家看的时候无从下手。

MySQL Internals Manual: https://dev.mysql.com/doc/internals/en/

介绍MySQL如何实现各种功能的文档,写的比较好,但是太少了,有很多章节直接跳过了。

何登成的github: https://github.com/hedengcheng/tech

登博的博客非常好,对事务、优化这讨论的细节也非常多,不过由于大多是PPT结构,字太少,对上下文不清楚的同学可能会一脸懵逼。

orczhou的博客: http://www.orczhou.com/

Jeremy Cole的博客: https://blog.jcole.us/innodb/

Jeremy Cole大神不仅写作了innodb_ruby这个非常棒的解析InnoDB存储结构的工具,还对这些存储结构写了一系列的博客,在我几乎要放弃深入研究表空间结构的时候,是他老人家的博客把我又从深渊里拉了回来。

那海蓝蓝(李海翔)的博客: https://blog.csdn.net/fly2nn

taobao月报: http://mysql.taobao.org/monthly/

因为MySQL的源码非常多,经常让大家无从下手,而taobao月报就是一个非常好的源码阅读指南。

吐槽一下,这个taobao月报也只能当作源码阅读指南看,如果真的不看源码光看月报,那只能当作天书看,十有八九被绕进去出不来了。

MySQL Server Blog: http://mysqlserverteam.com/

MySQL team的博客,一手资料,在我不知道看什么的时候给了很多启示。

mysql_lover的博客: https://blog.csdn.net/mysql_lover/

mariadb的关于查询优化的文档: https://mariadb.com/kb/en/library/query-optimizations/

不得不说mariadb的文档相比MySQL的来说就非常有艺术性了(里边儿有很多漂亮的插图),我很怀疑MySQL文档是程序员直接写的,mariadb的文档是产品经理写的。当我们想研究某个功能的原理,在MySQL文档干巴巴的说明中找不到头脑时,可以参考一下mariadb娓娓道来的风格。

Reconstructing Data Manipulation Queries from Redo Logs: https://www.sba-research.org/wp-content/uploads/publications/WSDF2012_InnoDB.pdf

非官方优化文档: http://www.unofficialmysqlguide.com/optimizer-trace.html

这个文档非常好,非常非常好~

MySQL8.0的源码文档: https://dev.mysql.com/doc/dev/mysql-server

一些书籍#

《数据库查询优化器的艺术》李海翔著

大家可以把这本书当作源码观看指南来看,不过讲的是5.6的源码,5.7里重构了一些,不过大体的思路还是可以参考的。

《MySQL运维内参》周彦伟、王竹峰、强昌金著

第25章_工作面试老大难-锁

第25章 工作面试老大难-锁

解决并发事务带来问题的两种基本方式#

上一章介绍了事务并发执行时可能带来的各种问题,并发事务访问相同记录的情况大致可以划分为3种:

读-读情况:即并发事务相继读取相同的记录。

读取操作本身不会对记录有一毛钱影响,并不会引起什么问题,所以允许这种情况的发生。

写-写情况:即并发事务相继对相同的记录做出改动。

我们前面说过,在这种情况下会发生脏写的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过来实现的。这个所谓的其实是一个内存中的结构,在事务执行前本来是没有锁的,也就是说一开始是没有锁结构和记录进行关联的,如图所示:

当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构,当没有的时候就会在内存中生成一个锁结构与之关联。比方说事务T1要对这条记录做改动,就需要生成一个锁结构与之关联:

其实在锁结构里有很多信息,不过为了简化理解,我们现在只把两个比较重要的属性拿了出来: - trx信息:代表这个锁结构是哪个事务生成的。 - is_waiting:代表当前事务是否在等待。

如图所示,当事务T1改动了这条记录后,就生成了一个锁结构与该记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false,我们把这个场景就称之为获取锁成功,或者加锁成功,然后就可以继续执行操作了。

在事务T1提交之前,另一个事务T2也想对该记录做改动,那么先去看看有没有锁结构与这条记录关联,发现有一个锁结构与之关联后,然后也生成了一个锁结构与这条记录关联,不过锁结构is_waiting属性值为true,表示当前事务需要等待,我们把这个场景就称之为获取锁失败,或者加锁失败,或者没有成功的获取到锁,画个图表示就是这样:

在事务T1提交之后,就会把该事务生成的锁结构释放掉,然后看看还有没有别的事务在等待获取锁,发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算获取到锁了。效果图就是这样:

我们总结一下后续内容中可能用到的几种说法,以免大家混淆:

+  

不加锁

意思就是不需要在内存中生成对应的锁结构,可以直接执行操作。

+  

获取锁成功,或者加锁成功

意思就是在内存中生成了对应的锁结构,而且锁结构的is_waiting属性为false,也就是事务可以继续执行操作。

+  

获取锁失败,或者加锁失败,或者没有获取到锁

意思就是在内存中生成了对应的锁结构,不过锁结构的is_waiting属性为true,也就是事务需要等待,不可以继续执行操作。

小贴士:这里只是对锁结构做了一个非常简单的描述,我们后边会详细介绍介绍锁结构的,稍安勿躁。

读-写写-读情况:也就是一个事务进行读取操作,另一个进行改动操作。

我们前面说过,这种情况下可能发生脏读不可重复读幻读的问题。

小贴士:幻读问题的产生是因为某个事务读了一个范围的记录,之后别的事务在该范围内插入了新记录,该事务再次读取该范围的记录时,可以读到新插入的记录,所以幻读问题准确的说并不是因为读取和写入一条相同记录而产生的,这一点要注意一下。

SQL标准规定不同隔离级别下可能发生的问题不一样: - 在READ UNCOMMITTED隔离级别下,脏读不可重复读幻读都可能发生。 - 在READ COMMITTED隔离级别下,不可重复读幻读可能发生,脏读不可以发生。 - 在REPEATABLE READ隔离级别下,幻读可能发生,脏读不可重复读不可以发生。 - 在SERIALIZABLE隔离级别下,上述问题都不可以发生。

不过各个数据库厂商对SQL标准的支持都可能不一样,与SQL标准不同的一点就是,MySQLREPEATABLE READ隔离级别实际上就已经解决了幻读问题。

怎么解决脏读不可重复读幻读这些问题呢?其实有两种可选的解决方案:

+  

方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁

所谓的MVCC我们在前一章有过详细的描述,就是通过生成一个ReadView,然后通过ReadView找到符合条件的记录版本(历史版本是由undo日志构建的),其实就像是在生成ReadView的那个时刻做了一次时间静止(就像用相机拍了一个快照),查询语句只能读到在生成ReadView之前已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突。

小贴士:我们说过普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。在READ COMMITTED隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改,也就是避免了脏读现象;REPEATABLE READ隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView,之后的SELECT操作都复用这个ReadView,这样也就避免了不可重复读和幻读的问题。

+  

方案二:读、写操作都采用加锁的方式。

第24章_一条记录的多幅面孔-事务的隔离级别与MVCC

第24章 一条记录的多幅面孔-事务的隔离级别与MVCC

事前准备#

为了故事的顺利发展,我们需要创建一个表: CREATE TABLE hero ( number INT, name VARCHAR(100), country varchar(100), PRIMARY KEY (number) ) Engine=InnoDB CHARSET=utf8; 小贴士:注意我们把这个hero表的主键命名为number,而不是id,主要是想和后边要用到的事务id做区别,大家不用大惊小怪~ 然后向这个表里插入一条数据: INSERT INTO hero VALUES(1, '刘备', '蜀'); 现在表里的数据就是这样的: mysql> SELECT * FROM hero; +--------+--------+---------+ | number | name | country | +--------+--------+---------+ | 1 | 刘备 | 蜀 | +--------+--------+---------+ 1 row in set (0.00 sec)

事务隔离级别#

我们知道MySQL是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称之为一个会话(Session)。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。在事务简介的章节中我们说过事务有一个称之为隔离性的特性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样子的话对性能影响太大,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高些,鱼和熊掌不可得兼,舍一部分隔离性而取性能者也。

事务并发执行遇到的问题#

怎么个舍弃法呢?我们先得看一下访问相同数据的事务在不保证串行执行(也就是执行完一个再执行另一个)的情况下可能会出现哪些问题:

脏写(Dirty Write

如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了脏写,示意图如下:

如上图,Session ASession B各开启了一个事务,Session B中的事务先将number列为1的记录的name列更新为'关羽',然后Session A中的事务接着又把这条number列为1的记录的name列更新为张飞。如果之后Session B中的事务进行了回滚,那么Session A中的更新也将不复存在,这种现象就称之为脏写。这时Session A中的事务就很懵逼,我明明把数据更新了,最后也提交事务了,怎么到最后说自己什么也没干呢?

第23章_后悔了怎么办-undo日志(下)

第23章 后悔了怎么办-undo日志(下)

上一章我们主要介绍了为什么需要undo日志,以及INSERTDELETEUPDATE这些会对数据做改动的语句都会产生什么类型的undo日志,还有不同类型的undo日志的具体格式是什么。本章会继续介绍这些undo日志会被具体写到什么地方,以及在写入过程中需要注意的一些问题。

通用链表结构#

在写入undo日志的过程中会使用到多个链表,很多链表都有同样的节点结构,如图所示:

在某个表空间内,我们可以通过一个页的页号和在页内的偏移量来唯一定位一个节点的位置,这两个信息也就相当于指向这个节点的一个指针。所以: - Pre Node Page NumberPre Node Offset的组合就是指向前一个节点的指针 - Next Node Page NumberNext Node Offset的组合就是指向后一个节点的指针。

整个List Node占用12个字节的存储空间。

为了更好的管理链表,设计InnoDB的大佬还提出了一个基节点的结构,里边存储了这个链表的头节点尾节点以及链表长度信息,基节点的结构示意图如下:

其中: - List Length表明该链表一共有多少节点。 - First Node Page NumberFirst Node Offset的组合就是指向链表头节点的指针。 - Last Node Page NumberLast Node Offset的组合就是指向链表尾节点的指针。

整个List Base Node占用16个字节的存储空间。

所以使用List Base NodeList Node这两个结构组成的链表的示意图就是这样:

小贴士:上述链表结构我们在前面的文章中频频提到,尤其是在表空间那一章重点描述过,不过我不敢奢求大家都记住了,所以在这里又强调一遍,希望大家不要嫌我烦,我只是怕大家忘了学习后续内容吃力而已~

FIL_PAGE_UNDO_LOG页面#

我们前面介绍表空间的时候说过,表空间其实是由许许多多的页面构成的,页面默认大小为16KB。这些页面有不同的类型,比如类型为FIL_PAGE_INDEX的页面用于存储聚簇索引以及二级索引,类型为FIL_PAGE_TYPE_FSP_HDR的页面用于存储表空间头部信息的,还有其他各种类型的页面,其中有一种称之为FIL_PAGE_UNDO_LOG类型的页面是专门用来存储undo日志的,这种类型的页面的通用结构如下图所示(以默认的16KB大小为例):

“类型为FIL_PAGE_UNDO_LOG的页”这种说法太绕口,以后我们就简称为Undo页面了。上图中的File HeaderFile Trailer是各种页面都有的通用结构,我们前面介绍过很多遍了,这里就不赘述了(忘记了的可以到讲述数据页结构或者表空间的章节中查看)。Undo Page HeaderUndo页面所特有的,我们来看一下它的结构:

其中各个属性的意思如下:

TRX_UNDO_PAGE_TYPE:本页面准备存储什么种类的undo日志

我们前面介绍了好几种类型的undo日志,它们可以被分为两个大类:

第22章_后悔了怎么办-undo日志(上)

第22章 后悔了怎么办-undo日志(上)

事务回滚的需求#

我们说过事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。但是偏偏有时候事务执行到一半会出现一些情况,比如: - 情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。 - 情况二:程序员可以在事务执行过程中手动输入ROLLBACK语句结束当前的事务的执行。

这两种情况都会导致事务执行到一半就结束,但是事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,我们需要把东西改回原先的样子,这个过程就称之为回滚(英文名:rollback),这样就可以造成一个假象:这个事务看起来什么都没做,所以符合原子性要求。

小时候我非常痴迷于象棋,总是想找厉害的大人下棋,赢棋是不可能赢棋的,这辈子都不可能赢棋的,又不想认输,只能偷偷的悔棋才能勉强玩的下去。悔棋就是一种非常典型的回滚操作,比如棋子往前走两步,悔棋对应的操作就是向后走两步;比如棋子往左走一步,悔棋对应的操作就是向右走一步。数据库中的回滚跟悔棋差不多,你插入了一条记录,回滚操作对应的就是把这条记录删除掉;你更新了一条记录,回滚操作对应的就是把该记录更新为旧值;你删除了一条记录,回滚操作对应的自然就是把该记录再插进去。说的貌似很简单的样子[手动偷笑😏]。

从上面的描述中我们已经能隐约感觉到,每当我们要对一条记录做改动时(这里的改动可以指INSERTDELETEUPDATE),都需要留一手 —— 把回滚时所需的东西都给记下来。比方说: - 你插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。 - 你删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。 - 你修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。

设计数据库的大佬把这些为了回滚而记录的这些东东称之为撤销日志,英文名为undo log,我们也可以土洋结合,称之为undo日志。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo日志。在真实的InnoDB中,undo日志其实并不像我们上面所说的那么简单,不同类型的操作产生的undo日志的格式也是不同的,不过先暂时把这些容易让人脑子糊的具体细节放一放,我们先回过头来看看事务id是个神马玩意儿。

事务id#

给事务分配id的时机#

我们前面在介绍事务简介时说过,一个事务可以是一个只读事务,或者是一个读写事务:

我们可以通过START TRANSACTION READ ONLY语句开启一个只读事务。

在只读事务中不可以对普通的表(其他事务也能访问到的表)进行增、删、改操作,但可以对临时表做增、删、改操作。

我们可以通过START TRANSACTION READ WRITE语句开启一个读写事务,或者使用BEGINSTART TRANSACTION语句开启的事务默认也算是读写事务。

在读写事务中可以对表执行增删改查操作。

如果某个事务执行过程中对某个表执行了增、删、改操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id,分配方式如下:

对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务id,否则的话是不分配事务id的。

小贴士:我们前面说过对某个查询语句执行EXPLAIN分析它的查询计划时,有时候在Extra列会看到Using temporary的提示,这个表明在执行该查询语句时会用到内部临时表。这个所谓的内部临时表和我们手动用CREATE TEMPORARY TABLE创建的用户临时表并不一样,在事务回滚时并不需要把执行SELECT语句过程中用到的内部临时表也回滚,在执行SELECT语句用到内部临时表时并不会为它分配事务id。 - 对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行增、删、改操作时才会为这个事务分配一个事务id,否则的话也是不分配事务id的。

有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务id

说了半天,事务id有什么子用?这个先保密,后边会一步步的详细介绍。现在只要知道只有在事务对表中的记录做改动时才会为这个事务分配一个唯一的事务id小贴士:上面描述的事务id分配策略是针对MySQL 5.7来说的,前面的版本的分配方式可能不同~

事务id是怎么生成的#

这个事务id本质上就是一个数字,它的分配策略和我们前面提到的对隐藏列row_id(当用户没有为表创建主键和UNIQUE键时InnoDB自动创建的列)的分配策略大抵相同,具体策略如下: - 服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务id时,就会把该变量的值当作事务id分配给该事务,并且把该变量自增1。 - 每当这个变量的值为256的倍数时,就会将该变量的值刷新到系统表空间的页号为5的页面中一个称之为Max Trx ID的属性处,这个属性占用8个字节的存储空间。 - 当系统下一次重新启动时,会将上面提到的Max Trx ID属性加载到内存中,将该值加上256之后赋值给我们前面提到的全局变量(因为在上次关机时该全局变量的值可能大于Max Trx ID属性值)。

这样就可以保证整个系统中分配的事务id值是一个递增的数字。先被分配id的事务得到的是较小的事务id,后被分配id的事务得到的是较大的事务id

trx_id隐藏列#

我们前面介绍InnoDB记录行格式的时候重点强调过:聚簇索引的记录除了会保存完整的用户数据以外,而且还会自动添加名为trx_id、roll_pointer的隐藏列,如果用户没有在表中定义主键以及UNIQUE键,还会自动添加一个名为row_id的隐藏列。所以一条记录在页面中的真实结构看起来就是这样的:

其中的trx_id列其实还蛮好理解的,就是某个对这个聚簇索引记录做改动的语句所在的事务对应的事务id而已(此处的改动可以是INSERTDELETEUPDATE操作)。至于roll_pointer隐藏列我们后边分析~