一条sql语句是怎么执行的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
1.客户端 --> 2.连接器(管理连接,权限校验)--> 3.分析器(词法分析,语法分析)-->4.优化器(执行计划生成,索引选择)-->5.执行器(操作引擎,返回结果)-->6.存储引擎(存储数据,提供读写接口)

2~5 是server层,大部分核心功能在此
6是存储引擎层,负责数据的存储和提取
不同的存储引擎共用一个server

连接器:mysql -h host -P port -u user -p
此步会进行权限的校验,成功则会从数据库中读取该用户支持的权限,不成功则直接会提示access denied for user

分析器:词法分析,识别各种关键字,根据词法分析的结果,进行语法分析,如出错,一般会收到“you have an error in your SQL syntax”

优化器:决定使用哪个索引,关联查询的时候表连接顺序等

执行器:执行语句,再次判断权限,但是对表的操作权限,有权限,则根据表的引擎调用对应的数据引擎的接口

select notExistField from t;
查询一个字段不存在的语句,是发生在分析器这个步骤中。

binlog与redolog的写入流程

redo log(重做日志)

1
2
3
4
5
redo log是innodb特有的日志。当一条记录更新时,先写将其写到redo log,并更新内存。innodb引擎会在适当的时候,将这个操作记录更新到磁盘中,往往是在系统比较空闲的时候

redo log的大小是固定的,可以配置为一组合4个文件,每个文件大小1G,总共可记录4G,当写满时,会循环到开头重新写

有了redo log innodb可以保证在数据库异常重启时,之前提交的记录也不会丢失,这个能力称为crash-safe

bin log(归档日志)

1
bin log属于mysql server层自带的日志,与存储引擎无关。binlog日志只能用于归档,并不带有crash-safe能力

两者区别

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

为什么 redo log 具有 crash-safe 的能力,而 binlog 没有?

redo log 是什么?

一个固定大小,“循环写”的日志文件,记录的是物理日志——“在某个数据页上做了某个修改”。

binlog 是什么?

一个无限大小,“追加写”的日志文件,记录的是逻辑日志——“给 ID=2 这一行的 c 字段加1”。

redo log 和 binlog 有一个很大的区别就是,一个是循环写,一个是追加写。也就是说 redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志,保存的是全量的日志。

当数据库 crash 后,想要恢复未刷盘但已经写入 redo log 和 binlog 的数据到内存时,binlog 是无法恢复的。虽然 binlog 拥有全量的日志,但没有一个标志让 innoDB判断哪些数据已经刷盘,哪些数据还没有。

举个栗子,binlog 记录了两条日志:

给 ID=2 这一行的 c 字段加1 给 ID=2 这一行的 c 字段加1 在记录1刷盘后,记录2未刷盘时,数据库 crash。重启后,只通过 binlog 数据库无法判断这两条记录哪条已经写入磁盘,哪条没有写入磁盘,不管是两条都恢复至内存,还是都不恢复,对 ID=2 这行数据来说,都不对。

但 redo log 不一样,只要刷入磁盘的数据,都会从 redo log 中抹掉,数据库重启后,直接把 redo log 中的数据都恢复至内存就可以了。这就是为什么 redo log 具有 crash-safe 的能力,而 binlog 不具备。

当数据库 crash 后,如何恢复未刷盘的数据到内存中? 根据 redo log 和 binlog 的两阶段提交,未持久化的数据分为几种情况:

change buffer 写入,redo log 虽然做了 fsync 但未 commit,binlog 未 fsync 到磁盘,这部分数据丢失。 change buffer 写入,redo log fsync 未 commit,binlog 已经 fsync 到磁盘,先从 binlog 恢复 redo log,再从 redo log 恢复 change buffer。 change buffer 写入,redo log 和 binlog 都已经 fsync,直接从 redo log 里恢复。 ———————————————— 版权声明:本文为CSDN博主「Zzz-_-ch」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_43698257/article/details/110676515

两阶段提交

1
2
3
4
5
6
7
因为innodb有两种日志,为了使两种日志数据的逻辑一致性,采用的一种策略

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

