MySQL


目录



数据库三大范式

  • 第一范式:每个列都不可以再拆分。
  • 第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
  • 第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。

在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。

MySQL的binlog的几种录入格式

  • statement模式下,每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
  • row级别下,不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。
  • mixed,一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。

此外,新版的MySQL中对row级别也做了一些优化,当表结构发生变化的时候,会记录语句而不是逐行记录。

索引有哪几种类型

  • 主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。

  • 唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。

ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引

  • 普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。

ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引 ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引。

  • 全文索引: 是目前搜索引擎使用的一种关键技术。

ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引

索引的数据结构(B+树)

一棵 B+ 树需要满足以下条件:

  1. 节点的子树数和关键字数相同(B 树是关键字数比子树数少一)
  2. 节点的关键字表示的是子树中的最大数,在子树中同样含有这个数据
  3. 叶子节点包含了全部数据,同时符合左小右大的顺序

B+ 树的三个优点:

  1. 层级更低,IO 次数更少
  2. 每次都需要查询到叶子节点,查询性能稳定
  3. 叶子节点形成有序链表,范围查询方便
B+树数据结构
B+树插入过程
B+树删除过程

索引算法

  1. BTree算法

BTree是最常用的mysql数据库索引算法,也是mysql默认的算法。因为它不仅可以被用在=,>,>=,<,<=和between这些比较操作符上,而且还可以用于like操作符,只要它的查询条件是一个不以通配符开头的常量。

  1. Hash算法

Hash Hash索引只能用于对等比较,例如=,<=>(相当于=)操作符。由于是一次定位数据,不像BTree索引需要从根节点到枝节点,最后才能访问到页节点这样多次IO访问,所以检索效率远高于BTree索引。

索引设计的原则

  1. 适合索引的列是出现在where子句中的列,或者连接子句中指定的列。
  2. 基数较小的类,索引效果较差,没有必要在此列建立索引
  3. 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
  4. 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

创建索引的原则

  1. 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配, 比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
  2. 较频繁作为查询条件的字段才去创建索引
  3. 更新频繁字段不适合创建索引
  4. 若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
  5. 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
  6. 定义有外键的数据列一定要建立索引。
  7. 对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。
  8. 对于定义为text、image和bit的数据类型的列不要建立索引。

创建索引时需要注意什么

  1. 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值;
  2. 取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
  3. 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。

聚簇索引与非聚簇索引

  • 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
  • 非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中, 当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因。

事物的四大特性(ACID)

  • 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  • 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  • 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

事务的隔离级别

为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。

SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别

Redo Log(事务安全)

在工程存储项目中,有一个重要的概念,那就是 crash safe,即当服务器突然断电或宕机,需要保证已提交的数据或修改不会丢失,未提交的数据能够自动回滚,这就是 mysql ACID 特性中的一个十分重要的特性 – Atomicity 原子性

依靠 binlog 是无法保证 crash safe 的,因为 binlog 是事务提交时写入的,如果在 binlog 缓存中的数据持久化到硬盘之前宕机或断电, 在服务器恢复工作后,由于 binlog 缺失一部分已提交的操作数据,而主数据库中实际上这部分操作已经存在,从数据库因此无法同步这部分操作,从而造成主从数据库数据不一致,这是很严重的

innodb 作为具体的一个存储引擎,他通过 redolog 实现了 crash safe 的支持

mysql 有一个基本的技术理念,那就是 WAL,即 Write-Ahead Logging,先写日志,再写磁盘,从而保证每一次操作都有据可查,这里所说的“先写日志”中的日志就包括 innodb 的 redolog

crash safe 与两阶段提交

每条 redolog 都有两个状态 – prepare 与 commit 状态

例如对于一张 mysql 表,我们执行一条 SQL 语句:

UPDATE A set C=C+1 WHERE ID=2

实际上,mysql 数据库会进行以下操作(下图中深色的是 mysql server 层所做的操作,浅色部分则是 innodb 存储引擎进行的操作):

