Mysql45讲

1.聚簇索引/非聚簇索引:

聚簇索引 (Clustered Index): 在InnoDB中,表的数据行是按照聚簇索引的键值存储的,这意味着表数据和索引数据是一起存储的。 一个表只能有一个聚簇索引。 聚簇索引的叶子节点直接包含了完整的数据行。 通常,主键索引就是聚簇索引,但也可以选择其他唯一的列作为聚簇索引

主键索引(聚簇索引)的叶子节点直接包含了对应的行数据。这意味着,查找主键索引时,一旦找到了对应的键值,相关的行数据就已经在那里了,不需要额外的数据块查找。非叶子节点:存储键值和指向子节点的指针。叶子节点:存储键值以及与该键值关联的完整数据行。

非聚簇索引 (Non-Clustered Index 或 Secondary Index): 非聚簇索引和表数据是分开存储的。 一个表可以有多个非聚簇索引。 非聚簇索引的叶子节点包含索引的键值和一个指向对应聚簇索引(通常是主键)的指针。 当使用非聚簇索引进行查询时,可能需要“回表”到聚簇索引来获取完整的数据行,除非查询被覆盖索引满足。

在InnoDB中,主键索引(也称为聚簇索引)是主要的索引,而所有非主键索引都被称为二级索引或非聚簇索引。二级索引的叶子节点不包含完整的数据行,而是包含键值和一个指向对应数据行的指针(通常是主键值)。当查询需要从二级索引“回表”到主键索引来获取完整的数据行时,会增加查询的I/O成本,因此覆盖索引在这种情况下就显得尤为重要,因为它可以避免这种额外的I/O开销。

覆盖索引 (Covering Index): 覆盖索引不是一个独立的索引类型,而是指的是一个查询可以完全通过使用一个非聚簇索引来满足,不需要回到聚簇索引来获取数据。 使用覆盖索引可以显著提高查询性能,因为它避免了额外的数据查找。 任何索引,无论是聚簇还是非聚簇,都可以作为覆盖索引,只要它满足查询的需求。

总结:聚簇索引和非聚簇索引是描述数据存储结构的术语,而覆盖索引是描述查询性能优化的术语。

2.最左前缀原则

联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符,只要满足最左前缀,就可以利用索引来加速检索 在建立联合索引的时候,如何安排索引内的字段顺序? 这里我们的评估标准是:索引的复用能力 因此,第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的

最左前缀原则是指在查询时,只有满足最左边的前缀条件,才能利用到联合索引。也就是说,在一个多列的联合索引中,如果不按照索引的最左列开始查找,则无法使用该索引。

举个例子来说明这个原则和如何安排索引内的字段顺序:

假设我们有一个用户表,表中有三个字段:user_id, user_name, 和 user_email。我们经常执行以下三种查询:

根据 user_id 查询 根据 user_name 查询 根据 user_id 和 user_name 一起查询

为了优化这三种查询,我们可能会考虑建立以下三个索引: 索引A:user_id 索引B:user_name 索引C:user_id, user_name

但是,根据最左前缀原则,我们实际上只需要建立以下两个索引: 索引A:user_id, user_name 索引B:user_name

为什么呢? 当我们只根据 user_id 查询时,可以使用索引A的最左前缀user_id。 当我们根据 user_name 查询时,可以使用索引B。 当我们根据 user_id 和 user_name 一起查询时,可以完全使用索引A。 这样,我们只需要维护两个索引,而不是三个,从而减少了索引的维护成本和空间。

所以,当我们说“索引的复用能力”和“如果通过调整顺序,可以少维护一个索引”的时候,意思是:在设计索引时,考虑到常见的查询模式,并尽量减少总的索引数量,从而提高效率和减少维护成本。

3.索引下推

索引下推(Index Condition Pushdown,简称 ICP)是MySQL 5.6及之后版本中引入的一个查询优化技术。它允许MySQL在使用索引检索数据时更早地评估WHERE子句中的条件,从而避免不必要的数据访问。

为了更好地理解索引下推,我们可以通过一个例子来说明。

假设我们有一个订单表orders,表中有order_id、customer_id和order_date三个字段。其中,order_id是主键,customer_id和order_date是一个联合索引。

现在,我们想查询所有在某个日期之后的订单,并且这些订单的customer_id为某个特定值。SQL查询可能是这样的:

SELECT * FROM orders WHERE customer_id = 1001 AND order_date > '2022-01-01';

在没有索引下推的情况下,MySQL的查询过程可能是这样的:

使用customer_id和order_date的联合索引找到所有customer_id为1001的记录。 对于每条找到的记录,回表获取完整的行数据。 检查order_date是否大于’2022-01-01’。 这意味着,即使order_date的条件不满足,MySQL也会回表获取数据,这是不必要的I/O操作。

但是,如果启用了索引下推,查询过程会有所不同:

使用customer_id和order_date的联合索引找到所有customer_id为1001的记录。 在索引层面,即在访问索引的时候,就检查order_date是否大于’2022-01-01’。 只有当order_date的条件满足时,才回表获取完整的行数据。 通过这种方式,索引下推可以减少不必要的数据访问,从而提高查询性能。

如何设置 ICP 在 MySQL 5.6 及更高版本中默认启用。可以通过以下方式检查它是否已启用:

SHOW VARIABLES LIKE 'optimizer_switch';

在 MySQL 中,索引下推(Index Condition Pushdown,简称 ICP)是一个优化策略,它允许 MySQL 在索引层评估 WHERE 子句的部分或全部条件,而不是在存储引擎层评估。这可以减少存储引擎必须检查的行数,从而提高查询性能。

在传统的查询执行中,MySQL 会使用索引来查找满足某些条件的行,然后将这些行的数据返回给服务器层,服务器层再对这些行应用 WHERE 子句中的其他条件。而在 ICP 中,这些额外的条件可以在索引层被评估,从而减少需要返回给服务器层的行数。

4.锁

根据加锁的范围,MySQL 里面的锁大致可以分为全局锁、表级锁和行锁三类

在 InnoDB 中,锁是加在索引上的

4.1全局锁

MySQL 加全局读锁的方法

Flush tables with read lock;(FTWRL)

全局锁的使用场景是,做全库逻辑备份

使用 FTWRL 的风险:

如果是在主库上备份,那么备份期间不能执行更新,导致业务停摆 如果是在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,导致主从延迟

更好的解决办法是使用官方自带的逻辑备份工具 mysqldump ,当 mysqldump 使用参数 -single-transaction 时,导数据之前会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。

4.2表级锁

表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。

4.3表锁的语法

lock tables ... read/write;

与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开时自动释放

元数据锁 MDL 不需要显式使用,在访问一个表的时候会被自动加上。当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。

读锁之间不互斥,因此可以有多个线程同时对一张表增删改查.

读写锁之间、写锁之间互斥,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。 事务中的 MDL 锁,在语句执行开始时申请,但是会等整个事务提交后再释放 在考虑做 DDL 变更的时候,要尽量避免长事务

5.行锁

两阶段锁

在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。

所以,如果在事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁的申请时机尽量往后放

死锁和死锁检测 死锁:当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。

出现死锁后,有两种策略: 一种策略是,直接进入等待,直到超时,超时时候可以通过参数来设置。 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。默认是开启状态

使用第一种策略的话,意味着要么设置等待时间较大,默认值是 50 s,这个等待时间往往是无法接受的,要么等待时间设置较小,这样出现死锁的时候,确实很快可以解开,但是如果该线程并不是死锁呢,而只是简单的锁等待呢,往往会造成误伤。

正常情况下都是要采用第二种策略,但是这样也会有症结,就是死锁检测要耗费大量的 CPU 资源,对于每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个 O(N) 的操作。

减少死锁的主要方向,就是控制访问相同资源的并发事务量

那么如何解决热点行更新导致的性能问题?

热点行更新是指在高并发的场景下,多个请求频繁地更新同一行数据,导致该行数据成为“热点”。这种情况可能会引起以下问题:

行锁竞争:多个事务试图更新同一行数据时,会产生行锁竞争。 事务延迟:由于锁竞争,事务可能会被迫等待,导致延迟增加。 CPU使用率上升:大量的锁竞争可能导致CPU使用率急剧上升。 为了解决热点行更新导致的性能问题,可以考虑以下策略:

分散热点:

数据细分:将热点数据分散到多个表或分区中,例如,使用哈希或范围分区。 使用缓存:将热点数据缓存在内存中,如Redis或Memcached,减少对数据库的直接访问。

