MySQL

存储引擎架构:这种架构的设计将查询处理及其他系统任务和数据的存储/提取相分离。

1.1 MySQL逻辑架构

MySQL_第1张图片
第一层:不是MySql所独有,比如连接处理,授权认证,安全等。
第二层:查询解析,分析,优化,缓存以及所有的内置函数,所有的跨存储引擎的功能都在这一层实现:存储过程,触发器,视图等。
第三层:存储引擎,存储引擎负责MySql中数据的存储和提取。

1.1.1连接管理与安全性
每个客户端连接都会子啊服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个CPU核心或者CPU中运行。服务器会负责缓存线程,因此不需要为每一个新建的连接创建或者销毁线程。
(ps:不知道是不是维护一个线程池,客户端来了即给予连接,用完放回线程池,减少了创建和释放线程的消耗)
1.1.2 优化与执行
MySQL会解析查询,并创建内部数据结构**(解析树)**,然后对其进行各种优化,包括重写查询,决定表的读取顺序,以及选择合适的索引等。
优化器并不关心表使用的是什么存储引擎,但存储引擎对于优化查询是有影响的。
对于SELECT语句,在解析查询之前,服务器会先检查查询缓存(Query Cache),如果能找到对应查询,直接返回结果集。

1.2 并发控制

只要有多个查询需要在同一时刻修改数据,都会产生并发控制的问题。

1.2.1读写锁
在处理并发读或者写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题,通常被称为共享锁(读锁)和排他锁(写锁)。
让锁定对象更有选择性。尽量只锁定需要修改的部分数据,而不是所有的资源。
每种MySQL存储引擎都可以实现自己的锁策略和锁粒度。
表锁

1.3 事务

START TRANSACTION
COMMIT
ROLLBACK
ACID

1.3.1 隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL 隔离级别
未提交读
提交读
可重复读:会产生幻读,但是InnoDB和XtraDB通过多版本并发控制(MVCC)解决了脏读问题。
可串行化:强制事务串行执行。

1.3.3 事务日志
事务日志可以帮助提高事务的效率,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘中。以后再慢慢刷回磁盘。修改数据需要写两次磁盘。
如果事务日志未写到硬盘中,系统出现崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。

1.3.4 MySQL中的事务
自动提交:
如果不是显式地开始一个事务,则每个查询都会被当作一个事务执行提交操作。
SET AUTOCOMMIT=1 或者 on 开启,0或者off关闭。
当关闭自动提交后,需要显式执行COMMIT或者ROLLBACK,该事务才会结束。

如果在事务中混合使用存储引擎,在回滚的时候,非事务型的表上的变更会无法撤销。

1.4 多版本并发控制

MVCC是行级锁的一个变种,但是他在很多情况下避免了加锁操作,因此开销更低。
大多数MVCC都实现了非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。
典型的有乐观并发控制和悲观并发控制。
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建版本号,一个保存行的过期版本号。

1.5 MySQL的存储引擎

不同的存储引擎保存数据和索引的方式是不同的,但表的定义则是在MySQL服务层统一处理的。
可以使用SHOW TABLE STATUS LIKE ‘表名’ 命令,显式表的相关信息

1.5.1 InnoDB 存储引擎
InnoDB的数据存储在表空间中,表空间是由InnoDB管理的一个黑盒子,由一系列的数据文件组成。也可以将每个表的数据和索引存放在单独的文件中。
InnoDB 采用MVCC来支持高并发,并且实现了四个标准的隔离级别,其默认级别是可重复读,并且通过间隙锁策略来防止幻读的出现(间隙锁:不仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入)
InnoDB 是基于聚簇索引建立的,聚簇索引对主键查询有很高的性能。若表上的索引较多的话,主键应当尽可能地小。
InnoDB 的存储格式是平台独立的。

InnoDB 内部做了很多优化,包括从磁盘读取数据时采用的可预测性预读,能够自动在内存中创建hash索引以加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓存区。