小问题及回答

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. redo log的概念是什么? 为什么会存在.
  2. 什么是WAL(write-ahead log)机制, 好处是什么.
  3. redo log 为什么可以保证crash safe机制.
  4. binlog的概念是什么, 起到什么作用, 可以做crash safe吗?
  5. binlog和redolog的不同点有哪些?
  6. 物理一致性和逻辑一直性各应该怎么理解?
  7. 执行器和innoDB在执行update语句时候的流程是什么样的?
  8. 如果数据库误操作, 如何执行数据恢复?
  9. 什么是两阶段提交, 为什么需要两阶段提交, 两阶段提交怎么保证数据库中两份日志间的逻辑一致性(什么叫逻辑一致性)?
 10. 如果不是两阶段提交, 先写redo log和先写bin log两种情况各会遇到什么问题?
 
 1. redo log是重做日志。主要用于MySQL异常重启后的一种数据恢复手段,确保了数据的一致性。归根到底是MySQL为了实现WAL机制的一种手段。因为MySQL进行更新操作,为了能够快速响应,所以采用了异步写回磁盘的技术,写入内存后就返回。但是会存在crash后内存数据丢失的隐患,而redo log具备crash safe能力。
2. WAL机制是写前日志,也就是MySQL更新操作后在真正把数据写入到磁盘前先记录日志。好处是不用每一次操作都实时把数据写盘,就算crash后也可以通过redo log重放恢复,所以能够实现快速响应SQL语句。
3. 因为redo log是每次更新操作完成后,就一定会写入的,如果写入失败,这说明此次操作失败,事务也不可能提交。redo log内部结构是基于页的,记录了这个页的字段值变化,只要crash后读取redo log进行重放就可以恢复数据。(因为redo log是循环写的,如果满了InnoDB就会执行真正写盘)
4. bin log是归档日志,属于MySQL Server层的日志。可以起到全量备份的作用。当需要恢复数据时,可以取出某个时间范围内的bin log进行重放恢复。但是bin log不可以做crash safe,因为crash之前,bin log可能没有写入完全MySQL就挂了。所以需要配合redo log才可以进行crash safe。
5. bin log是Server层,追加写,不会覆盖,记录了逻辑变化,是逻辑日志。redo log是存储引擎层,是InnoDB特有的。循环写,满了就覆盖从头写,记录的是基于页的物理变化,是物理日志,具备crash safe操作。
6. 前者是数据的一致性,后者是行为一致性。(不清楚)
7. 执行器在优化器选择了索引后,调用InnoDB读接口,读取要更新的行到内存中,执行SQL操作后,更新到内存,然后写redo log,写bin log,此时即为完成。后续InnoDB会在合适的时候把此次操作的结果写回到磁盘。
8. 数据库在某一天误操作,就可以找到距离误操作最近的时间节点前的bin log,重放到临时数据库里,然后选择当天误删的数据恢复到线上数据库。
9. 两阶段提交就是对于三步操作而言:1.prepare阶段 2. 写入bin log 3. commit
redo log在写入后,进入prepare状态,然后bin log写入后,进入commit状态,事务可以提交。
如果不用两阶段提交的话,可能会出现bin log写入之前,机器crash导致重启后redo log继续重放crash之前的操作,而当bin log后续需要作为备份恢复时,会出现数据不一致的情况。所以需要对redo log进行回滚。
如果是bin log commit之前crash,那么重启后,发现redo log是prepare状态且bin log完整(bin log写入成功后,redo log会有bin log的标记),就会自动commit,让存储引擎提交事务。
10.先写redo log,crash后bin log备份恢复时少了一次更新,与当前数据不一致。先写bin log,crash后,由于redo log没写入,事务无效,所以后续bin log备份恢复时,数据不一致。

事务

mysql的事务指的是满足 ACID 特性的一组操作,ACID分别是指原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)。

原子性是指事务是不可分隔的最小单元,事务中的所有操作要么全部提交成功,要么全部失败回滚 一致性是指事务执行前后保持一致状态。举个例子。就是A和B的钱是1000元,A给你100元,无论最后双方转了多少次,总的钱一定是1000元。 隔离性是指一个事务所做的修改如果未提交,则对其他事务不可见 持久性是指一旦事务提交,其修改将永久保存至数据库

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念

事务隔离级别

  • 读未提交(READ UNCOMMINTED)