优化锁策略: 减少锁的持有时间:尽量缩短事务的执行时间,使锁尽快释放。 使用乐观锁:通过版本号或时间戳来检测数据在读取和写入之间是否被其他事务修改,从而避免使用昂贵的行锁。

读写分离:将读操作和写操作分发到不同的服务器或集群,以减轻单个数据库节点的压力。

使用延迟写入:对于非关键数据,可以考虑使用延迟写入策略,例如,先写入缓存或消息队列,然后再批量写入数据库。

限流:使用限流工具或中间件来控制到数据库的请求速率,确保系统在可控的负载下运行。

数据库优化:

调整隔离级别:例如,使用读已提交(Read Committed)隔离级别,可能会减少锁竞争。 使用更快的存储:例如,使用SSD来加速数据的读写。

代码层面优化: 批量操作:尽量使用批量插入、更新或删除,而不是单条记录的操作。 避免长事务:确保事务尽可能短,避免长时间持有锁。 监控与告警:持续监控数据库的性能指标,如锁等待、CPU使用率等,并设置合适的告警阈值,以便及时发现并处理热点问题。 总之,解决热点行更新的性能问题需要从多个维度来考虑,结合具体的业务场景和系统架构来选择合适的策略。

6.事务的两种启动方式

第一种,begin / start transaction 方式启动事务,一致性视图是在执行第一个快照读语句时创建的 第二种,使用 start transaction with consistent snapshot 命令启动,一致性视图在启动时就创建

在 MySQL 中,有两个 “视图” 的概念 一个是 view 。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view … ,而它的查询方法与表一样。 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC 和 RR隔离级别的实现。

7.快照在 MVCC 中是怎样工作的

快照是基于整库的 InnoDB 里面每个事务都有一个唯一的事务 ID,叫做 transaction id。(按申请顺序严格递增的)

每行数据都有多个版本,每次事务更新数据,都会生成一个新的数据版本,并且把 transaction id 赋给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中有指针指向着它。

一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

版本未提交,不可见; 版本已提交,但是是在视图创建后提交的,不可见; 版本已提交,而且是在视图创建前提交的,可见; InnoDB 利用了 “所有数据都有多个版本” 的这个特性,实现了 “秒级创建快照” 的能力。

8.MVCC

首先,我们需要理解InnoDB的MVCC(多版本并发控制)机制。MVCC是InnoDB为了支持高并发事务而采用的一种技术,它允许多个事务同时对同一数据进行读取和修改,而不会互相阻塞。

事务ID和数据版本 事务ID:每当一个新的事务开始,它都会从一个全局递增的计数器中获得一个唯一的事务ID。

数据版本:当事务更新一行数据时,InnoDB不会直接覆盖原始数据,而是为这行数据创建一个新的版本,并将当前事务的ID赋给这个新版本的row trx_id。原始数据(旧版本)仍然被保留,并且新版本中有一个指针指向它。

即使使用一个简单的UPDATE语句(不在显式的事务块中),InnoDB仍然会为这行数据创建一个新的版本。这是因为InnoDB存储引擎在内部为每个UPDATE或DELETE操作隐式地启动了一个事务。所以,无论是在显式事务中还是单独的UPDATE/DELETE操作,InnoDB都会使用MVCC来处理数据的修改,不会直接覆盖原始数据。这种行为支持了InnoDB的事务隔离特性,并允许其他事务在同一时间读取旧版本的数据,直到新事务提交并使新版本的数据变得可见。

读取数据时的版本可见性 当一个事务尝试读取一行数据时,它需要确定哪个版本的数据是“可见”的。这取决于以下几点:

读取事务的ID:正在尝试读取数据的事务有其自己的事务ID。

数据的row trx_id:每个数据版本都有一个与之关联的事务ID,表示是哪个事务创建的这个版本。

根据上面的规则,数据的可见性可以确定为:

如果数据版本的row trx_id与读取事务的ID相同,那么这个版本是可见的(因为总是可以看到自己的修改)。

如果数据版本的row trx_id比读取事务的ID大(即,数据版本是在读取事务开始后创建的),那么这个版本是不可见的。

如果数据版本的row trx_id比读取事务的ID小,并且与其关联的事务已经提交,那么这个版本是可见的。

快照