1.5.2 MyISAM 存储引擎
MyISAM会将表存储在两个文件中:数据文件和索引文件
加锁与并发:表锁,共享锁,排他锁,但是在表有读取查询的同时,也可以往表中插入新的记录(并发插入)
支持全文索引
设计简单,数据紧密格式存储
表锁性能的问题严重

原文:https://time.geekbang.org/column/article/68319
MySQL 的基本架构

MySQL_第2张图片

连接器:

连接器负责跟客户端建立连接、获取权限、维持和管理连接。

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

show processlist命令:查看连接状态。

默认建议使用长连接,因为创建连接消耗的资源较大。
但如果长连接过多,内存也会上涨得很快,所以:

1.定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
2.如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

查询缓存(8.0之后此功能消失)
连接建立完成后,你就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存。
MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。

但查询缓存往往弊大于利。
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。

将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。
对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样:

mysql> select SQL_CACHE * from T where ID=10;

分析器
词法分析:分析你的动作
语法分析:分析语句是否合法

优化器
确定需要做什么。
对索引字段做函数操作,优化器会放弃走树搜索功能。

执行器
先判断用户对表有没有执行权限。
如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
如:

//ID 字段没有索引
mysql> select * from T where ID=10;

1.调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
2.调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
3.执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。

可以在在数据库的慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。

日志系统

redo log

WAL(Write-Ahead Logging)
当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。

InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写。

redo log file:
文件格式:ib_logfile{x}

MySQL_第3张图片

因为日志的大小限制,所以超过之后,需要先将一部分更新到磁盘(flush),并且删除,有空闲的内存继续写到日志上。

write pos 是当前记录的位置,一边写一边后移,write pos 是当前记录的位置,一边写一边后移,checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

crash-safe
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失。

读数据的时候直接从内存返回结果。不需要先更新磁盘中的数据

redo log 的写入机制
MySQL_第4张图片

1.存在 redo log buffer 中,物理上是在 MySQL 进程内存中,就是图中的红色部分;
2.写到磁盘 (write),但是没有持久化(fsync),物理上是在文件系统的 page cache 里面,也就是图中的黄色部分;
3.持久化到磁盘,对应的是 hard disk,也就是图中的绿色部分。

开启redo log:(控制写入策略)
1.设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
2.设置为 1 的时候,表示每次事务提交时都将 redo 直接持久化到磁盘;
3.设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。

除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的 redo log 写入到磁盘中。
1.一种是,redo log buffer 占用的空间即将达到innodb_log_buffer_size 一半的时候,后台线程会主动写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是 write,而没有调用 fsync,也就是只留在了文件系统的 page cache。
2.另一种是,并行的事务提交的时候,顺带将这个事务的redo log buffer 持久化到磁盘。假设一个事务A 执行到一半,已经写了一些 redo log 到 buffer 中,这时候有另外一个线程的事务 B 提交,如果innodb_flush_log_at_trx_commit设置的是 1,那么按照这个参数的逻辑,事务 B 要把 redo log buffer 里的日志全部持久化到磁盘。这时候,就会带上事务 A 在 redo log buffer 里的日志一起持久化到磁盘。

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

开启binlog:
sync_binlog 这个参数设置成 1

双‘1’配置
sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。

binlog的三种格式:
statement、row、mixed

statement
记录的是原始命令
第一行:BEGIN
中间:执行的语句
最后一行:COMMIT /xid = xx/ xid是代表binlog记录完整的一个标志

binlog 的写入机制(一次性写完)
binlog 的写入逻辑比较简单:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。

MySQL_第5张图片

可以看到,每个线程有自己 binlog cache,但是共用同一份 binlog 文件。
1.图中的 write,指的就是指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快。
2.图中的 fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为 fsync 才占磁盘的 IOPS。

write 和 fsync 的时机,是由参数 sync_binlog 控制的:

1.sync_binlog=0 的时候,表示每次提交事务都只write,不 fsync;
2.sync_binlog=1 的时候,表示每次提交事务都会执行fsync; 
3.sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才fsync。(通常做法,但是异常重启会丢失N个事物的binlog日志)
mysql> update T set c=c+1 where ID=2;

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

两阶段提交
为了让两份日志之间的逻辑一致,即保持两份日志上的逻辑一致。
由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。

如果不采用两阶段提交,可能出现的问题:
1.先写 redo log 后写 binlog。
如果写入binlog的时候系统重启,则binlog没有该逻辑日志,日后需要临时恢复库的时候就会丢失一次数据。
2.先写 binlog 后写 redo log。
如果在写入redo log的时候系统奔溃了,那么在日后如果临时恢复库的时候,就会多了一个事务出来。

MySQL_第6张图片
如果时刻A崩溃,回滚
如果时刻B崩溃,会做出判断规则:
1.如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交;
2.如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整:
a. 如果是,则提交事务;
b. 否则,回滚事务。

两阶段提交细化
MySQL_第7张图片

如果想提升 binlog 组提交的效果,可以通过设置
###组提交,而不是每次事物提交就写入磁盘,有利于IOPS

binlog_group_commit_sync_delay和 binlog_group_commit_sync_no_delay_count 来实现。
1.binlog_group_commit_sync_delay参数,表示延迟多少微秒后才调用 fsync;
2.binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用 fsync

所有如果MySQL出现IO上的性能问题,可考虑以下方法:
1.设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,减少 binlog 的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
2.将 sync_binlog 设置为大于 1 的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢 binlog 日志。
3.将 innodb_flush_log_at_trx_commit 设置为 2。这样做的风险是,主机掉电的时候会丢数据。

如何判断binlog是完整的
statement 格式的 binlog,最后会有 COMMIT;
row 格式的 binlog,最后会有一个 XID event;
另外,在 MySQL 5.6.2 版本以后,还引入了 binlog-checksum 参数,用来验证binlog 内容的正确性。对于 binlog 日志由于磁盘原因,可能会在日志中间出错的情况,MySQL可以通过校验 checksum 的结果来发现。所以,MySQL 还是有办法验证事务 binlog 的完整性的。

redo log 和 binlog 是怎么关联起来的?
它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log:
1.如果碰到既有 prepare、又有 commit 的 red log,就直接提交;
2.如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。

事务隔离

事务隔离的实现
每条记录在更新的时候都会同时记录一条回滚操作。同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。

回滚日志什么时候删除
系统会判断当没有事务需要用到这些回滚日志的时候,回滚日志会被删除。
什么时候不需要了
当系统里么有比这个回滚日志更早的read-view的时候。

为什么尽量不要使用长事务
长事务意味着系统里面会存在很老的事务视图,在这个事务提交之前,回滚记录都要保留,这会导致大量占用存储空间。除此之外,长事务还占用锁资源,可能会拖垮库。

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

可以在 information_schema 库的 innodb_trx 这个表中查询长事务

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

当前读:
更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。
select语句加锁也是当前读。
MySQL_第8张图片
下面两个sql分别加了读锁(S锁,共享锁)和写锁(X锁,排他锁)

mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;

索引

InnoDB 的索引模型

每一个索引在 InnoDB 里面对应一棵 B+ 树。

如有一个表,表中有两个索引,分别为主键索引和非主键索引。

mysql> create table T(
id int primary key, 
k int not null, 
name varchar(16),
index (k))engine=InnoDB;

表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下。
MySQL_第9张图片
主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。

如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树.
如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。(需要扫描多一次表)

索引维护

页分裂
插入新的节点,会挪动其他受影响的数据,最糟情况是,如果所在的数据页已经满了,根据B+树的算法,这时候需要申请一个新的数据页。然后挪动部分数据过去(称为页分裂)。页分裂除了影响性能外,还会影响数据页的利用率,整体空间利用率降低大约50%。

页合并
当相邻的两个页利用率很低之后,会将数据页做合并。