1
一个事务还未提交,它做的变更可别的事务看到
  • 读提交 (READ COMMITTED)
1
一个事务提交之后,它做的变更才会被其他事务看到
  • 可重复读(REPEATABLE READ)
1
2
一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是
一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的
  • 可串行化(SERIALIZABLE):强制事务串行执行。
事务1 事务2
启动 启动
查询值得到1
查询值得到1,将1改成2
查询值得到v1
提交
查询值得到v2
提交
查询得到v3
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
隔离级别未提交读:
v1: 2  v2:2 v3:2

隔离级别提交读
v1: 1  v2: 2 v3:2

隔离级别可重复读
v1: 1  v2: 1 v3: 2

隔离级别串行化
v1: 1 v2:1   v3: 2
  • 不同隔离级别实现上的区别
1
2
3
4
5
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准
可重复读: 这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
读提交: 这个视图是在每个SQL语句开始执行的时候创建的。
读未提交: 直接返回记录上的最新值,没有视图概念;
串行化: 用加锁的方式来避免并行访问
  • 不同隔离级别下会产生的问题
1
2
3
4
5
6
7
脏读:财务发工资15000,未读交,正好你此时查看银行卡,发现余额多了15000,但财务最终将工资修改为10000,然后正式提交。此时就产生了脏读,实际到账只有10000. 读未提交会产生该问题

不可重复读:是指在一个事务内,多次读同一数据。准备出去happy,从银行卡取钱10000,查看余额12000,正准备取钱的时候,你女朋友消费5000,此时你从银行取出来,发现余额不足。读提交会产生该问题

幻读:查看流水账单时,发现这个月才消费500,此时女朋友在另一个地方消费了1000,你再打印流水时,发现多了1000这条记录

不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

undo log

1
2
3
4
5
6
7
8
9
作用:1.提供回滚 2.多个行版本控制(MVCC)

在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。

当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。

undo log也会产生redo log,因为undo log也要实现持久性保护。

ACID靠什么保证

1
2
3
4
5
6
7
A 原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql

C一致性一般由代码层面来保证

I隔离性由MVCC来保证

D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复

mvcc

MVCC(Multi-Version Concurrency Control)即多版本并发控制主要是为了解决数据库中并发事务读写的一致性问题.MVCC在MySQL innodb中的实现主要是为了提高数据库并发性能,用更好的方式去处理读写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。

当前读
1
像开启事务后执行,select * from tbl lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。简单的说,A开启了事务,执行了上面说的SQL, 其他事务B 需要在A事务没有执行完成前,一直等待,直到A事务执行完成。
快照读
1
开始事务后简单的select操作就是快照读(读之前搞个快照数据),即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;快照读是为了提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
当前读,快照读,mvcc的关系
1
MVCC多版本并发控制指的是维持一个数据的多个版本,使得读写操作没有冲突,快照读是MySQL为实现MVCC的一个非阻塞读功能。MVCC模块在MySQL中的具体实现是由三个隐式字段,undo日志、read view三个组件来实现的。
mvcc的实现

依赖于记录中的三个, (隐藏字段,undolog,read view)组件。

  • 隐藏字段
1
2
3
4
5
InnoDB引擎会在每行记录除了我们自定义的字段外,还会添加数据库隐式的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

DB_TRX_ID: 6字节,最近修改事务id,记录创建这条记录或者最后一次修改该记录的事务id。
DB_ROLL_PTR: 7字节,回滚指针,指向这条记录的上一个版本,用于配合undolog,指向上一个旧版本。
DB_ROW_ID:  6字节,隐藏的主键,如果数据表没有主键,那么innodb会自动生成一个6字节的row_id。
  • undo log
1
undolog被称之为回滚日志,表示在进行insert,delete,update操作的时候产生的方便回滚的日志。当进行insert操作的时候,产生的undolog只在事务回滚的时候需要,并且在事务提交之后可以被立刻丢弃。当进行update和delete操作的时候,产生的undolog不仅仅在事务回滚的时候需要,在快照读的时候也需要,不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。
  • read view
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
read view 是 InnDB 在实现 MVCC 时用到的一致性读视图,用于支持 RC(读提交)以及 RR(可重复读)隔离级别的实现。