当我们说InnoDB可以“秒级创建快照”时,我们的意思是,当事务开始时,它会立即获得一个当前的事务ID,这个ID决定了事务在其整个生命周期中可以看到哪些数据版本。这就是所谓的“快照”。

这种方法的好处是,事务不需要锁定读取的数据,因为它总是读取与其快照相对应的数据版本,这大大提高了并发性能。

总之,InnoDB的MVCC机制通过为数据的每个版本分配一个事务ID,并在读取时根据事务ID判断版本的可见性,从而实现了高并发下的数据一致性和隔离性。

9.一致性读和当前读

  1. 当前读 (Current Read) 在数据库中,当我们说到“读”,可能有两种不同的读:

一致性读:这是默认的读操作。当事务读取数据时,它不会看到其他并发事务所做的未提交的更改。它只会看到在它开始之前已经提交的更改,以及它自己所做的更改。这种读取确保了事务的隔离性。

当前读:这是当事务想要修改数据时所做的读取。在这种情况下,事务需要读取数据的最新版本,无论这个版本是否已经被提交。因为在修改数据之前,事务需要知道数据的当前状态。

  1. 更新数据的流程 当一个事务想要更新一行数据时,它的操作流程大致如下:

锁定数据:首先,事务会尝试为这行数据加锁。这确保了在此事务完成之前,没有其他事务可以修改这行数据。

执行当前读:事务会读取这行数据的最新版本。这是为了确保事务基于最新的数据状态进行更新。

修改数据:事务根据当前读取的数据值进行修改。

提交事务:如果事务成功地修改了数据并且没有遇到其他问题,它会提交,这时候更改会被永久保存。

  1. 两阶段锁协议 提到的“两阶段协议”是数据库事务管理中的一个重要概念。简单来说,它规定了事务如何加锁和释放锁:

Growing Phase:在这个阶段,事务可以获取任意数量的锁,但不能释放任何锁。

Shrinking Phase:在这个阶段,事务可以释放锁,但不能再获取新的锁。

这个协议确保了数据库的串行化,即使在高并发环境下,也能确保数据的一致性和事务的隔离性。

总结:当事务更新数据时,它首先锁定数据,然后执行当前读来获取数据的最新状态,接着基于这个状态进行修改。这个过程确保了数据的一致性和事务的隔离性。

10.隔离性如何实现

事务的隔离性是指在并发环境中,一个事务的操作不应该被其他事务所看到,除非该事务已经提交。为了实现这一特性,数据库系统提供了不同的隔离级别。在InnoDB中,事务隔离是通过以下几种机制来实现的:

多版本并发控制 (MVCC):

如前所述,InnoDB为每行数据维护多个版本。当事务读取数据时,它不是读取最新版本,而是读取在事务开始时或之前有效的版本。这确保了事务在其生命周期内看到的数据是一致的,即使其他事务正在修改数据。 MVCC允许多个读事务同时进行,而不会互相阻塞。 锁定:

共享锁 (S Locks) 和 排他锁 (X Locks): 当事务想要读取数据并可能在之后修改它时,它会请求共享锁。当事务想要修改数据时,它会请求排他锁。多个事务可以同时持有共享锁,但只有一个事务可以持有排他锁,且在此期间其他事务不能获得该数据的共享锁。 意向锁: 这是一种表明事务接下来将请求哪种类型的锁(共享或排他)的锁。它们用于处理表级锁和行级锁之间的冲突。

Next-Key Locking: InnoDB使用这种锁定策略来避免幻读(Phantom Reads)。这种策略不仅锁定查询到的行,还锁定索引范围内的间隙,确保在事务执行期间不会有新的行插入到该范围。

一致性非锁定读: 在某些隔离级别下,InnoDB允许事务执行非锁定读,这意味着读取操作不会设置任何锁,而是直接读取数据的旧版本。 根据这些机制,InnoDB提供了以下四个隔离级别:

READ UNCOMMITTED: 最低的隔离级别。一个事务可以看到其他未提交事务的更改。 READ COMMITTED: 一个事务只能看到其他事务已提交的更改。 REPEATABLE READ: 这是InnoDB的默认隔离级别。在这个级别下,事务在开始时看到的数据快照在事务结束之前都不会改变。 SERIALIZABLE: 最高的隔离级别。它通过对所有读取的行设置共享锁来实现完全的串行化。 事务隔离性的实现是一个平衡性能和数据一致性的问题。不同的应用可能需要不同的隔离级别,取决于它们的具体需求。