自增主键

NOT NULL PRIMARY KEY AUTO_INCREMENT

因为自增主键都是追加操作,所以不会挪动其他数据。
另一优势更在于自增主键占用空间小。

重建索引和重建主键索引
但切记,如果同时操作,其实第一个重建索引是多余的。

alter table T drop index k;
alter table T add index(k);

alter table T drop primary key;
alter table T add primary key(id);

覆盖索引
如果一个索引包含(或覆盖)所有需要查询的字段的值,称为‘覆盖索引’。即只需扫描索引而无须回表。
只扫描索引而无需回表的优点:
1.索引条目通常远小于数据行大小,只需要读取索引,则mysql会极大地减少数据访问量。
2.因为索引是按照列值顺序存储的,所以对于IO密集的范围查找会比随机从磁盘读取每一行数据的IO少很多。
3.一些存储引擎如myisam在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用
4.innodb的聚簇索引,覆盖索引对innodb表特别有用。(innodb的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询)

最左前缀原则
在建立联合索引的时候,如何安排索引内的字段顺序。
1.如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
2.考虑联合索引的空间。如果既有联合查询,又有基于a,b各自的查询。那么可以创建索引(a,b)(b)。

索引下推

在索引遍历过程中,对索引中包含的字段先做判断,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

change buffer(只有普通索引可以使用)
适用于写多读少的场景,不用频繁的访问I/O。如果场景是更新完立刻查询,则不适合,因为会立即触发merge过程。
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。
change buffer 在内存中有拷贝,也会被写入到磁盘上。
将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。
事物提交的时候,会把change buffer的操作也记录到redo log里。

merge的执行流程:
1.从磁盘读入数据页到内存(老版本的数据页);
2.从 change buffer 里找出这个数据页的 change buffer 记录 (可能有多个),依次应用,得到新版数据页;
3.写 redo log。这个 redo log包含了数据的变更和 change buffer 的变更。

对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。所以每次判断都要将数据页读入内存才能判断。不适合使用change buffer

redo log 主要节省的是随机写磁盘的 IO 消耗转成顺序写。
change buffer 主要节省的则是随机读磁盘的 IO 消耗。

创建字符串索引

1.直接创建完整索引,这样可能比较占用空间;

// index1 索引里面,包含了每个记录的整个字符串
mysql> alter table SUser add index index1(email);

2.创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引

//前缀索引
// index2 索引里面,对于每个记录都是只取前 6 个字节。
mysql> alter table SUser add index index2(email(6));

选择字符串前缀索引的长度

算出这个字符串有多少个不同的值

mysql> select count(distinct email) as L from SUser;

依次选取不同长度的前缀来看这个值,如:

mysql> select 
  count(distinct left(email,4))as L4,
  count(distinct left(email,5))as L5,
  count(distinct left(email,6))as L6,
  count(distinct left(email,7))as L7,
from SUser;

不过使用前缀索引就用不上覆盖索引对查询性能的优化了

3.倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题。

4.创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描。

对索引字段做函数操作,优化器会放弃走树搜索功能。

MySQL的三种锁

全局锁
对整个数据库实例加锁。
命令是 Flush tables with read lock (FTWRL)
当需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。

全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都 select 出来存成文本。(InnoDB等拥有事务的引擎会有更好的方法。)

如果拥有事务操作,那我们在备份数据库的时候,会启动一个事务,确保拿到一致性视图,由于MVCC的支持,这个过程中数据是可以更新的。(MyIASM没有事务,只能使用FTWRL)

表级锁
表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)
表锁的语法是 lock tables … read/write。与 FTWRL 类似,可以用 unlocktables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。

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

1.读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
2.读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性.

共享锁: SELECT … LOCK IN SHARE MODE
排它锁: SELECT … FOR UPDATE

其中排他锁这个场景大家都知道, 就是多个session的事务要对同一个表的一/多条数据进行更新操作的时候, 要先锁定再更新来消除并发造成的数据不一致