read view 不是真实存在的,只是一个概念,undolog才是它的体现。它主要是通过版本和 undolog 计算出来的。作用是决定事务能看到哪些数据。

每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。

1. 版本未提交,不可见;
2. 版本已提交,但是是在视图创建后提交的,不可见;
3. 版本已提交,而且是在视图创建前提交的,可见。

@todo 需要看更多的文章理解

索引

作用

1
2
3
1.索引大大减少服务器需要扫描的数据量
2.索引可以帮助服务器避免排序和临时表
3.索引可以将随机I/O变为顺序I/O

索引的类型

主键索引
1
主键索引的数据保存在叶子结点上,在InnoDB里,主键索引也被称为聚簇索引(clustered index)
非主键索引
1
非主键索引的叶子节点内容是主键的值。在InnoDB里,非主键索引也被称为二级索引(secondary index)
主键索引与普通索引区别
1
主键索引查找数据不需要回表,而非主键索引如果是非覆盖索引的话,则需要回表。非主键索引的查询需要多扫描一棵索引树
惟一索引
  • 普通索引与惟一索引的区别
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
首先要确保业务能保证插入的数据惟一,不然的话,对于有惟一性要求的,只能选择惟一索引。

在业务能保证数据惟一的情况下,该选择普通索引还是惟一索引?

普通索引:
1. 根据索引找到第一条满足查询条件的记录
2. 找到下一行记录
3. 判断下一行是否满足查询条件
4. 满足重复步骤2,不满足返回满足条件的结果集

惟一索引:
1. 根据索引找到第一条满足查询条件的记录
2. 返回该行记录

在等值查询的时候,惟一索引更有优势,查找到即直接返回,不需要再查找下一个数据。InnoDB中,数据是按页为单位来读写,读取一行记录会将包含该行的整个页读到内存,因此这个差异只是一次指针寻找和一次比较判断,对性能的影响微乎其微。


唯一索引:将数据页读入内存,判断到没有冲突,更新这个值,语句执行结束
普通索引:将更新记录在 change buffer,语句执行就结束了。

将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一。change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

因些对于更新来说,普通索引的更新会更快。

因为惟一索引是需要做惟一性判断的,所以惟一索引是无法使用change buffer的,在插入数据的时候或者更新数据的时候
@todo 这里牵扯到change buffer的概念,还需要再了解了解

change buffer相关解读:
在MySQL5.5之前,叫插入缓冲(insert buffer),只针对insert做了优化;现在对delete和update也有效,叫做写缓冲(change buffer)。

 它是一种应用在非唯一普通索引页(non-unique secondary index page)不在缓冲池中,对页进行了写操作,并不会立刻将磁盘页加载到缓冲池,而仅仅记录缓冲变更(buffer changes),等未来数据被读取时,再将数据合并(merge)恢复到缓冲池中的技术。写缓冲的目的是降低写操作的磁盘IO,提升数据库性能。
https://juejin.cn/post/6844903875271475213

有时间得了解下索引索引实现的原理
  • 主键索引与惟一索引的区别
1
2
主键索引是聚簇索引,叶子节点上会存储数据。
而惟一索引实际上还只是普通索引,是二级索引,叶子结点上存储的是主键的值,只是加了一层惟一性约束而已
覆盖索引
1
覆盖索引可以减少树的搜索次数(不用回表,因为当前的索引树上的数据已经满足要求),显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段.
联合索引
1
2
3
联合索引:由于数据的查询条件可能很多,但又不能为每个单独的字段建立索引,联合索引的出现能够减少索引的产生,同时也能带来性能上的提升。

只要满足最左前缀,就可以利用索引来加速检索。比如index(name,age)与index(name),我们就可以直接使用前者来代替后者,来减少索引。

索引下推

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
当建立联合索引时,如果使用不上全部的索引,在mysql5.6后以采用索引下推的方式来提高查找效率

建立联合索引uk(name,age)
eg: select * from user where name like "赵%" and age = 20

此时,在mysql5.6之前如下方式
先匹配索引上的name值,如果是赵*,则取出对应的id去主键索引树查找数据匹配age的值,如果匹配上,下一条,如果匹配不上,放弃