11.共享锁和排他锁

共享锁(S Locks)和排他锁(X Locks)通常是指行锁。在InnoDB中,这些锁是直接应用于数据行的。当事务想要读取某一行并可能在之后修改它时,它会请求这一行的共享锁;而当事务想要修改某一行时,它会请求这一行的排他锁。

意向锁则是一种稍微不同的锁,它是表级的锁。意向锁并不阻止其他事务对行进行读取或修改,而是表示一个事务在未来希望获得某一行的共享锁或排他锁。意向锁的存在是为了优化性能,使得数据库能够更有效地处理锁请求,避免不必要的锁冲突。

简而言之:

共享锁和排他锁是行级锁,直接作用于具体的数据行。 意向锁是表级锁,表示事务打算在未来对某行请求共享锁或排他锁。

12.change buffer 与 redo log 的不同

在 InnoDB 中,change buffer 机制不是一直会被用到,仅当待操作的数据页当前不在内存中,需要先读磁盘加载数据页时,change buffer 才有用武之地。而 redo log 机制,为了保证 crash-safe,一直都会用到

redo log 主要节省的是随机写磁盘的 I / O 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 I / O 消耗

13.redo log 实现持久性

redo log 在 InnoDB 存储引擎中是用来保证事务的持久性(Durability)的关键组件。它是一个循环写的日志文件,主要有以下几个原因来确保循环写不会有问题:

顺序写:首先,redo log 是顺序写的,这意味着磁盘写入操作是连续的,这大大提高了写入性能。

Write-Ahead Logging (WAL) 原则:当事务发生时,首先会写入redo log,并不立即修改数据文件。只有当redo log被持久化到磁盘后,事务才被认为是提交的。这确保了即使在系统崩溃的情况下,也可以通过redo log来恢复数据。

循环利用:redo log 是固定大小的,当它写满后,会从头开始循环写。但是,只有当旧的日志记录不再需要(即对应的数据已经持久化到数据文件中)时,它们才会被覆盖。这是通过检查redo log的检查点(checkpoint)来实现的。

检查点 (Checkpoint):检查点是redo log中的一个位置,它之前的所有操作都已经被应用到了数据文件中。这意味着,当redo log循环写入时,只有在检查点之后的位置才能被安全地覆盖。

双写缓冲区:为了防止在写入数据页到磁盘时发生部分写入的情况,InnoDB 使用了双写缓冲区。首先,修改后的页会被写入到双写缓冲区,然后再从双写缓冲区写入到实际的数据文件。这进一步确保了数据的完整性和持久性。

14.Mysql 选错索引

MySQL 有时可能会选择不是最优的索引,这通常是由于查询优化器基于统计信息和成本模型做出的决策。以下是一些可能导致MySQL选择错误索引的原因:

统计信息不准确:MySQL 使用统计信息来估计执行特定查询的成本。如果这些统计信息不准确或过时,查询优化器可能会做出不佳的决策。

成本模型的局限性:查询优化器使用一个成本模型来估计查询的执行成本。这个模型可能不总是完美的,特别是在某些复杂的查询场景中。

索引新建或者发生了大量的数据变更:如果一个索引刚刚被创建或者表中的数据发生了大量变更,统计信息可能还没有更新,导致优化器做出错误的决策。

复杂的查询:对于某些复杂的查询,优化器可能难以确定最佳的执行计划。

使用了FORCE INDEX或USE INDEX:这些查询提示可能会导致优化器选择非最优的索引。

数据分布不均:如果表中的数据分布非常不均匀,统计信息可能不能准确地反映实际的数据分布,导致优化器选择错误的索引。

系统变量设置:某些MySQL系统变量的设置可能会影响优化器的决策。

版本差异:不同版本的MySQL可能有不同的优化策略和算法。

为了避免或解决选择错误的索引的问题,可以考虑以下策略:

定期运行ANALYZE TABLE来更新统计信息。 仔细检查查询,确保它们是最优化的。 在必要时使用查询提示,如FORCE INDEX。 考虑使用EXPLAIN来查看查询的执行计划,以确定是否选择了正确的索引。 保持MySQL版本更新,以获得最新的优化器改进。

15.索引选择异常和处理

