Mysql事务

事务分类

隔离级别 异常情况 异常情况 异常情况
读未提交 脏读 不可重复读 幻读
读已提交 不可重复读 幻读
可重复读 幻读
序列化

事务测试

查询事务隔离级别

  1. select @@tx_isolation;

修改隔离级别

  1. set session transaction isolation level read committed;

关闭自动提交

  • 打开mysql的命令行,将自动提交事务并关闭
    1. --查看是否是自动提交 1表示开启,0表示关闭
    2. select @@autocommit;
    3. --设置关闭
    4. set autocommit = 0;

    事务启动方式

    -- 开启事务
    begin/start transaction
    -- 提交事务
    commit;
    -- 回滚事务
    rollback
    -- 提交当前事务,并开启下个事务
    commit work and chain
    

    begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。 你只要想,mysql在设计的时候肯定是最大考虑并发,这样就能理解了。 如果想立马启动一个事务可以使用如下命令start transaction with consistent snapshot;

�数据准备

--创建数据库
create database tran;
--切换数据库 两个窗口都执行
use tran;
--准备数据
 create table psn(id int primary key,name varchar(10)) engine=innodb;
--插入数据
insert into psn values(1,'zhangsan');
insert into psn values(2,'lisi');
insert into psn values(3,'wangwu');
commit;

测试事务

�测试1:脏读 read uncommitted

脏读:一个事务正在对一条记录做修改,在这个事务提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”的数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做“脏读”。�

  • �sql ```sql set session transaction isolation level read uncommitted;

A:start transaction; A:select from psn; B:start transaction; B:select from psn; A:update psn set name=’msb’; A:select from psn; B:select from psn; —读取的结果msb。产生脏读,因为A事务并没有commit,读取到了不存在的数据 A:commit; B:select * from psn; —读取的数据是msb,因为A事务已经commit,数据永久的被修改


- 结果
- ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25955514/1645342014812-84d1caaf-f097-4b87-828f-479a38579111.png#clientId=u8a19f054-4729-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=602&id=u60ccdc2e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1204&originWidth=942&originalType=binary&ratio=1&rotation=0&showTitle=false&size=123766&status=done&style=none&taskId=u5a187df9-e272-41b8-b883-a3d64f105f0&title=&width=471)
- ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25955514/1645342057198-24f586a5-d3a0-4d27-934c-0434222c4187.png#clientId=u8a19f054-4729-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=547&id=u3a29c5b3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1094&originWidth=954&originalType=binary&ratio=1&rotation=0&showTitle=false&size=92062&status=done&style=none&taskId=uc828bd9a-7462-43d9-bc94-353d130c083&title=&width=477)
<a name="xp28f"></a>
#### 测试2:当使用`read committed`的时候,就不会出现脏读的情况了,但是会出现不可重复读的问题
> 不可重复读:一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫做**“不可重复读”**。 

- �sql
```sql
set session transaction isolation level read committed;
A:start transaction;
A:select * from psn;
B:start transaction;
B:select * from psn;
--执行到此处的时候发现,两个窗口读取的数据是一致的
A:update psn set name ='zhangsan' where id = 1;
A:select * from psn;
B:select * from psn;
--执行到此处发现两个窗口读取的数据不一致,B窗口中读取不到更新的数据
A:commit;
A:select * from psn;--读取到更新的数据
B:select * from psn;--也读取到更新的数据
--发现同一个事务中多次读取数据出现不一致的情况
  • 执行结果
  • image.png
  • image.png

    测试3:当使用repeatable read的时候,就不会出现不可重复读的问题,但是会出现幻读的问题

    幻读:指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的

  • sql

    set session transaction isolation level repeatable read;
    A:start transaction;
    A:select * from psn;
    B:start transaction;
    B:select * from psn;
    --此时两个窗口读取的数据是一致的
    A:insert into psn values(4,'sisi');
    A:commit;
    A:select * from psn;--读取到添加的数据
    B:select * from psn;--读取不到添加的数据
    B:insert into psn values(4,'sisi');--报错,无法插入数据
    --此时发现读取不到数据,但是在插入的时候不允许插入,出现了幻读,设置更高级别的隔离级别即可解决
    
  • 执行结果

  • image.png
  • image.png

    事务隔离的实现

  • 数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。

  • 在“读未提交”隔离级别下:直接返回记录上的最新值,没有视图概念
  • 在“读提交”隔离级别下:这个视图是在每个 SQL 语句开始执行的时候创建的。
  • 在“可重复读”隔离级别下:这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
  • 在“串行化”隔离级别下:直接用加锁的方式来避免并行访问。

    Mysql的MVCC

  1. 在Mysql中更新一条记录,会在Redo Log里记录,也会在Undo Log里记录一条回滚操作。
  2. 假设一个值从1被按顺序改为了2、3、4,那么在回滚日志中会记录如下:

image.png