索引下推方式:
匹配索引上的name值,如果是赵*,继续判断该索引的age是否是20,不是20,则直接放弃

可以发现,索引下推可以有效减少回表次数,从而达到效率上的提升

字符串索引

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
有些字符串因为长度很长,如果加了索引,导致索引的key值就很长,很占空间,这时候可以使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。

定义前缀索引,怎么找到合适的长度呢?

可以计算区分度
select count(distinct left(name,n))/count(distinct name) from user;
调整n,使n增长到一定值,如果该值提升对区分度提升微小的情况下,则选择。

总结:
1. 直接创建完整索引,这样可能比较占用空间;
2. 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;
3. 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题;
4. 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描。

索引维护

索引数据结构

为什么采用B+树做索引
  • 为什么不采用哈希作索引
1
哈希作索引,增加,更新,删除,查找都能做到O(1)的平均时间复杂度,但是查找只针对单条记录查找,对于范围查找,复杂度则为O(n)
  • 为什么不采用AVL 树(平衡二叉树)和红黑树(二叉查找树)作索引
1
2
3
4
5
6
对于二叉树来说,增加,查找,更新,删除的复杂度都能做到O(logn),和B+树,B树是同一个量级的,但为什么不采用,这跟读取磁盘IO有关系,索引文件会存放在磁盘上,由于节点并不是顺序存放,因此对于每次取下一个节点来说,都可能会去磁盘读取一次数据,而树的高度越高,代表去磁盘读取的次数越多。而与磁盘io的交互相对于内存里的操作来说是很费时的.

100 万节点的平衡二叉树,树高 20。一次查询可能需要访问 20 个数
据块。在机械硬盘时代,从磁盘随机读一个数据块需要10ms左右的寻址时间。也就是说,对于一个100万行的表,如果使用二叉树来存储,单独访问一个行可能需要20个10ms 的时间,这个查询可真够慢的。

红黑树的实现是基于二叉树的结构,这就造成了逻辑上很近的节点(父子)物理上可能很远,无法利用空间局部性的原理,像楼主说的每次加载好几层,但是这些数据在物理上是分散的,相当于是随机IO,速度会非常慢。而在B+树的实现中,每个节点存储了多个关键字,这些关键字往往是以数组的形式存储的,数组一般在物理上是连续的,所以B+树的同一个节点的数据可以存储在同一个页中,每次IO操作只需要读取该页即可,效率很高。
  • 为什么不采用B树
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
B树的特点
1、m叉树,树的高度低,能有效减少磁盘io读取
2、叶子结点,非叶子结点都存放数据
3、中序遍历,可以获得所有节点

B树被作为实现索引的数据结构被创造出来,是因为它能够完美的利用“局部性原理”。

什么是局部性原理?
局部性原理的逻辑是这样的:
(1)内存读写块,磁盘读写慢,而且慢很多;
(2)磁盘预读:磁盘读写并不是按需读取,而是按页预读,一次会读一页的数据,每次加载更多的数据,如果未来要读取的数据就在这一页中,可以避免未来的磁盘IO,提高效率;通常,一页数据是4K。
(3)局部性原理:软件设计要尽量遵循“数据读取集中”与“使用到一个数据,大概率会使用其附近的数据”,这样磁盘预读能充分提高磁盘IO;

因此B树是很适合做索引的,但B+树在改进了B树,使得更适合作数据库的索引
  • 为什么采用B+树
1
2
3
4
5
6
7
8
相对于B树,B+树改进了以下几点
1、只在叶子结点存储数据,中间结点只存取索引数据,B+树中根到每一个节点的路径长度一样,而B树不是这样;
2、叶子之间,增加了链表,获取所有节点,不再需要中序遍历;

改进后,以下方面能得到明显的改善
(1)范围查找,定位min与max之后,中间叶子节点,就是结果集,不用中序回溯;范围查询在SQL中用得很多,这是B+树比B树最大的优势。
(2)叶子节点存储实际记录行,记录行相对比较紧密的存储,适合大数据量磁盘存储;非叶子节点存储记录的PK,用于查询加速,适合内存存储;
(3)非叶子节点,不存储实际记录,而只存储记录的KEY的话,那么在相同内存的情况下,B+树能够存储更多索引;
  • 总结
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
数据库索引用于加速查询
虽然哈希索引是O(1),树索引是O(log(n)),但SQL有很多“有序”需求,故数据库使用树型索引