采用 force index 强行选择一个索引 考虑修改语句,引导 MySQL 使用我们期望的索引 在某些场景下,可以考虑新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引

16.给字符串字段加索引

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

17.SQL 语句为什么会突然变慢了

WAL 技术:全称是 Write-Ahead Logging ,它的关键点就是先写日志,再写磁盘。

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

WAL 技术:全称是 Write-Ahead Logging ,它的关键点就是先写日志,再写磁盘。

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

利用 WAL 技术,数据库将随机写转换成了顺序写,大大提升了数据库的性能,但是由此也带来了内存脏页的问题。

脏页:当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为 “脏页” 干净页:内存数据写入磁盘后,内存和磁盘上的数据页的内容就一致了,称为 “干净页” 平时执行很快的更新操作,其实就是在写内存和日志,而 MySQL 偶尔 “抖” 一下的那个瞬间,可能就是在刷脏页(flush)

18.数据库 flush

首先,我们需要理解几个关键概念:

Redo Log:这是一个用于确保事务持久性的日志系统。当对数据库进行更改(如插入、更新、删除等)时,这些更改首先被写入到redo log中,然后在稍后的某个时间点,这些更改会被应用到实际的数据文件中。

Checkpoint:这是一个特定的点,表示到此点为止的所有事务都已经保存到数据文件中。

脏页:这是在内存中已经被修改但尚未写入磁盘的数据页。

现在,让我们解析上述内容:

当redo log写满时,系统需要为新的日志条目腾出空间。为了做到这一点,它会移动checkpoint,这意味着它会将之前的更改应用到数据文件中,从而可以在redo log中回收空间。

如果系统内存不足,它可能需要淘汰一些数据页以腾出空间。如果被淘汰的页是脏页(即已修改但未写入磁盘的页),那么这些更改需要先被写入磁盘。

即使在系统忙碌时,MySQL也会尝试在后台刷新脏页。这是为了确保数据的持久性并优化性能。

当正常关闭MySQL时,它会确保所有在内存中的脏页都被写入磁盘,确保数据的完整性。

19.引发数据库 flush 的情况:

InnoDB 的 redo log 写满了,这时候系统会停止所有更新操作,把 checkpoint 往前推进,redo log 留出空间可以继续写(redo log 的空间是循环使用的,无所谓释放。对应的内存页会变成干净页,但是等淘汰的时候才会逐出内存) 系统空间不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是脏页,就要先将脏页写到磁盘。 MySQL 认为系统 “空闲” 的时候,但即使是不空闲的时候,MySQL 也会见缝插针地找时间,只要有机会就刷一点脏页 MySQL 正常关闭的情况下,这时候,MySQL 会把内存的脏页都 flush 到磁盘上。 对于脏页,脏页会被后台线程自动 flush,也会由于数据页淘汰而触发 flush,而刷脏页的过程由于会占用资源,就有可能会让的更新和查询语句的响应时间长一些。

20.为什么表中的数据被删除了,但是表空间却没有被回收?

InnoDB存储结构:InnoDB使用一个称为表空间的结构来存储数据。默认情况下,所有InnoDB表的数据和索引都存储在一个名为ibdata1的共享表空间文件中。

删除操作:当从InnoDB表中删除数据时,数据确实会被删除,但是与此数据相关的空间只会在InnoDB内部被标记为可用,而不会被立即返回给操作系统。

空间再利用:尽管这些空间没有被返回给操作系统,但InnoDB仍然可以再次使用它们。如果在后续的操作中插入新的数据,InnoDB会优先使用这些已释放的空间。

表空间回收:要真正从磁盘上回收这些空间,通常需要执行额外的操作,如优化表(OPTIMIZE TABLE命令)。但请注意,这通常涉及重建整个表,可能需要很长时间,并且在此过程中表可能会被锁定。

文件-per-table模式:在较新版本的MySQL中,可以启用innodb_file_per_table配置选项,这样每个InnoDB表都会有其自己的表空间文件。这可以使空间管理更加灵活,因为当优化或删除一个表时,与该表相关的空间可以被操作系统回收。

碎片化:随着时间的推移,表空间可能会碎片化,尤其是在频繁的插入、删除和更新操作之后。这也是为什么表的物理大小可能大于实际存储的数据大小。

21.重建表

经过大量增删改查的表,都是有可能存在空洞的,所以,如果能够把这些空洞去掉,就能达到收缩表空间的目的。