而共享锁的使用场景说的有主-从表的这种情况, 比如想在从表insert一条记录, 需要先将主表相关的数据加S锁锁定, 然后再insert从表, 来实现主从表数据一致性, 即有可能其他session会再此时delete主表的这条数据而造成只有从表有数据而主表无数据的数据不一致结果

解决幻读
给数据加上间隙锁,如6个数据加上了行锁,还同时加入了7个间隙锁。
间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。也就是说,我们的表 t 初始化以后,如果用select * from t for update 要把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。(假如只有6个数据)

间隙锁和行级锁组合起来称为Next-Key Lock

间隙锁会影响并发度

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

如何安全地给小表加字段?
首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。
但考虑一下这个场景。如果你要变更的表是一个热点表,虽然数据量不大,但是上面的请求很频繁,而你不得不加个字段,你该怎么做呢?
这时候 kill 可能未必管用,因为新的请求马上就来了。比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。

ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ... 

Online DDL

  1. 拿MDL写锁
  2. 降级成MDL读锁
  3. 真正做DDL
  4. 升级成MDL写锁
  5. 释放MDL锁

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

以下给一个例子:

如果A在电影院B买票,在数据库中执行三个操作
1.用户A账户扣钱
2.电影院B账户加钱
3.记录交易日志
如果现在有另一个人C买票,因为上述三个步骤要加事务。所以怎么优化会时效率最高呢。
答案是将电影院加钱的操作放最后,因为两个人买票,会给同一个价钱的行加锁。所以放到最后,可以减少事务之间的锁等待,提升了并发度。

死锁和死锁检测

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

超时设置通常时间很难把握,太长会导致业务卡顿,太快可能有些不是死锁的业务也被回滚。所以通常使用第二种策略,但是第二种策略也会有缺陷,如果并发量高,那死锁检测是指数级别的。

死锁检测的优化
1.确保业务不会出现死锁,将将死锁检测关掉。
2.控制并发度,如服务端逻辑控制,加队列。
3.将一行改成逻辑上的多行来减少锁冲突。

采样统计
采样统计的时候,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。
当变更的数据行数超过 1/M 的时候,会自动触发重新做一次索引统计。

在 MySQL 中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent 的值来选择:
1.设置为 on 的时候,表示统计信息会持久化存储。这时,默认的 N 是 20,M 是 10。
2.设置为 off 的时候,表示统计信息只存储在内存中。这时,默认的 N 是 8,M 是 16。

选错索引的优化:
1.如果认为统计索引信息错误,可以让mysql重新统计:
analyze table t ;
2.强制使用索引:
select * from t force index(索引字段)…
3.修改语句来引导优化器
4.增加或删除索引来绕过这个问题。

MySQL出现性能抖动情况
1."redo log"满了,刷新脏页到磁盘数据。
这种情况是 InnoDB 要尽量避免的。因为出现这种情况的时候,整个系统就不能再接受更新了,所有的更新都必须堵住。如果你从监控上看,这时候更新数会跌为 0。

2.内存不足,淘汰内存中的脏页,同时将脏页数据刷新到磁盘。
InnoDB 用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:第一种是,还没有使用的;第二种是,使用了并且是干净页;第三种是,使用了并且是脏页。
而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:如果要淘汰的是一个干净页,就直接释放出来复用;但如果是脏页呢,就必须将脏页先刷到磁盘,变成干净页后才能复用。
1.一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长;
2.日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的。

3.空闲状态下,刷新脏页到磁盘。(影响小)
4.刷新全部脏页到磁盘。(影响小)