在写入 binlog 及事务提交前,innodb 先记录了 redolog,并标记为 prepare 状态,在事务提交后,innodb 会将 redolog 更新为 commit 状态,这样在异常发生时,就可以按照下面两条策略来处理:

  • 当异常情况发生时,如果第一次写入 redolog 成功,写入 binlog 失败,MySQL 会当做事务失败直接回滚,保证了后续 redolog 和 binlog 的准确性
  • 如果第一次写入 redolog 成功,binlog 也写入成功,当第二次写入 redolog 时候失败了,那数据恢复的过程中,MySQL 判断 redolog 状态为 prepare,且存在对应的 binlog 记录,则会重放事务提交,数据库中会进行相应的修改操作

整个过程是一个典型的两阶段提交过程,由 binlog 充当了协调者的角色,针对每一次日志写入,innodb 都会随之记录一个 8 字节序列号 – LSN(日志逻辑序列号 log sequence number),他会随着日志写入不断单调递增

binlog、DB 中的数据、redolog 三者就是通过 LSN 关联到一起的,因为数据页上记录了 LSN、日志开始与结束均记录了 LSN、刷盘节点 checkpoint 也记录了 LSN,因此 LSN 成为了整套系统中的全局版本信息

当异常发生并重新启动后,innodb 会根据出在 prepare 状态的 redo log 记录去查找相同 LSN 的 binlog、数据记录,从而实现异常后的恢复

MVCC(多版本并发控制)(Undo Log)

undo log 与 redo log 一起构成了 MySQL 事务日志,日志先行原则 WAL 除了包含 redo log 外,也包括 undo log,事务中的每一次修改,innodb 都会先记录对应的 undo log 记录

redo log 用于数据的灾后重新提交,undo log 主要用于数据修改的回滚

redo log 记录的是物理页的修改,undo log 记录的是逻辑日志

delete 一条记录时,undo log 中会记录一条对应的 insert 记录,反之亦然,当 update 一条记录时,它记录一条对应相反的 update 记录,如果 update 的是主键,则是对先删除后插入的两个事件的反向逻辑操作的记录

undo log

这样,在事务回滚时,我们就可以从 undo log 中反向读取相应的内容,并进行回滚,同时,我们也可以根据 undo log 中记录的日志读取到一条被修改后数据的原值

正是依赖 undo log,innodb 实现了 ACID 中的 C – Consistency 即一致性

MVCC实现原理

InnoDB 中 MVCC 的实现方式为:每一行记录都有两个隐藏列:DATA_TRX_ID、DATA_ROLL_PTR(如果没有主键,则还会多一个隐藏的主键列)。

隐藏列:DATA_TRX_ID, DATA_ROLL_PTR
  • DATA_TRX_ID:记录最近更新这条行记录的事务 ID,大小为 6 个字节

  • DATA_ROLL_PTR:表示指向该行回滚段(rollback segment)的指针,大小为 7 个字节,InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在 undo 中都通过链表的形式组织。

  • DB_ROW_ID:行标识(隐藏单调自增 ID),大小为 6 字节,如果表没有主键,InnoDB 会自动生成一个隐藏主键,因此会出现这个列。另外,每条记录的头信息(record header)里都有一个专门的 bit(deleted_flag)来表示当前记录是否已经被删除。

多个事务并行操作某行数据的情况下,不同事务对该行数据的 UPDATE 会产生多个版本,然后通过回滚指针组织成一条 Undo Log 链

版本链

MVCC与事务

多版本读

在事务 A 提交前后,事务 B 读取到的 x 的值是什么呢?答案是:事务 B 在不同的隔离级别下,读取到的值不一样。

  • 如果事务 B 的隔离级别是读未提交(RU),那么两次读取均读取到 x 的最新值,即 20。
  • 如果事务 B 的隔离级别是读已提交(RC),那么第一次读取到旧值 10,第二次因为事务 A 已经提交,则读取到新值 20。
  • 如果事务 B 的隔离级别是可重复读或者串行(RR,S),则两次均读到旧值 10,不论事务 A 是否已经提交。

在不同的隔离级别下,数据库通过 MVCC 和隔离级别,让事务之间并行操作遵循了某种规则,来保证单个事务内前后数据的一致性。