而重建表,就可以达到这样的目的

可以用来重建表的命令

alter table t engine=InnoDB; 注意:

从 MySQL 5.6 版本开始,alter table t engine=InnoDB 可以支持 Online 了 analyze table t 其实不是重建表,只是对表的索引信息做重新统计,没有修改数据,这个过程中加入了 MDL 读锁 optimize table t 等于 重建表+ analyze 。

22.count(*) 为什么这么慢?

首先需要明确的是,在不同的 MySQL 引擎中,count(*) 有不同的实现方式

MyISAM 引擎把一个表的总行数存放在磁盘上,查询时直接返回 InnoDB 在执行 count(*) 时,需要把数据一行一行的从引擎中读出来,然后累积计数 注意,这是没有在考虑过滤条件的情况下,如果加了 WHERE 条件,MyISAM 表也不可能返回的这么快 为什么 InnoDB 不跟 MyISAM 一样,也把数字存起来呢

这是因为即使是在同一时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表 “应该返回多少行” 也是不确定的

对于 count(*) 优化的基本思路是:找一个地方,把操作记录表的行数存起来

22.1 使用缓存系统保存计数

比如说使用 Redis,但是这样会存在丢失更新的问题,而且即使 Redis 正常,这个值还是逻辑上不精确的

22.2 在数据库保存计数

把这个计数直接放到数据库里单独的一张计数表 C 中,这样做是行得通的,利用事务这个特性,可以将问题解决掉

其实,把计数放在 Redis 里面,不能够保证计数和 MySQL 表里的数据精确一致的原因,是 这两个不同的存储构成的系统不支持分布式事务,无法拿到精确一致的视图。 而把计数值也放在 MySQL 中,就解决了一致性视图的问题。 附,count() 函数效率排序 count(字段) < count(主键 id) < count(1) ≈ count( * ),所以建议是,尽量使用 count(*)

23.order by 是怎样工作的

全字段排序 ​取出想要查询的所有列的值放入 sort_buffer 中,然后再排序返回给客户端

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

rowid 排序 ​如果单行长度太大,全字段索引的效率就不够好,这时候可以采用 rowid 排序,只将要排序的列以及主键 id 放入 sort_buffer ,等排序完再按照 id 值回到原表中取出所有要查询的字段再返回给客户端

如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表中去取数据。

24.逻辑相同,性能差异巨大?

对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能 要注意隐式类型转换,索引字段被进行了隐式类型转换,就相当于使用了 CAST() 函数,进行了函数操作 注意,在 MySQL 中,字符串和数字做比较的话,是将字符串转换成数字

25.为什么只查一行数据,也执行这么慢?

查询长时间不返回

等 MDL 锁 状态:使用 show processlist 命令查看到状态为 Waiting for table metadata lock

原因:可能有一个线程正在表上请求或者持有 MDL 写锁,把 select 语句堵住了

解决办法:找到谁持有 MDL 写锁,然后把它 kill 掉。通过查询 sys.schema_table_lock_waits 这张表,就可以直接找出造成阻塞的 process id,把这个连接用 kill 命令断开即可。

等 flush 状态:查询出来线程的状态是 Waiting for table flush

原因:这个状态表示现在有一个线程正要对这个表做 flush 操作

解决办法:也是使用 show processlist ,然后就更上一种解决方式一样了

等行锁 状态:加共享锁的查询语句被阻塞住 原因:有另一个事务在这行记录上持有一个写锁 解决办法:MySQL 5.7 之后可以通过 sys.innodb_lock_waits 查到是谁占着这个写锁,然后给它 kill 掉

26.幻读

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

在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入数据的。因此,幻读在当前读下才会出现 幻读仅专指 “新插入的行” 幻读首先会产生语义上的问题,会破坏加锁声明,其次是数据一致性的问题,即使把所有的记录都加上锁,还是阻止不了新插入的记录

如何解决幻读 产生幻读的原因是,行锁只能只能锁住行,但是新插入记录这个动作,要更新的是记录之间的 “间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁(Gap Lock)。

间隙锁之间并不存在冲突关系,跟间隙锁存在冲突关系的,是 “往这个间隙中插入一个记录” 这个操作