缓存池:
buffer pool(缓存池)是innodb存储引擎带的一个缓存池,查询数据的时候,它首先会从内存中查询,如果内存中存在的话,直接返回,从而提高查询响应时间。Buffer pool是设置的越大越好,一般设置为服务器物理内存的70%。
注意:query cache是mysql的server层的,buffer pool是innodb的,两个属于不同层的
作用:InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。通常使用缓冲池来提高数据库的整体性能。缓冲池简单说就是一块内存,通过内存的速度弥补磁盘速度较慢对数据库性能的影响。在数据库中进行读操作时,首先将从磁盘读到的页存放在缓冲池中,下一次读取相同的页时,首先判定是否存在缓冲池中,如果有就是被命中直接读取,没有的话就从磁盘中读取。在数据库进行改操作时,首先修改缓冲池中的页,然后在以一定的频率刷新到磁盘上。这里的刷新机制不是每页在发生变更时触发。而是通过一种checkpoint机制刷新到磁盘的。可以通过innodb_buffer_pool_size(单位块)参数来设置缓冲池的大小。缓冲池中的数据库类型有:索引页、数据页、undo页、插入缓存页(insert buffer)、自适应hash(adaptive hash index)、innodb存储的锁信息(lock info)、数据字典信息(data dictionary)。通过参数innodb_buffer_pool_instances设置允许有多个缓冲池实例,每个页根据哈希值平均分配到不同的实例中,以减少数据库内部资源的竞争,增加数据库的并发处理能力。

InnoDB 刷脏页的控制策略
一、正确地告诉 InnoDB 所在主机的 IO 能力
测试磁盘随机读写的命令:

 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_io_capacity 这个参数了,它会告诉 InnoDB 你的磁盘能力

控制刷脏页的速度
速度参考的因素是:1.脏页比例。2.redo log写盘速度
参数 innodb_max_dirty_pages_pct 是脏页比例上限,默认值是 75%。InnoDB 会根据当前的脏页比例(假设为 M),算出一个范围在 0 到 100 之间的数字。

F1(M)
{
  if M>=innodb_max_dirty_pages_pct then
      return 100;
  return 100*M/innodb_max_dirty_pages_pct;
}

nnoDB 每次写入的日志都有一个序号,当前写入的序号跟checkpoint 对应的序号之间的差值,我们假设为 N。InnoDB 会根据这个 N 算出一个范围在 0 到 100 之间的数字,这个计算公式可以记为 F2(N)。F2(N) 算法比较复杂,你只要知道 N 越大,算出来的值越大。

根据上述算得的 F1(M) 和 F2(N) 两个值,取其中较大的值记为 R,之后引擎就可以按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度。

MySQL_第10张图片
InnoDB的空间回收

InnoDB 表包含两部分,即:表结构定义和数据。

参数 innodb_file_per_table
表数据既可以存在共享表空间里,也可以是单独的文件。这个行为是由参数 innodb_file_per_table 控制的:
1.这个参数设置为 OFF 表示的是,表的数据放在系统共享表空间,也就是跟数据字典放在一起;(删除表,空间也是不会回收的)
2.这个参数设置为 ON 表示的是,每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中。(建议)

数据删除流程
MySQL_第11张图片

如果要删除R4这个记录,InnoDB 引擎只会把 R4 这个记录标记为删除。如果之后要再插入一个 ID 在 300 和 600 之间的记录时,可能会复用这个位置。但是,磁盘文件的大小并不会缩小。(记录复用)

如果要删除整个页,那么整个数据页都可以被复用。(数据页复用)

记录的复用,只限于符合范围条件的数据。
而当整个页从 B+ 树里面摘掉以后,可以复用到任何位置。

如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用。

所以如果我们用 delete 命令把整个表的数据删除,结果就是,所有的数据页都会被标记为可复用。但是磁盘上,文件不会变小。

如果插入一条新数据的时候造成分页,原来表的末尾就会留下了空洞。(可能不止1个位置是空洞的)
如果更新索引上的值,可以理解为删除一个旧的值,再插入一个新的值,也会造成空洞。

重建表
使用 alter table A engine=InnoDB命令来重建表。
这种操作就像创建一个临时表B,然后将表A的数据依次插入表B,最后用表B替换表A。