为什么需要MVCC

InnoDB 相比 MyISAM 有两大特点,一是支持事务而是支持行级锁,事务的引入带来了一些新的挑战。相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况:

  • 更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 —— 最后的更新覆盖了其他事务所做的更新。如何避免这个问题呢,最好在一个事务对数据进行更改但还未提交时,其他事务不能访问修改同一个数据。
  • 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些尚未提交的脏数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做 “脏读”。
  • 不可重复读(Non-Repeatable Reads):一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫做“不可重复读”。
  • 幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为 “幻读”。

以上是并发事务过程中会存在的问题,解决更新丢失可以交给应用,但是后三者需要数据库提供事务间的隔离机制来解决。实现隔离机制的方法主要有两种:

  1. 加读写锁
  2. 一致性快照读,即 MVCC

当前读,快照读 与 幻读

当前读

select…lock in share mode (共享读锁) select…for update update , delete , insert

当前读, 读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。

例如,假设要update一条记录,但是另一个事务已经delete这条数据并且commit了,如果不加锁就会产生冲突。所以update的时候肯定要是当前读,得到最新的信息并且锁定相应的记录。

当前读的实现方式

  • next-key锁(行记录锁+Gap间隙锁)

间隙锁:只有在Read Repeatable、Serializable隔离级别才有,就是锁定范围空间的数据,假设id有3,4,5,锁定id>3的数据,是指的4,5及后面的数字都会被锁定,因为此时如果不锁定没有的数据,例如当加入了新的数据id=6,就会出现幻读,间隙锁避免了幻读。

1.对主键或唯一索引,如果当前读时,where条件全部精确命中(=或者in),这种场景本身就不会出现幻读,所以只会加行记录锁。

2.没有索引的列,当前读操作时,会加全表gap锁,生产环境要注意。

3.非唯一索引列,如果where条件部分命中(>、<、like等)或者全未命中,则会加附近Gap间隙锁。例如,某表数据如下,非唯一索引2,6,9,9,11,15。如下语句要操作非唯一索引列9的数据,gap锁将会锁定的列是(6,11],该区间内无法插入数据。

非唯一索引列间隙锁

快照读

简单的select操作(不包括 select … lock in share mode, select … for update)。    

Read Committed隔离级别:每次select都生成一个快照读。

Read Repeatable隔离级别:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读。

快照读不会加锁,因此开启事务后进行快照读select,并不能阻止其他事务写入或修改当前事务里的数据,因此可能会出现幻读。

快照读的实现方式

  • MVCC(undo log)

锁的粒度

  • 行级锁:行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 表级锁:表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
  • 页级锁:页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

锁的类别

  • 共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
  • 排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

锁的算法

  • Record lock:单个行记录上的锁
  • Gap lock:间隙锁,锁定一个范围,不包括记录本身
  • Next-key lock:record+gap 锁定一个范围,包含记录本身

什么是死锁?怎么解决

  • 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
  • 常见的解决死锁的方法
    • 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
    • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
    • 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
  • 如果业务处理不好可以用分布式事务锁或者使用乐观锁

乐观锁和悲观锁

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。

大表数据查询,怎么优化?

  • 优化shema、sql语句+索引;
  • 第二加缓存,memcached, redis;
  • 主从复制,读写分离;
  • 垂直拆分,根据你模块的耦合度,将一个大的系统分为多个小的系统,也就是分布式系统
  • 水平切分,针对数据量大的表,这一步最麻烦,最能考验技术水平,要选择一个合理的sharding key, 为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表

参考

Mysql最全面试指南

InnoDB MVCC 机制,看这篇就够了

mysql 异常情况下的事务安全 – 详解 mysql redolog

一文讲透 MySQL 的 MVCC 机制

【MySQL】当前读、快照读、MVCC

MySQL InnoDB Cluster 详解

MySQL InnoDB Cluster实战

MySQL-8.0 Group Replication 研究与改造汇总

如何看待MySQL发布的Group Replication?

美团点评基于MGR的CMDB高可用架构搭建之路


下一页