间隙锁与行锁合称为 next-key lock ,每个 next-key lock 是前开后闭区间,间隙锁和 next-key lock 的引入,帮助我们解决了幻读的问题,但同时因为间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的

27.业务高峰期 MySQL 中临时提高性能的方法

第一种方法:先处理掉哪些占着连接但是不工作的线程 使用 show processlist 结合 information_schema 库的 innodb_trx 表,然后使用 kill connection + id 的命令主动断开客户端连接

优先断开事务外空闲太久的连接;如果这样还不够,再考虑断开事务内空闲太久的连接。

第二种方法:减少连接过程的消耗⚠

慢查询性能问题 导致慢查询的第一种可能是,索引没有设计好 第二种可能是,语句没写好,MySQL 5.7 提供了 query_rewrite (查询重写)功能,可以把输入的一种语句改写成另外一种模式 第三种可能是,MySQL 选错了索引,应急方案是可以给这个语句加上 force index ,同样也可以使用查询

重写功能 QPS 突增问题 如果是由全新业务的 bug 导致,可以从数据库端直接把白名单去掉 如果这个新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删除 使用查询重写功能,把压力最大的 SQL 语句直接重写成 select 1 返回 ⚠

28.MySQL 如何保证数据不丢

WAL 机制为什么好 WAL 机制主要得益于两个方面: redo log 和 bin log 都是顺序写,磁盘的顺序写比随机写速度要快 组提交机制,可以大幅度降低磁盘的 IOPS 消耗 实际上数据库的 crash-safe 保证的是:

如果客户端收到事务成功的消息,事务就一定持久化了 如果客户端收到事务失败(比如主键冲突、回滚等)的消息,事务就一定失败了 如果客户端收到 “执行异常” 的消息,应用需要重连后通过查询当前状态来继续后续的逻辑。此时数据库只需要保证内部(数据和日志之间,主库和备库之间)一致就可以了

29.MySQL 是如何保证主备一致的

binlog 的三种格式对比 binlog 有三种格式,statement、row 以及这两种模式的混合 mixed

statement statement 格式下记录到 binlog 里的是语句原文,因此可能会出现这样一种情况:在主库执行这条 SQL 语句的时候,用的是索引 a ;而在备库执行这条语句的时候,却使用了索引 b 。因此,MySQL 认为这样写是有风险的

row 当 binlog_format 使用 row 格式的时候,binlog 里面记录了真实删除行的主键 id,这样 binlog 传到备库去的时候,就肯定会删除指定行的数据,不会出现主备删除不同行的问题

mixed 为什么会有 mixed 这种 binlog 格式的存在场景呢

因为 statement 格式的 binlog 可能会导致主备不一致,所以要使用 row 格式,但 row 格式的缺点是很占空间,要记录下所有被修改行的记录,不仅会占用更大的空间,同时写 binlog 也要耗费 IO 资源,影响执行速度,

所以,MySQL 就取了个折中方案,也就是有了 mixed 格式的 binlog。mixed 格式的意思是,MySQL 自己会判断这条 SQL 语句是否可能会引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式

30.循环复制问题

实际生产上使用比较多的 双M结构

如何解决

规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系; 一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog; 每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志

主备延迟 所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值

在备库上执行 show slave status 命令,它的返回结果里会显式 seconds_behind_master ,用于表示当前备库延迟了多少秒

主备延迟最直接的表现是,备库消费中转日志(relay log)的速度,比主库生产 binlog 的速度要慢。

31.主备延迟

主备延迟来源 第一种可能是,在有些部署条件下,备库所在机器的性能要比主库所在的机器性能差。

第二种可能是,备库的压力大,一般可以这样处理:

一主多从。除了备库外,可以多接几个从库,让从库来分担读的压力 通过 binlog 输出到外部系统,比如 Hadoop 这类系统,让外部系统提供统计类查询的功能 还有第三种可能,就是大事务,这种情况就是因为主库必须等事务执行完成才会写入 binlog ,再传给备库。所以,如果一个主库上的语句执行了 10 分钟,那这个事务很可能就会导致从库延迟 10 分钟。还有另一种典型的大事务场景,就是大表 DDL 。

造成主备延迟还有一个大方向的原因,就是 备库的并行复制能力 由于主备延迟的存在,所以在主备切换的时候,就相应的有不同的策略

Tags: Mysql
Share: X (Twitter) Facebook LinkedIn