5.6版本之后引入Online DDL,对重建表进行了优化
1.建立一个临时文件,扫描表 A 主键的所有数据页;
2.用数据页中表 A 的记录生成 B+ 树,存储到临时文件中;
3.生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中,对应的是图中 state2 的状态;
4.临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3的状态。
5.用临时文件替换表 A 的数据文件。

DDL 过程如果是 Online 的,就一定是 inplace的。

optimize table:recreate+analyze
analyze table:对表的索引信息做重新统计
alter table:alter table t engine = InnoDB(也就是recreate),重建表

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

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

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

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

Order by

全字段排序

explain select city,name,age from t where city='杭州' order by name limit 1000  ;
//需要先给city加普通索引
//其实加个联合索引alter table t add index city_user(city, name);就不需要排序了(使用了覆盖索引)

在这里插入图片描述
执行流程如下:
1.初始化 sort_buffer,确定放入 name、city、age 这三个字段;
2.从索引 city 找到第一个满足 city='杭州’条件的主键 id,也就是图中的 ID_X;
3.到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer中
4.从索引 city 取下一个记录的主键 id;
5.重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的 ID_Y;
6.对 sort_buffer 中的数据按照字段 name 做快速排序;
7.按照排序结果取前 1000 行返回给客户端。

sort_buffer_size
MySQL 为排序开辟的内存(sort_buffer)的大小。
如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。

可以用下面介绍的方法,来确定一个排序语句是否使用了临时文件。

/* 打开 optimizer_trace,只对本线程有效 */
SET optimizer_trace='enabled=on'; 

/* @a 保存 Innodb_rows_read 的初始值 */
select VARIABLE_VALUE into @a from  performance_schema.session_status where variable_name = 'Innodb_rows_read';

/* 执行语句 */
select city, name,age from t where city='杭州' order by name limit 1000; 

/* 查看 OPTIMIZER_TRACE 输出 */
SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G

/* @b 保存 Innodb_rows_read 的当前值 */
select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';

/* 计算 Innodb_rows_read 差值 */
select @b-@a;

rowid 排序
控制用于排序的行数据的长度的一个参数:max_length_for_sort_data
如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。

上面的执行流程变为:

explain select city,name,age from t where city='杭州' order by name limit 1000  ;
//需要先给city加普通索引

1.初始化 sort_buffer,确定放入两个字段,即 name和id;
2.从索引 city 找到第一个满足 city='杭州’条件的主键 id,也就是图中的 ID_X;
3.到主键 id 索引取出整行,取 name、id这两个字段,存入 sort_buffer 中;
4.从索引 city 取下一个记录的主键 id;
5.重复步骤 3、4 直到不满足 city='杭州’条件为止,也就是图中的 ID_Y;
6.对 sort_buffer 中的数据按照字段 name 进行排序。
7.遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。(扫描行会更多)

磁盘临时表
tmp_table_size 这个配置限制了内存临时表的大小,默认值是16M。
如果临时表大小超过了 tmp_table_size,那么内存临时表就会转成磁盘临时表。

随机算法

如果要取表中的随机三行数据。
比较好的做法是,扫描行数C,然后得到三个值分别X,Y,Z=rand()*C,最后分别select limit X,1;

mysql> select count(*) into @C from t;
set @Y1 = floor(@C * rand());
set @Y2 = floor(@C * rand());
set @Y3 = floor(@C * rand());
select * from t limit @Y1,1; // 在应用代码里面取 Y1、Y2、Y3 值,拼出 SQL 后执行
select * from t limit @Y2,1;
select * from t limit @Y3,1;

关闭表/关闭所有表

flush tables t with read lock;

flush tables with read lock;

查看谁占有写锁

mysql> select * from t sys.innodb_lock_waits where locked_table=`'test'.'t'`\G

主备同步流程
MySQL_第12张图片

1.在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
2.在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。
3.主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
4.备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
5.sql_thread 读取中转日志,解析出日志里的命令,并执行。

你可能感兴趣的:(数据库)