InnoDB不支持哈希索引
数据预读的思路是:磁盘读写并不是按需读取,而是按页预读,一次会读一页的数据,每次加载更多的数据,以便未来减少磁盘IO

局部性原理:软件设计要尽量遵循“数据读取集中”与“使用到一个数据,大概率会使用其附近的数据”,这样磁盘预读能充分提高磁盘IO

数据库的索引最常用B+树:
(1)很适合磁盘存储,能够充分利用局部性原理,磁盘预读;
(2)很低的树高度,能够存储大量数据;
(3)索引本身占用的内存很小;
(4)能够很好的支持单点查询,范围查询,有序性查询;

全局锁(不重要,可忽略)

1
2
3
全局锁,用于给整个数据库实例加锁
命令: Flush tables with read lock (FTWRL)
使用场景:全库逻辑备份

表级锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
命令: lock tables … read/write
正常情况下,myisam引擎才会使用该命令,innodb不会使用该命令,因为支持粒度更小的行级锁
-----------------------
MDL(metadata lock):元数据表锁
使用场景:在给表加字段的时候

该表锁不需要显式调用,当对一个表做增删改查操作的时候,会自动加MDL读锁;当要对表做结构变更操作的时候,会加MDL写锁。

对读写非常频繁的表加字段需要注意:可能会因为长事务(增删改查)导致MDL读锁不能及时释放,MDL写锁又阻塞,导致后面的查询阻塞。所以最好在 alter table语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。

可使用以下命令:
ALTER TABLE tbl_name NOWAIT add column ...

行级锁

1
2
3
在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻
释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
因此如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
死锁
事务1 事务2
begin begin
update set a = a + 1 where id = 1
update set a = a + 2 where id = 2
update set a = a + 2 where id = 2
update set a = a + 1 where id = 1
commit
commit
1
2
3
4
5
6
7
8
以上两个事务开启后,互相锁住对应所在的行,但都只有等到commit之后才能释放,就导致了死锁

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

由于第一种策略不太好把握超时时间,因此大多数情况下采用第二种策略

间隙锁

在可重复读的隔离级别下,可能会存在幻读的问题,即使把所有的记录都加上锁,还是阻止不了新插入的记录,产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁 (Gap Lock)。

如果用 select * from t for update 要把整个表所有记录锁起来,就形 成了 7 个 next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、 (25, +supremum]。

间隙锁:锁的就是两个值之间的空隙。

间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。

1
2
3
4
5
原则 1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 是前开后闭区间。
原则 2:查找过程中访问到的对象才会加锁。
优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。

count(*)

MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count() 的时候会直接返回这个数,效率很高;仅限无筛选条件执行 而 InnoDB 引擎就麻烦了,它执行count()的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加。最后返回累计值。

所以,count(*)、count(主键)count(id)和count(1)都表示返回满足条件的结果集的总行数;而count(字段),则表示返回满足条件的数据行里面,参数“字段”不为 NULL 的总个数。

count(主键 id): InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server层。server层拿到id后,判断是不可能为空的,就按行累加。

count(1): InnoDB 引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。

count(1) 执行得要比 count(主键 id) 快。因为从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作

count(字段): 
1. 如果这个“字段”是定义为 not null的话,一行行地从记录里面读出这个字段,判断不
能为 null,按行累加;
2. 如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。

count(*):不会把全部字段取出来,而是专门做了优化,不取值。count(*)
肯定不是 null,按行累加。

按照效率排序的话,count(字段)<count(主键 id)<count(1)≈count(*),所
以我建议你,尽量使用 count(*)。

mysql 主从复制流程

1
2
3
主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中;
从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进 自己的relay log中;
从:sql执行线程——执行relay log中的语句;

mysql幂等性处理方案

  1. 悲观锁,锁记录,查询时使用for update
  2. 乐观锁,类似加版本号 先查询出记录,然后再update where version = …
  3. redis,借助外部锁,比如redis实现的锁
  4. 建张流水表,惟一主键记录