同一条记录在Mysql中可以存在多个版本,这就是Mysql的MVCC多版本并发控制

  • 避免存在长事务,通过以下命令查询事务执行时间大于60秒

    select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
    

    举例

  • sql语句

    CREATE TABLE `t` (
    `id` int(11) NOT NULL,
    `k` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB;
    insert into t(id, k) values(1,1),(2,2);
    

    image.png

    结果

  • 事务A查询到的值是1

  • 事务B查询到的值是3

    解释

  • 事务B在执行的时候会使用当前读,读到的是2,更新为3。

  • 事务A在执行的时候使用的是一致性读,也就是事务创建的时候的值。

    • 如果是RR:则值为1
    • 如果是RC:则值为2

      Mysql锁机制

      MySQL锁的基本介绍

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

  • 相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking)InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

  • 从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适!仅从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。

    MyISAM表锁

  • MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)

  • 对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞自己对同一表的写请求;
  • 对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的!

    建表语句

    ``sql CREATE TABLEmylock(idint(11) NOT NULL AUTO_INCREMENT,NAMEvarchar(20) DEFAULT NULL, PRIMARY KEY (id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

INSERT INTO mylock (id, NAME) VALUES (‘1’, ‘a’); INSERT INTO mylock (id, NAME) VALUES (‘2’, ‘b’); INSERT INTO mylock (id, NAME) VALUES (‘3’, ‘c’); INSERT INTO mylock (id, NAME) VALUES (‘4’, ‘d’);

<a name="ersjF"></a>
### MyISAM写锁阻塞读的案例:

- 当一个线程获得对一个表的写锁之后,只有持有锁的线程可以对表进行更新操作。其他线程的读写操作都会等待,直到锁释放为止。
| session1 | session2 |
| --- | --- |
| 获取表的write锁定<br />lock table mylock write; |  |
| 当前session对表的查询,插入,更新操作都可以执行<br />select * from mylock;<br />insert into mylock values(5,'e'); | 当前session对表的查询会被阻塞<br />select * from mylock; |
| 释放锁:<br />unlock tables; | 当前session能够立刻执行,并返回对应结果 |

<a name="rbSKD"></a>
### MyISAM读阻塞写的案例

- �一个session使用`lock table`给表加读锁,这个session可以锁定表中的记录,但更新和访问其他表都会提示错误,同时,另一个session可以查询表中的记录,但更新就会出现锁等待。
| session1 | session2 |
| --- | --- |
| 获得表的read锁定<br />`lock table mylock read;` |  |
| 当前session可以查询该表记录:<br />`select * from mylock;` | 当前session可以查询该表记录:<br />`select * from mylock;` |
| 当前session不能查询没有锁定的表<br />`select * from person`<br />Table 'person' was not locked with LOCK TABLES | 当前session可以查询或者更新未锁定的表<br />`select * from person;`<br />`insert into person values(1,'zhangsan');` |
| 当前session插入或者更新表会提示错误<br />`insert into mylock values(6,'f')`<br />Table 'mylock' was locked with a READ lock and can't be updated<br />`update mylock set name='aa' where id = 1;`<br />Table 'mylock' was locked with a READ lock and can't be updated | 当前session插入数据会等待获得锁<br />`insert into mylock values(6,'f');` |
| 释放锁<br />unlock tables; | 获得锁,更新成功 |

- MyISAM在执行查询语句之前,会自动给涉及的所有表加读锁,在执行更新操作前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要使用命令来显式加锁,上例中的加锁时为了演示效果。
<a name="hX98J"></a>
### 查看锁竞争情况
> 可以通过检查`table_locks_waited`和`table_locks_immediate`状态变量来分析系统上的表锁定争夺:

```sql
mysql> show status like 'table_locks%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Table_locks_immediate | 352   |
| Table_locks_waited    | 2     |
+-----------------------+-------+
--如果Table_locks_waited的值比较高,则说明存在着较严重的表级锁争用情况。

InnoDB锁

全局锁

对整个库加锁,整个库都在只读状态,适用于做全库逻辑备份

Flush tables with read lock
  • 也可以使用可重复读隔离级别完成此任务。
  • 也可以使用命令完成

    • set global readonly=true
    • 让全库都进入只读,但是此方式不如第一种,如果出现异常并不会自动释放只读。

      InnoDB的行锁模式及加锁方法

      共享锁(S)又称读锁:

  • 允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。

  • 若事务T对数据对象A加上S锁,则事务T可以读A但其它事务不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。
  • 这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

    排他锁(X)又称写锁:

  • 允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。

  • 若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。

    MySQL InnoDB引擎默认的修改数据语句:

  • update,delete,insert都会自动给涉及到的数据加上排他锁。

  • select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。
  • 所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

    InnoDB行锁实现方式

    �InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁。否则,InnoDB将使用表锁!

在不通过索引条件查询的时候,innodb使用的是表锁而不是行锁

create table tab_no_index(
  id int,
  name varchar(10)
) engine=innodb;
insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');
session1 session2
set autocommit=0
select * from tab_no_index where id = 1;
set autocommit=0
select * from tab_no_index where id =2
select * from tab_no_index where id = 1 for update
select * from tab_no_index where id = 2 for update;
  • session1只给一行加了排他锁,但是session2在请求其他行的排他锁的时候,会出现锁等待。原因是在没有索引的情况下,innodb只能使用表锁

    �创建带索引的表进行条件查询,innodb使用的是行锁

    create table tab_with_index(
    id int,
    name varchar(10)
    ) engine=innodb;
    alter table tab_with_index add index id(id);
    insert into tab_with_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');
    

    | session1 | session2 | | —- | —- | | set autocommit=0
    select from tab_with_index where id = 1; | set autocommit=0
    select
    from tab_with_index where id =2 | | select from tab_with_index where id = 1 for update | | | | select from tab_with_index where id = 2 for update; |

  • 此时可以看到mysql不会阻塞�

    由于mysql的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现冲突的。(表锁)

    alter table tab_with_index drop index id;
    insert into tab_with_index  values(1,'4');
    

    | session1 | session2 | | —- | —- | | set autocommit=0 | set autocommit=0 | | select from tab_with_index where id = 1 and name=’1’ for update | | | | select from tab_with_index where id = 1 and name=’4’ for update
    虽然session2访问的是和session1不同的记录,但是因为使用了相同的索引,所以需要等待锁 |

查看锁竞争情况

可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况

mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 18702 |
| Innodb_row_lock_time_avg      | 18702 |
| Innodb_row_lock_time_max      | 18702 |
| Innodb_row_lock_waits         | 1     |
+-------------------------------+-------+
--如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高

记录锁(Record lock)

它是建立在索引记录上的锁(主键和唯一索引都算)�,如果没有主键或唯一索引,那么innodb会退化为表锁。

间隙锁(Gap lock)

只有在InnoDB的RR隔离级别中才会生效。 MySQL默认事务隔离级别是可重复读(RR),这个隔离级别为了避免幻读现象,引入了这个间隙锁,对索引项之间的间隙上锁。

  • 他指的是在索引记录之间的间隙上的锁,或者在第一个索引记录之前或最后一个索引记录之后的间隙上的锁。
  • 对于具有唯一搜索条件的唯一索引,InnoDB只锁定找到的索引记录,而不会锁定间隙。
  • 对于其他搜索条件,InnoDB锁定扫描的索引范围,使用gap lock或next-key lock来阻塞其他事务插入范围覆盖的间隙。

    sql

    ```sql CREATE TABLE t_url_mapping ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘id’ , url varchar(5000) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT ‘原url’ , expire_in bigint(255) NULL DEFAULT NULL COMMENT ‘过期时间点(毫秒)’ , create_time datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’ , PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8 COLLATE=utf8_bin COMMENT=’短链接映射表’ AUTO_INCREMENT=1 ROW_FORMAT=COMPACT;

INSERT INTO t_url_mapping (id, url, expire_in, create_time) VALUES (1, ‘www.baidu.com’, NULL, NULL); INSERT INTO t_url_mapping (id, url, expire_in, create_time) VALUES (2, ‘www.95303.com’, NULL, NULL);

| session1 | session2 |
| --- | --- |
| set autocommit=0; | set autocommit=0 |
|                                start transaction; | <br /> |
| select * from t_url_mapping where id>3 lock in share mode; | <br /> |
| <br /> | start transaction; |
| <br /> | insert into t_url_mapping (id, url, expire_in, create_time) VALUES (11, 'www.sina.com',NULL, NULL);<br />不提交,一直卡住。 |

<a name="TXS07"></a>
## 临键锁(Next-Key Lock)
> 只有在InnoDB的RR隔离级别中才会生效。
> Next-Key锁是索引记录上的记录锁和索引记录之前间隙上的间隙锁的组合。

- 每个数据行上的**_非唯一索引_**列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
- 假设一个索引包含值10、11、13和20。此索引可能的next-key锁包括以下区间:
```sql
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, ∞ ]

sql

CREATE TABLE student (
    id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
    age bigint(20) NOT NULL COMMENT '年龄',
    name varchar(100) NULL DEFAULT NULL COMMENT '姓名',
    PRIMARY KEY (id),
    KEY age (age) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

INSERT INTO student (id, age, name) VALUES (1, 10, 'Lee');
INSERT INTO student (id, age, name) VALUES (3, 24, 'Soraka');
INSERT INTO student (id, age, name) VALUES (5, 32, 'Zed');
INSERT INTO student (id, age, name) VALUES (7, 45, 'Talon');
-- 该表中age列潜在的临键锁有: (-∞, 10], (10, 24], (24, 32], (32, 45], (45, +∞],
session1 session2
set autocommit=0; set autocommit=0
start transaction;
update student set name = ‘Vladimir’ where age = 24;
select * from student where age = 24 for update;

start transaction;
insert into student values(100, 26, ‘Ezreal’);
—一直卡住
  • 很明显,会话1在对age为 24 的列进行 UPDATE 操作的同时,也获取了(24, 32]这个区间内的临键锁。
  • 如果插入的age为非(24,32]之间的数据,则不会被锁住。

    为什么项目中使用RC而不是RR

  • 在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多!

  • 在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行!
  • 在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性!

    Mysql为什么选择RR作为默认隔离级别

    主从复制:是基于binlog复制的。 binlog格式:

    • statement:记录的是修改SQL语句
    • row:记录的是每行实际数据的变更
    • mixed:statement和row模式的混合
  • 那Mysql在5.0这个版本以前,binlog只支持STATEMENT这种格式!而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的,因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!

    sql

    image.png

    结论

  • 此时在主(master)上执行下列语句

    select * from test;
    --输出
    +---+
    | b |
    +---+
    | 3 |
    +---+
    1 row in set
    
  • 但是,你在此时在从(slave)上执行该语句,得出输出如下

    Empty set
    
  • 这样,你就出现了主从不一致性的问题!原因其实很简单,就是在master上执行的顺序为先删后插!而此时binlog为STATEMENT格式,它记录的顺序为先插后删!从(slave)同步的是binglog,因此从机执行的顺序和主机不一致!就会出现主从不一致!

    如何解决?

  • 解决方案有两种!

    • 隔离级别设为可重复读(Repeatable Read),在该隔离级别下引入间隙锁。当Session 1执行delete语句时,会锁住间隙。那么,Ssession 2执行插入语句就会阻塞住!
    • 将binglog的格式修改为row格式,此时是基于行的复制,自然就不会出现sql执行顺序不一样的问题!奈何这个格式在mysql5.1版本开始才引入。因此由于历史原因,mysql将默认的隔离级别设为可重复读(Repeatable Read),保证主从复制不出问题!

      如何给表安全的加字段

  • 解决长事务,事务不提交,就会一直占着 MDL 锁。要考虑先暂停 DDL,或者 kill 掉这个长事务。

  • 比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。
  • MariaDB 已经合并了 AliSQL 的这个功能,所以这两个开源分支目前都支持 DDL NOWAIT/WAIT n 这个语法。

    Mysql的索引

    回表

  • 回到主键索引树的过程

    索引覆盖

  • 这次查询所需要的数据已经在这颗索引树上了,比如id为主键,name为普通索引。

  • select id from Test where name =’’;

    最左前缀

  • 左字段的组合或左字段的一部分。

  • 减少每个字段都创建索引,联合索引(A,B),意味着建立了(A,B)和(B)的索引。

    索引下推

    Index Condition Pushdown,简称 ICP

  • 是Mysql 5.6版本引入的技术优化。旨在 在“仅能利用最左前缀索的场景”下(而不是能利用全部联合索引),对不在最左前缀索引中的其他联合索引字段加以利用

  • 在遍历索引时,就用这些其他字段进行过滤(where条件里的匹配)。过滤会减少遍历索引查出的主键条数,从而减少回表次数,提示整体性能。
  • 如果查询利用到了索引下推ICP技术,在Explain输出的Extra字段中会有“Using index condition”。即代表本次查询会利用到索引,且会利用到索引下推。
  • 索引下推技术的实现——在遍历索引的那一步,由只传入可以利用到的字段值,改成了多传入下推字段值。

    举例

    select * from tuser where name like '张%' and age=10 and ismale=1;
    -- 索引列为 name和age
    
    image.png

    InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。 在我们的这个例子中,只需要对 ID4、ID5 这两条记录回表取数据判断,就只需要回表 2 次。

普通索引和唯一索引的选择

Innodb的数据是以页为单位读取的,当需要根据索引来读取数据的时候,默认是读16K的页到内存中。

查询

  • 普通索引
    • 如果当前所要读取的数据都在索引页里,那么只需要判断下即可。
    • 如果当前读取的数据刚好在索引页的最后一行,那么只需要再读一页数据到内存。
  • 唯一索引
    • 读到数据即停止读取
  • 对比

    • 性能相差较小,微乎其微。

      更新

      引入Change Buffer 在不影响一致性的前提下,Mysql会将更新操作缓存在Change Buffer中。

  • 数据在内存页中

    • 普通索引
      • 找到 3 和 5 之间的位置,插入这个值,语句执行结束。
    • 唯一索引
      • 找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束。
    • 对比
      • 没有影响
  • 数据不在内存页
    • 普通索引
      • 将更新记录在 change buffer,语句执行就结束了。
    • 唯一索引
      • 所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插入 (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。
      • 如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。
      • 因此,唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。
  • 总结
    • 写多读少使用 change buffer 可以加快执行速度(减少数据页磁盘 io)。
    • 写后立马会做查询, 则会触发 change buffer 立即 merge 到磁盘, 这样的场景磁盘 io 次数不会减少,反而会增加 change buffer 的维护代价。
    • Change Buffer适用于账单、日志类的场景,写完后马上被访问到的概率小。

      索引优化实战

      创建表

      CREATE TABLE `employees` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
      `age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
      `position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
      `hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
      PRIMARY KEY (`id`),
      KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
      ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='员工记录表';
      INSERT INTO employees(name,age,position,hire_time) VALUES('LiLei',22,'manager',NOW());
      INSERT INTO employees(name,age,position,hire_time) VALUES('HanMeimei', 23,'dev',NOW());
      INSERT INTO employees(name,age,position,hire_time) VALUES('Lucy',23,'dev',NOW());
      

      使用存储过程插入数据

      drop procedure if exists insert_emp;
      delimiter ;;
      create procedure insert_emp()
      begin
      declare i int;
      set i=1;
      while(i<=100000)do
      insert into employees(name,age,position) values(CONCAT('zhuge',i),i,'dev');
      set i=i+1;
      end while;
      end;;
      delimiter ;
      call insert_emp();
      

      举一个大家不容易理解的综合例子

      联合索引第一个字段用范围不会走索引

      EXPLAIN SELECT * FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='manager';
      
      image.png
      结论:
      联合索引第一个字段就用范围查找不会走索引,mysql内部可能觉得第一个字段就用范围,结果集应该很大,回表效率不高,还不如全表扫描。

      强制走索引

      EXPLAIN SELECT * FROM employees force index(idx_name_age_position) WHERE name > 'LiLei' AND age = 22 AND position ='manager';
      
      image.png
      结论:
      虽然使用了强制走索引,让联合索引第一个地钻范围查找走了索引,扫描的行也少了一些,但是最终查找效率不一定比全表扫描高,因为回表效率不高。

      覆盖索引优化

      EXPLAIN SELECT name,age,position FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='mana er';
      
      image.png
      结论:
      只查索引列里的三个字段,会走索引。

      in和or在表数据量比较大的情况下会走索引,在表记录不多的情况下会选择全表扫描

      EXPLAIN SELECT * FROM employees WHERE name in ('LiLei','HanMeimei','Lucy') AND age = 22 AND position ='manager';
      
      image.png
      EXPLAIN SELECT * FROM employees WHERE (name = 'LiLei' or name = 'HanMeimei') AND age = 22 AND position ='manager';
      
      image.png
      结论:
      如果employess表只有几条记录,那么上述语句都不会走索引。

      like KK% 一般情况都会走索引

      EXPLAIN SELECT * FROM employees WHERE name like 'LiLei%' AND age = 22 AND position ='manager';
      
      image.png
      索引下推
      正常情况按照最左前缀原则,EXPLAIN SELECT * FROM employees WHERE name like 'LiLei%' AND age = 22 AND position ='manager'; 这种情况只会走name字段索引,因为根据name字段过滤完,得到的索引行里的age和position是无序的,无法很好的利用索引。
  1. 在mysql5.6之前,这个查询只能在联合索引里匹配到名字是'LiLei'开头的索引,然后拿这些索引对应的主键逐个回表,到主键索引上找出相应的记录,再比对age和position这两个字段的值是否符合。
  2. 在mysql5.6之后,可以在索引遍历过程中,对索引中包含的所有字段先做判断,过滤掉不符合条件的记录之后再回表,可以有效的减少回表次数。也就是说在匹配到名字是 'LiLei'开头的索引之后,同时还会在索引里过了age和position这两个字段,拿着过滤完剩下的索引对应的主键id再回表查整行数据。

    常见sql深入优化

    Case1

    EXPLAIN select * from employees where name='LiLei' and position='dev' order by age;
    

    image.png
    分析:
    利用最左前缀原则:中间字段不能断,因此查询用到了name索引,从key_len=74也能看出,age索引列用在排序过程中,因此没使用Using filesort

    Case2

    EXPLAIN select * from employees where name='LiLei' order by position;
    

    image.png
    分析:
    用了position进行排序,跳过了age,出现了Using filesort

    Case3

    EXPLAIN select * from employees where name='LiLei' order by age,position;
    

    image.png
    分析:
    只用索引name,而age和position用于排序,无Using filesort

    Case4

    EXPLAIN select * from employees where name='LiLei' order by position,age;
    

    image.png
    分析:
    因为索引顺序是name、age、position,但是排序 age和position颠倒位置了。

    Case5

    EXPLAIN select * from employees where name='LiLei' and age = 18 order by position,age;
    

    image.png
    分析:
    因为age是常量,在排序中被优化,索引未颠倒。

    Case6

    EXPLAIN select * from employees where name='LiLei' order by age asc, position desc;
    

    image.png
    分析:
    虽然排序字段列与索引顺序一样,但是order by 默认升序,position desc 降序,导致与索引的排序方式不同,产生Using filesort

    Case7

    EXPLAIN select * from employees where name in('LiLei','zhuge') order by age, position;
    

    image.png
    分析:
    对于排序来说,多个相等条件也是范围查询

    Case8

    EXPLAIN select * from employees where name > 'a' order by name;
    

    image.png

    EXPLAIN select name,age,position from employees where name > 'a' order by name;
    

    image.png
    分析:
    使用覆盖索引优化

    优化总结

  3. Mysql支持两种方式的排序filesort和index,Using index是指MySQL扫描索引本身完成排序。index效率高,filesort效率低

  4. order by 满足两种情况使用Using index
    1. order by语句使用索引最左前列
    2. 使用where字句与order by字句条件列组合满足索引最左前列。
  5. 尽量在索引列上完成排序,遵循索引建立时的最左前缀法则。
  6. 如果order by的条件不在索引列上,就会产生Using filesort
  7. 能用覆盖索引尽量用覆盖索引
  8. group by和order by很类似,其本质是先排序后分组。

    Using filesort文件排序原理详解

    filesort文件排序方式

  9. 单路排序:是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序;用trace工具可以看到sort_mode信息里显示 < sort_key,additional_fileds > 或者 <sort_key,packed_additional_fileds >

  10. 双路排序(又叫回表排序模式):是首先根据相应的条件取出相应的排序字段和可以直接定位行数据的行ID,然后在sort buffer中进行排序,排序完成后需要再次取回其它需要的字段;用trace工具可以看到sort_mode信息里显示<sort_key,rowid>

    怎么选择使用哪种排序方式

    Mysql通过比较系统变量max_length_for_sort_data(默认1024字节)的大小和需要查询的字段总大小来判断使用哪种排序模式
  • 如果字段的总长度小于上面该值,那么使用 单路排序模式。
  • 如果字段的总长度大于上面该值,那么使用 双路排序模式。

    示例验证

    EXPLAIN select * from employees where name = 'zhuge' order by position;
    
    image.png

    开启trace

    set session optimizer_trace="enabled=on";
    select * from information_schema.OPTIMIZER_TRACE;
    

    单路排序详细过程

  1. 从索引name找到第一个满足 name='zhuge 条件的主键id
  2. 根据主键id取出整行,取出所有字段的值,存入sort_buffer中
  3. 从索引name找到下一个满足 name='zhuge'条件的主键id
  4. 重复步骤2、3 直到不满足 name='zhuge'
  5. 对sort_buffer中的数据按照字段position进行排序
  6. 返回结果给客户端

    双路排序详细过程

  7. 从索引name找到第一个满足name='zhuge'的主键id

  8. 根据主键id取出整行,把排序字段position和主键id这两个字段放到sort buffer中
  9. 从索引name取下一个满足 name='zhuge' 记录的主键id
  10. 重复3、4 直到不满足 name='zhuge'
  11. 对sort_buffer中的字段position和主键id按照字段position进行排序
  12. 遍历排序好的id和字段position,按照id的值回原表中取出所有字段的值返回给客户端

    索引设计原则

  13. 代码先行,索引后上

  14. 联合索引尽量覆盖条件
    1. 可以设计1到3个联合索引,尽量包含sql语句里的where、order by、group by等字段,并且尽量满足最左前缀原则
  15. 不要在小基数字段上建立索引
  16. 长字符串我们可以采用前缀索引
    1. 比如varchar(255)这种大字段可能会比较占用磁盘空间,可以使用前20个字符建立索引
    2. 此时在where条件里搜索的时候,那么就会取该索引的前20个字符匹配的部分数据来对比
    3. 假如你要是order by、group by,那么就无法使用该索引了
  17. 可以使用监控后台检测一些慢SQL

    索引设计实战

    假设要设计一个用户搜索的场景

  18. 会根据省份、城市、性别来进行筛选

    1. 建立索引 index(province,city,sex)
    2. select xx from user where province = xx and city = xx and sex = xx;
  19. 假设这时候又要根据年龄来筛选
    1. 修改索引为index(province,city,sex,age)
    2. select xx from user where province = xx and city = xx and sex in ('male','female') and age > xx;

      分页查询优化

      Mysql执行计划

      可以使用explain+SQL语句来模拟优化器执行SQL查询语句,从而知道mysql是如何处理sql语句的。

Mysql执行计划变种

  1. explain extended:会在 explain 的基础上额外提供一些查询优化的信息。紧随其后通过 show warnings 命令可以得到优化后的查询语句。
    explain extended select * from;
    show warnings;
    

    Mysql执行计划中包含的信息

    | Column | Meaning | | —- | —- | | id | The SELECT
    identifier | | select_type | The SELECT
    type | | table | The table for the output row | | partitions | The matching partitions | | type | The join type | | possible_keys | The possible indexes to choose | | key | The index actually chosen | | key_len | The length of the chosen key | | ref | The columns compared to the index | | rows | Estimate of rows to be examined | | filtered | Percentage of rows filtered by table condition | | extra | Additional information |

id

select查询的序列号,包含一组数字,表示查询中执行select子句或者操作表的顺序

  • 如果id相同,那么执行顺序从上到下

    explain select * from emp e join dept d on e.deptno = d.deptno join salgrade sg on e.sal between sg.losal and sg.hisal;
    
  • 如果id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行

    explain select * from emp e where e.deptno in (select d.deptno from dept d where d.dname = 'SALES');
    
  • id相同和不同的,同时存在:相同的可以认为是一组,从上往下顺序执行,在所有组中,id值越大,优先级越高,越先执行。id为null,最后执行

    explain select * from emp e join dept d on e.deptno = d.deptno join salgrade sg on e.sal between sg.losal and sg.hisal where e.deptno in (select d.deptno from dept d where d.dname = 'SALES');
    

    select_type

    主要用来分辨查询的类型,是普通查询还是联合查询还是子查询

select_type
Value
Meaning
SIMPLE Simple SELECT (not using UNION or subqueries)
PRIMARY Outermost SELECT
UNION Second or later SELECT statement in a UNION
DEPENDENT UNION Second or later SELECT statement in a UNION, dependent on outer query
UNION RESULT Result of a UNION.
SUBQUERY First SELECT in subquery
DEPENDENT SUBQUERY First SELECT in subquery, dependent on outer query
DERIVED Derived table
UNCACHEABLE SUBQUERY A subquery for which the result cannot be cached and must be re-evaluated for each row of the outer query
UNCACHEABLE UNION The second or later select in a UNION that belongs to an uncacheable subquery (see UNCACHEABLE SUBQUERY)

simple:简单的查询,不包含子查询和union

explain select * from emp;

�primary:查询中若包含任何复杂的子查询,最外层查询则被标记为Primary

explain select staname,ename supname from (select ename staname,mgr from emp) t join emp on t.mgr=emp.empno ;

union:若第二个select出现在union之后,则被标记为union

explain select * from emp where deptno = 10 union select * from emp where sal >2000;

dependent union:跟union类似,此处的depentent表示union或union all联合而成的结果会受外部表影响

explain select * from emp e where e.empno  in ( select empno from emp where deptno = 10 union select empno from emp where sal >2000)

union result:从union表获取结果的select

explain select * from emp where deptno = 10 union select * from emp where sal >2000;

subquery:在select或者where列表中包含子查询

explain select * from emp where sal > (select avg(sal) from emp) ;

dependent subquery:subquery的子查询要受到外部表查询的影响

explain select * from emp e where e.deptno in (select distinct deptno from dept);

DERIVED: from子句中出现的子查询,也叫做派生类

explain select staname,ename supname from (select ename staname,mgr from emp) t join emp on t.mgr=emp.empno ;

UNCACHEABLE SUBQUERY:表示使用子查询的结果不能被缓存

explain select * from emp where empno = (select empno from emp where deptno=@@sort_buffer_size);

uncacheable union:表示union的查询结果不能被缓存:sql语句未验证

table

�对应行正在访问哪一个表,表名或者别名,可能是临时表或者union合并结果集

  • 如果是具体的表名,则表明从实际的物理表中获取数据,当然也可以是表的别名
  • 表名是derivedN的形式,表示使用了id为N的查询产生的衍生表
  • 当有union result的时候,表名是union n1,n2等的形式,n1,n2表示参与union的id

    type

    �type显示的是访问类型,访问类型表示我是以何种方式去访问我们的数据,最容易想的是全表扫描,直接暴力的遍历一张表去寻找需要的数据,效率非常低下,访问的类型有很多,效率从最好到最坏依次是:

null > system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
  • 一般情况下,得保证查询至少达到range级别,最好能达到ref

    null:mysql能够在优化阶段分解查询语句,如果在执行阶段不用访问表。例如在索引列中选择最小值,就不需要再访问表了。

    �all:全表扫描,一般情况下出现这样的sql语句而且数据量比较大的话那么就需要进行优化。

    explain select * from emp;
    

    index:全索引扫描这个比all的效率要好,主要有两种情况,一种是当前的查询时覆盖索引,即我们需要的数据在索引中就可以索取,或者是使用了索引进行排序,这样就避免数据的重排序

    explain  select empno from emp;
    

    range:表示利用索引查询的时候限制了范围,在指定范围内进行查询,这样避免了index的全索引扫描,适用的操作符: =, <>, >, >=, <, <=, IS NULL, BETWEEN, LIKE, or IN()

    explain select * from emp where empno between 7000 and 7500;
    

    index_subquery:利用索引来关联子查询,不再扫描全表

    explain select * from emp where emp.job in (select job from t_job);
    

    unique_subquery: 该连接类型类似与index_subquery,使用的是唯一索引

    explain select * from emp e where e.deptno in (select distinct deptno from dept);
    

    index_merge:在查询过程中需要多个索引组合使用,没有模拟出来

    ref_or_null:对于某个字段即需要关联条件,也需要null值的情况下,查询优化器会选择这种访问方式

    explain select * from emp e where  e.mgr is null or e.mgr=7369;
    

    ref:使用了非唯一性索引进行数据的查找

    create index idx_3 on emp(deptno);
    explain select * from emp e,dept d where e.deptno =d.deptno;
    

    eq_ref :使用唯一性索引进行数据查找

    explain select * from emp,emp2 where emp.empno = emp2.empno;
    

    const:这个表至多有一个匹配行,

    explain select * from emp where empno = 7369;
    

    system:表只有一行记录(等于系统表),这是const类型的特例,平时不会出现

    possible_keys

    �显示可能应用在这张表中的索引,一个或多个,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。

  1. 如果显示null,可能是表中的数据不多,mysql认为索引没有意义,走的全表扫描。

    key

  • �实际使用的索引,如果为null,则没有使用索引,查询中若使用了覆盖索引,则该索引和查询的select字段重叠。
  • 可以使用 force index 或者ignore index来强制使用/忽略索引

    �key_len

  • �表示索引中使用的字节数,可以通过key_len计算查询中使用的索引长度,在不损失精度的情况下长度越短越好。

    key_len的计算规则

  1. 字符串,char(n)和varchar(n),5.0.3以后版本中,n均代表字符数,而不是字节数,如果是utf-8,一个数字或字母占1个字节,一个汉字占3个字节
    1. char(n):如果存汉字长度就是 3n 字节
    2. varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,因为 varchar是变长字符串
  2. 数值类型
    1. tinyint:1字节
    2. smallint:2字节
    3. int:4字节
    4. bigint:8字节
  3. 时间类型
    1. date:3字节
    2. timestamp:4字节
    3. datetime:8字节
  4. 如果字段允许为 NULL,需要1字节记录是否为 NULL

索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索 引。

ref

  • �显示索引的哪一列被使用了,如果可能的话,是一个常数

    extra

    ```sql —using filesort:说明mysql无法利用索引进行排序,只能利用排序算法进行排序,会消耗额外的位置 explain select * from emp order by sal;

—using temporary:建立临时表来保存中间结果,查询完成之后把临时表删除 explain select ename,count(*) from emp where deptno = 10 group by ename;

—using index:这个表示当前的查询时覆盖索引的,直接从索引中读取数据,而不用访问数据表。如果同时出现using where 表名索引被用来执行索引键值的查找,如果没有,表面索引被用来读取数据,而不是真的查找 explain select deptno,count(*) from emp group by deptno limit 10;

—using index condition:查询的列不完全被索引覆盖

—using where:使用where进行条件过滤 explain select * from t_user where id = 1;

—using join buffer:使用连接缓存,情况没有模拟出来

—impossible where:where语句的结果总是false explain select * from emp where empno = 7469;

<a name="pYq7U"></a>
# Mysql的数据结构
<a name="lBuoh"></a>
## 数据结构选择
<a name="jm7XO"></a>
### Hash表
> 键 - 值(key-value)存储数据的结构,我们只要输入待查找的键即 key,就可以找到其对应的值即 Value。
> 哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。

<a name="bq4xZ"></a>
#### 缺点

- 利用hash存储的话需要将所有的数据文件添加到内存,比较耗费内存空间。
- 如果所有的查询都是等值查询,那么hash确实很快,但是在企业或者实际工作环境中范围查找的数据更多,而不是等值查询,因此hash就不太适合了。
<a name="WYNSG"></a>
### 二叉树和红黑树
<a name="WozEo"></a>
#### 缺点

- 无论是二叉树还是红黑树,都会因为树的深度过深而造成io次数变多,影响数据读取的效率。
<a name="KImRM"></a>
### B树
<a name="SUf69"></a>
#### 图解
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25955514/1645515215167-0589a00d-680e-403b-9d7c-a265e67c768c.png#clientId=u50b9a9a3-22c4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=429&id=u52e91d50&margin=%5Bobject%20Object%5D&name=image.png&originHeight=858&originWidth=2166&originalType=binary&ratio=1&rotation=0&showTitle=false&size=227276&status=done&style=none&taskId=u343e5892-66dc-436b-ac39-3b55504a875&title=&width=1083)

- 每个节点占用一个磁盘块,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为 16 和 34,P1 指针指向的子树的数据范围为小于 16,P2 指针指向的子树的数据范围为 16~34,P3 指针指向的子树的数据范围为大于 34。 
- 查找关键字过程:
   - 根据根节点找到磁盘块 1,读入内存。【磁盘 I/O 操作第 1 次】。
   - 比较关键字 28 在区间(16,34),找到磁盘块 1 的指针 P2。
   - 根据 P2 指针找到磁盘块 3,读入内存。【磁盘 I/O 操作第 2 次】。
   - 比较关键字 28 在区间(25,31),找到磁盘块 3 的指针 P2。
   - 根据 P2 指针找到磁盘块 8,读入内存。【磁盘 I/O 操作第 3 次】。
   - 在磁盘块 8 中的关键字列表中找到关键字 28。 
<a name="YDKUS"></a>
#### 缺点

- 每个节点都有key,同时也包含data,而每个页存储空间是有限的,如果data比较大的话会导致每个节点存储的key数量变小。
- 当存储的数据量很大的时候会导致深度较大,增大查询时磁盘io次数,进而影响查询性能。
<a name="nGudZ"></a>
### B+树(Mysql使用的数据结构)
> B+Tree是在BTree的基础之上做的一种优化,变化如下:
> 1. B+Tree每个节点可以包含更多的节点,这么做的原因有两个,第一个原因是为了降低树的高度,第二个原因是将数据范围变为多个区间,区间越多,数据检索越快。
> 1. 非叶子节点存储key,叶子节点存储key和数据。
> 1. 叶子节点两两指针相互连接(符合磁盘的预读特性),顺序查询性能更高。

<a name="m0MJu"></a>
#### 图解
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25955514/1645515833820-3d84a91d-cdf4-4c03-875e-c3bf81d6a146.png#clientId=u50b9a9a3-22c4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=482&id=uf857da42&margin=%5Bobject%20Object%5D&name=image.png&originHeight=964&originWidth=2554&originalType=binary&ratio=1&rotation=0&showTitle=false&size=262300&status=done&style=none&taskId=ud4a87e29-9f20-4e11-9bad-34b139163a8&title=&width=1277)
> 注意:在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对 B+Tree 进行两种查找运算:**_一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。_**

<a name="IyoqZ"></a>
#### 缺点

- 页分裂和页合并
   - 如磁盘块5的数据满了,此时需要插入一条数据进来,就会导致页分裂。
   - 如果某个磁盘块上的数据全部被删除了,就会导致页合并。
> **_自增主键防止页分裂,逻辑删除并非物理删除防止页合并_**

<a name="s4Lqk"></a>
### B+树(Innodb)
<a name="AGEpd"></a>
#### 图解
<a name="VWh0b"></a>
### ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25955514/1645516016429-53e96e93-714a-4600-8141-76c78c4588d6.png#clientId=u50b9a9a3-22c4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=369&id=u71d96f7d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=738&originWidth=2212&originalType=binary&ratio=1&rotation=0&showTitle=false&size=178836&status=done&style=none&taskId=ua40cb34c-3463-4726-888b-9a793f6a3ec&title=&width=1106)
> 注意:
> 1. InnoDB是通过B+Tree结构对**_主键_**创建索引,然后叶子节点中存储记录,如果没有主键,那么会选择**_唯一键_**,如果没有唯一键,那么会生成一个**_6位的row_id来作为主键。_**
> 1. 如果创建索引的键是其他字段,那么在叶子节点中存储的是该记录的主键,然后再通过主键索引找到对应的记录,叫做回表。

<a name="ymOXw"></a>
# Mysql的日志结构
<a name="wdgTU"></a>
## Mysql的架构
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25955514/1645516512531-bcdc96b8-6fe2-4f23-bcb2-12d02bc7da85.png#clientId=u50b9a9a3-22c4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=404&id=c6QaQ&margin=%5Bobject%20Object%5D&name=image.png&originHeight=808&originWidth=1078&originalType=binary&ratio=1&rotation=0&showTitle=false&size=538533&status=done&style=none&taskId=u27599352-ac10-4992-9a66-fd871bac614&title=&width=539)
<a name="LGtgA"></a>
## Redo Log
> 存储引擎层:只有Innodb才有Redo Log日志文件

- InnoDB 引擎就会先把记录写到 redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面。
> InnoDB引擎先把记录写到redo log 中,redo log 在哪,他也是在磁盘上,这也是一个写磁盘的过程,但是与更新过程不一样的是,更新过程是在磁盘上随机IO,费时。 而写redo log 是在磁盘上顺序IO。效率要高。

<a name="l7RTA"></a>
### Redo Log的大小

- Innodb的redo log是固定大小的,采用循环写的方式记录日志
- ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25955514/1645516734469-a1751166-8756-4591-a93a-3c19c9dcdb18.png#clientId=u50b9a9a3-22c4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=306&id=ue75823f0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=612&originWidth=638&originalType=binary&ratio=1&rotation=0&showTitle=false&size=224597&status=done&style=none&taskId=u9107a3e6-6d11-4a5c-91e8-82b085535dd&title=&width=319)
- write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
<a name="fXTKw"></a>
### Crash Safe能力

- 有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失。
<a name="rJE1D"></a>
## Bin Log
> Server层:

- Binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
<a name="YdON2"></a>
### Bin Log归档
binlog是Server层实现的二进制日志,他会记录我们的cud操作。Binlog有以下几个特点:

1. Binlog在MySQL的Server层实现(引擎共用) 
1. Binlog为逻辑日志,记录的是一条语句的原始逻辑 
1. Binlog不限大小,追加写入,不会覆盖以前的日志

如果,我们误删了数据库,可以使用binlog进行归档! 要使用binlog归档,首先我们得记录binlog,因此需要先开启MySQL的 binlog功能。<br />**配置my.cnf**
```shell
# 配置开启binlog
log-bin=/mysql-bin
#5.7以及更高的版本需要配置:server_id=1(自定义,保证唯一性)
#binlog格式,有3种statement、row、mixed
binlog_format=ROW
#表示每次执行写入就与硬盘同步,会影响性能,为0时表示事务提交时mysql不做刷盘操作,由操作系统决定
sync_binlog=1
#配置定期清理
expire_logs_days=5

binlog命令

show variables like '%log_bin%'; # 查看bin-log是否开启
flush logs; #会多一个最新的bin-log日志
show master status; #查看最后一个bin-log日志的相关信息
reset master; #清空所有的bin-log日志

查看binlog内容

/home/mysql/data/mysql-bin ‐‐no‐defaults /home/mysql/data/mysql-bin.000002

binlog里的内容不具备可读性,所以需要我们自己去判断恢复的逻辑点位,怎么观察呢?
看重点信息,比如begin,commit这种关键词信息,只要在binlog当中看到了,你就可以理解为begin-commit之间的信息是一个完整的事务逻辑,然后再根据位置 position判断恢复即可。binlog内容如下:
image.png
归档操作
image.png

Undo Log

  • 用作MVCC回滚事务使用。

    Redo Log和Bin Log的区别

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

    两阶段提交

    image.png

    Mysql刷脏页

    测试磁盘随机读写的能力

    fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
    

    �Innodb脏页的比例

    select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';
    select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';
    select @a/@b;