Bootstrap

数据库面试题

数据库

mysql

数据库基础知识

什么是MySQL?

MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。

数据库三大范式是什么

第一范式:每个列都不可以再拆分。

第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。

第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。

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

SQL的几种连接查询方式(内连接、外连接、全连接、联合查询)

一、内连接(inner join)

二、外连接

1、左外连接(left join)以左表为主表(查询全部), 右表为辅表(没有的显示null)

2、右外连接(right join)

三、全连接(full join)

四、联合(合并)查询(union)

MySQL 不识别 FULL join,所以可以通过 union 来实现

五、区别分析

外连接:外连不但返回符合连接和查询条件的数据行,还返回不符合条件的一些行。外连接分三类:左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)和全外连接(FULL OUTER JOIN)

三者的共同点是都返回符合连接条件和查询条件(即:内连接)的数据行。不同点如下:

左外连接:还返回左表中不符合连接条件单符合查询条件的数据行。

右外连接:还返回右表中不符合连接条件单符合查询条件的数据行。

全外连接:还返回左表中不符合连接条件单符合查询条件的数据行,并且还返回右表中不符合连接条件单符合查询条件的数据行。全外连接实际是上左外连接和右外连接的数学合集(去掉重复),即“全外 = 左外 UNION 右外”。

说明:左表就是在“(LEFT OUTER JOIN)”关键字左边的表。右表当然就是右边的了。在三种类型的外连接中,OUTER 关键字是可省略的。

六、SQL查询的基本原理

单表查询:根据WHERE条件过滤表中的记录,形成中间表(这个中间表对用户是不可见的);然后根据SELECT的选择列选择相应的列进行返回最终结果。

两表连接查询:对两表求积(笛卡尔积)并用ON条件和连接连接类型进行过滤形成中间表;然后根据WHERE条件过滤中间表的记录,并根据SELECT指定的列返回查询结果。

多表连接查询:先对第一个和第二个表按照两表连接做查询,然后用查询结果和第三个表做连接查询,以此类推,直到所有的表都连接上为止,最终形成一个中间的结果表,然后根据WHERE条件过滤中间表的记录,并根据SELECT指定的列返回查询结果。理解SQL查询的过程是进行SQL优化的理论依据。

引擎

MySQL存储引擎MyISAM与InnoDB区别

常用的存储引擎有以下:

  • Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
  • MyISAM引擎(原本MySQL的默认引擎):不提供事务的支持,也不支持行级锁和外键。
  • MEMORY引擎:所有的数据都在内存中,数据的处理速度快,但是安全性不高。

MyISAM与InnoDB区别

InnodbMyISAM
存储结构每张表都保存在同一个数据文件中每张表被存放在三个文件:表定义文件、数据文件、索引文件
数据和索引存储方式数据和索引是集中存储的,查询时做到覆盖索引会非常高效数据和索引是分开存储的,索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据
记录存储顺序按主键大小有序插入按记录插入顺序保存
索引聚簇索引非聚簇索引
索引的实现方式B+树索引,Innodb 是索引组织表B+树索引,myisam 是堆表
全文索引不支持支持
哈希索引支持不支持
外键支持不支持
事务支持不支持
锁粒度(锁是避免资源争用的一个机制,MySQL锁对用户几乎是透明的)行级锁定、表级锁定,锁定力度越小并发能力越高表级锁定
SELECTMyISAM更优
select count(*)myisam更快,因为myisam内部维护了一个计数器,可以直接调取。
INSERT、UPDATE、DELETEInnoDB更优
存储引擎选择

MyISAM:适用于管理非事务表,它提供高速存储和检索, 以及全文搜索能力的场景。比如博客系统、新闻门户网站。

InnoDB:适用于更新操作频繁,或者要保证数据的完整性,并发量高,支持事务和外键的场景。比如OA自动化办公系统。

如果没有特别的需求,使用默认的Innodb即可。

索引

什么是索引?

索引是一种数据结构,是数据库管理系统中一个排序的数据结构,以协助快速查询数据库表中数据。索引的实现通常使用B+树或hash表。

更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。

索引有哪些优缺点?

索引的优点

  • 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
  • 通过使用索引,可以在查询的过程中,使用优化器,提高系统的性能。

索引的缺点

  • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/删/改的执行效率;
  • 空间方面:索引需要占物理空间。
索引有哪几种类型?

主键索引:数据列不允许重复,不允许为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);创建组合索引

全文索引:是目前搜索引擎使用的一种关键技术,MyISAM存储引擎才有全文索引。

  • 可以通过ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引
索引的数据结构(B+树,Hash)

在MySQL中使用较多的索引有Hash索引B+树索引等,索引的数据结构和具体存储引擎的实现有关,而我们经常使用的InnoDB存储引擎,默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录等值查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择B+树索引。

1)B+树索引

MySQL通过存储引擎存取数据,基本上90%的人用的就是InnoDB了,按照实现方式分,InnoDB的索引类型目前只有两种:BTREE(B树)索引和HASH索引。B树索引是MySQL数据库中使用最频繁的索引类型,基本所有存储引擎都支持BTree索引。通常我们说的索引不出意外指的就是(B树)索引(实际是用B+树实现的,因为在查看表索引时,mysql一律打印BTREE,所以简称为B树索引)

B+树数据结构

图片

众所周知,一颗传统的M阶B+树需要满足以下几个要求:

  • 从根节点到叶节点的所有路径都具有相同的长度
  • 所有数据信息都存储在叶子节点,非叶子节点仅作为叶节点的索引存在
  • 叶子节点通过指针连在一起
  • 根节点至少拥有两个子树
  • 每个树节点最多拥有M个子树
  • 每个树节点(除了根节点)拥有至少M/2个子树

B+树是为了磁盘及其他存储辅助设备而设计的一种平衡查找树(不是二叉树),在B+树中,所有记录的节点按大小顺序存放在同一层的叶节点中,各叶子节点用指针进行连接,而B+树索引本质上就是B+树在数据库中的实现,与纯粹的B+树数据结构还是有点区别。

B+树与B+树索引的区别如下:

B+树B+树索引
存储位置内存磁盘
扇出率
并发控制可以不考虑需考虑
分裂方向不需要考虑向左、向右

B+树的一些特性:

1、B+树中的B不是代表的二叉(Binary) ,而是代表平衡(Balance),因为B+树是从最早的平衡二叉树演化而来,但是B+树不是一个二叉树。

2、B+树是为磁盘或其他直接存取辅助设备设计的一种平衡查找树,在B+树中,所有的记录节点都是按照键值大小顺序存在同一层的叶子节点,由叶子节点指针进行相连。

3、B+树在数据库中的特点就是高扇出,因此在数据库中B+树的高度一般都在24层,这也就是说查找一个键值记录时,最多只需要2到4次IO,当前的机械硬盘每秒至少可以有100次IO,24次IO意味着查询时间只需要0.02~0.04秒。

4、B+树索引并不能找到一个给定键值的具体行,B+树索引能找到的只是被查找的键值所在行的页,然后数据库把页读到内存,再内存中进行查找,最后找到要查找的数据。

5、数据库中B+树索引可以分为,聚簇索引和非聚簇索引,但是不管是聚簇索引还是非聚簇索引,其内部都是B+树实现的,即高度是平衡的,叶子节点存放着所有的数据,聚簇索引和非聚簇索引不同的是,叶子节点是否存储的是一整行信息。每张表只能有一个聚簇索引。

6、B+树的每个数据页(叶子节点)是通过一个双向链表进行链接,数据页上的数据的顺序是按照主键顺序存储的。

数据库为什么使用B+树而不是B树
  • B+树的叶子节点存储了所有的数据,非叶子节点中存储的是比较关键字。而B树所有的节点都会存储数据。B+树的叶子节点之间存在一个指针连接,B树不存在指针连接。B+树这种设计结构能带来什么好处呢?B+树所有的数据都存储在叶子节点,那么顺着叶子节点从左往右即可完成对数据的遍历,极大了简化了排序操作。这也是mysql设计索引是采用B+树的原因,不仅仅能方便查找,而且有助于排序,在mysql的索引中叶子节点之间数双向链表可正反遍历,更加灵活;
  • B树只适合随机检索,而B+树同时支持随机检索和顺序检索
  • B+树空间利用率更高,可减少I/O次数,磁盘读写代价更低。一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗。B+树的内部结点并没有指向关键字具体信息的指针,只是作为索引使用,其内部结点比B树小,盘块能容纳的结点中关键字数量更多,一次性读入内存中可以查找的关键字也就越多,相对的,IO读写次数也就降低了。而IO读写次数是影响索引检索效率的最大因素;
  • B+树的查询效率更加稳定。B树搜索有可能会在非叶子结点结束,越靠近根节点的记录查找时间越短,只要找到关键字即可确定记录的存在,其性能等价于在关键字全集内做一次二分查找。而在B+树中,顺序检索比较明显,随机检索时,任何关键字的查找都必须走一条从根节点到叶节点的路,所有关键字的查找路径长度相同,导致每一个关键字的查询效率相当。
  • B-树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。B+树的叶子节点使用指针顺序连接在一起,只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作。
  • 增删文件(节点)时,效率更高。因为B+树的叶子节点包含所有关键字,并以有序的链表结构存储,这样可很好提高增删效率。
索引算法有哪些?

索引算法有 BTree算法和Hash算法

BTree算法

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

-- 只要它的查询条件是一个不以通配符开头的常量
select * from user where name like 'jack%';
-- 如果一通配符开头,或者没有使用常量,则不会使用索引,例如:
select * from user where name like '%jack';

Hash算法

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

创建索引的原则?索引设计的原则?

索引虽好,但也不是无限制的使用,最好符合以下几个原则

  1. 为常作为查询条件的字段建立索引,where子句中的列,或者连接子句中指定的列
  2. 为经常需要排序、分组操作的字段建立索引
  3. 更新频繁字段不适合创建索引
  4. 不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
  5. 对于定义为text、image和bit的数据类型的列不要建立索引
  6. 最左前缀原则,就是最左边的优先。指的是联合索引中,优先走最左边列的索引。对于多个字段的联合索引,如 index(a,b,c) 联合索引,则相当于创建了 a 单列索引,(a,b)联合索引,和(a,b,c)联合索引(但并不是建立了多个索引树)。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的顺序可以任意调整。使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
  7. 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值
  8. 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长
什么情况使用了索引,查询还是慢
  • 索引全表扫描 use index
  • 索引过滤性不好
  • 频繁回表的开销
MySQL使用自增主键的好处
  1. 自增主键按顺序存放,增删数据速度快,对于检索非常有利;
  2. 数字型,占用空间小,易排序;
  3. 使用整形才可以使用AUTO_INCREAMENT,不用担心主键重复问题。
什么是聚簇索引?何时使用聚簇索引与非聚簇索引
  • 聚簇索引:将数据与索引放到了一块,索引结构的叶子节点存储了行数据,找到索引也就找到了数据
  • 非聚簇索引:将数据与索引分开存储,索引结构的叶子节点存储的是行数据的地址

聚簇索引的优点

  • 数据访问更快。聚族索引将索引和数据保存在同一个B+树中,因此从聚族索引中获取数据通常比非聚族索引中查找更快。
  • 当你需要取出一定范围内的数据时,用聚簇索引也比用非聚簇索引好。
  • 使用覆盖索引扫描的查询可以直接使用节点中的主键值。

聚簇索引的缺点

  • 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列作为主键。
  • 更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。
  • 通过辅助索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。

几个概念

  • 对于普通索引,如 name 字段,则需要根据 name 字段的索引树(非聚簇索引)找到叶子节点对应的主键,然后再通过主键去主键索引树查询一遍,才可以得到要找的记录,这就叫回表查询。先定位主键值,再定位行记录,它的性能较扫描一遍索引树的效率更低
  • InnoDB的行锁是建立在索引的基础之上的,行锁锁的是索引,不是数据,所以提高并发写的能力要在查询字段添加索引
  • 主索引和辅助索引:主索引就是主键索引,辅助索引就是根据业务需要,自己设置的普通的非主键的索引。这个在Myisam里面区别不大,但是在Innodb的时候差别很大
  • 聚簇索引:Innodb的主索引采用的是聚簇索引,一个表只能有1个聚簇索引,因为表数据存储的物理位置是唯一的。聚簇索引的value存的就是真实的数据,不是数据的地址。主索引树里面包含了真实的数据。key是主键值,value值就是data,key值按照B+树的规则分散排布的叶子节点。
  • 非聚簇索引:Myisam的主索引和辅助索引都采用的是非聚簇索引,索引和表数据是分离的,索引的value值存储的是行数据的地址。
  • Innodb的索引:主索引采用聚簇索引,叶子节点的value值,直接存储的真实的数据。辅助索引是非聚簇索引,value值指向主索引的位置。所以在Innodb中,根据辅助索引查询值需要遍历2次B+树,同时主键的长度越短越好,越短辅助索引的value值就越小。Innodb中根据主键进行范围查询,会特别快。
  • Myisam的索引:主索引和辅助索引都是非聚簇索引
  • B+树:不管是什么索引,在mysql中的数据结构都是B+树的结构,可以充分利用数据块,来减少IO查询的次数,提升查询的效率。一个数据块data里面,存储了很多个相邻key的value值,所有的非叶子节点都不存储数据,都是指针。
  • mysql采用B+树的优点:IO读取次数少(每次都是页读取),范围查找更快捷(相邻页之间有指针)
联合索引是什么?组合索引是什么?

MySQL可以使用多个字段组合建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。

联合索引数据结构和实现原理,使用联合索引是怎么进行查询的

假设,我们对(a,b)字段建立索引,那么入下图所示
在这里插入图片描述

如上图所示他们是按照a来进行排序,在a相等的情况下,才按b来排序。

因此,我们可以看到a是有序的1,1,2,2,3,3。而b是一种全局无序,局部相对有序状态!什么意思呢?

从全局来看,b的值为1,2,1,4,1,2,是无序的,因此直接执行b = 2这种查询条件没有办法利用索引。

从局部来看,当a的值确定的时候,b是有序的。例如a = 1时,b值为1,2是有序的状态。当a=2时候,b的值为1,4也是有序状态。因此,你执行a = 1 and b = 2是a,b字段能用到索引的。而你执行a > 1 and b = 2时,a字段能用到索引,b字段用不到索引。因为a的值此时是一个范围,不是固定的,在这个范围内b值不是有序的,因此b字段用不上索引。

综上所示,最左匹配原则,在遇到范围查询的时候,就会停止匹配。

什么是最左前缀原则?什么是最左匹配原则?为什么需要注意联合索引中的顺序?
  • 最左前缀原则,就是最左边的优先。指的是联合索引中,优先走最左边列的索引。对于多个字段的联合索引,如 index(a,b,c) 联合索引,则相当于创建了 a 单列索引,(a,b)联合索引,和(a,b,c)联合索引(但并不是建立了多个索引树)。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的顺序可以任意调整。
  • =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。
  • 如果建立的索引顺序是 (a,b) 那么直接采用 where b = 5 这种查询条件是无法利用到索引的,这一条最能体现最左匹配的特性。

事务

什么是数据库事务?

事务是逻辑上的一组操作,要么都执行,要么都不执行。

事务的四大特性(ACID)介绍一下?

关系性数据库需要遵循ACID规则,具体内容如下:

特性说明
原子性 Atomic事务是最小的执行单位,不允许分割。事务包含的所有操作要么全部成功,要么全部失败回滚。
一致性 Consistency事务执行之前和执行之后都必须处于一致性状态。举例:拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
隔离性 Isolation隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间是相互隔离的。数据库规定了多种事务隔离级别,不同的隔离级别对应不同的干扰程度。隔离级别越高,数据一致性越好,但并发性越差。
持久性 Durability持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下,也不会丢失提交事务的操作。
什么是脏读?不可重复读?幻读?
  • 脏读(Dirty Read):一个事务读取到另外一个事务未提交的数据。举例:一个事务1读取了被另一个事务2修改但还未提交的数据。由于某种异常事务2回滚,则事务1读取的是无效数据。

  • 不可重复读(Non-repeatable read):一个事务读取同一条记录2次,得到的结果不一致。这可能是两次查询过程中间,另一个事务更新了这条记录。

  • 幻读(Phantom Read):幻读发生在两个完全相同的查询,得到的结果不一致。这可能是两次查询过程中间,另一个事务增加或者减少了行记录。

不可重复度和幻读区别

不可重复读的重点是修改,幻读的重点在于新增或者删除。

什么是事务的隔离级别?MySQL的默认的隔离级别是什么?

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

隔离级别脏读不可重复读幻读
READ-UNCOMMITTED
READ-COMMITTED×
REPEATABLE-READ××
SERIALIZABLE×××

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

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

这里需要注意的是:MySQL 默认采用的 REPEATABLE_READ隔离级别,Oracle 默认采用的 READ_COMMITTED隔离级别

事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取已提交),但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重复读)**并不会有任何性能损失。

InnoDB 存储引擎在 分布式事务 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。

MySQL数据库可重复读隔离级别是怎么实现的,MVCC并发版本控制原理

MySQL可重复读是通过MVCC实现的

MVCC(Multi Version Concurrency Control的简称),代表多版本并发控制。与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的优势:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能

MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

InnoDB MVCC 实现原理

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

DATA_TRX_ID

记录最近更新这条行记录的事务 ID,大小为 6 个字节

DATA_ROLL_PTR

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

DB_ROW_ID

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

增删改查

假设初始版本号为1:

INSERT

insert into user (id,name) values (1,'Tom');
idnamecreate_versiondelete_version
1Tom1

下面模拟一下文章开头的场景:

SELECT (事务A)

select * from user where id = 1;

此时读到的版本号为1

UPDATE(事务B)

update user set name = 'Jerry' where id = 1;

在更新操作的时候,该事务的版本号在原来的基础上加1,所以版本号为2。先将要更新的这条数据标记为已删除,并且删除的版本号是当前事务的版本号,然后插入一行新的记录

idnamecreate_versiondelete_version
1Tom12
1Jerry2

SELECT (事务A)

此时事务A再重新读数据:

select * from user where id = 1;

由于事务A一直没提交,所以此时读到的版本号还是为1,所以读到的还是Tom这条数据,也就是可重复读

DELETE

delete from user where id = 1;

在删除操作的时候,该事务的版本号在原来的基础上加1,所以版本号为3删除时,将当前版本号作为删除版本号

idnamecreate_versiondelete_version
1Jerry23

对MySQL的锁了解吗

当数据库有并发事务的时候,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,锁机制就是这样的一个机制。

隔离级别与锁的关系

在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突

在Read Committed级别下,读操作需要加共享锁,在语句执行完以后释放共享锁;

在Repeatable Read级别下,读操作需要加共享锁,事务执行完毕后才释放共享锁。

在SERIALIZABLE级别下,是限制性最强的隔离级别,该级别下锁定整个范围的键,并一直持有锁,直到事务完成。

按照锁的粒度分数据库锁有哪些?

在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。

MyISAM和InnoDB存储引擎使用的锁:

  • MyISAM采用表级锁(table-level locking)。
  • InnoDB支持行级锁(row-level locking)和表级锁,默认采用行级锁

行级锁,表级锁和页级锁对比

行级锁 行级锁是MySQL中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。

特点:锁定粒度最小,对当前操作的行记录加锁,发生锁冲突的概率最低,并发度也最高;加锁开销大,加锁慢;会出现死锁;

  1. 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持。

  2. 间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。

  3. 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持。

表级锁 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。

特点:锁定粒度大,对当前操作的整张表加锁,发出锁冲突的概率最高,并发度最低;加锁开销小,加锁快;不会出现死锁;

页级锁 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

共享锁和排他锁的区别

共享锁

共享锁 share lock 又称读锁 read lock,简称S锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。

如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获得共享锁的事务只能读数据,不能修改数据

读取为什么要加读锁呢:防止数据在被读取的时候被别的线程加上写锁

使用方式:在需要执行的语句后面加上 for update就可以了

排他锁

排他锁 exclusive lock 又称写锁 writer lock,简称X锁。排他锁是悲观锁的一种实现。

若事务T对数据A加上排他锁,则只允许事务T读取和修改数据A,其他任何事务都不能再对A加任何类型的锁,直到事务T释放X锁。排他锁会阻塞所有的排他锁和共享锁

数据库的乐观锁和悲观锁是什么?怎么实现的?

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

悲观锁:假定会发生并发冲突,每次去查询数据的时候都认为别人会修改,每次查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制

乐观锁:假设不会发生并发冲突,每次去查询数据的时候都认为别人不会修改,所以不会上锁,在修改数据的时候才把事务锁起来。实现方式:乐观锁一般会使用版本号机制或CAS算法实现

SQL优化

如何定位及优化SQL语句的性能问题

对于低性能的SQL语句的定位,最重要也是最有效的方法就是使用执行计划,MySQL提供了explain命令来查看语句的执行计划。我们知道,不管是哪种数据库,或者是哪种数据库引擎,在对一条SQL语句进行执行的过程中都会做很多相关的优化,对于查询语句,最重要的优化方式就是使用索引。而执行计划,就是显示数据库引擎对于SQL语句执行的详细情况,其中包含了是否使用索引,使用什么索引,使用的索引的相关信息等

执行计划包含的信息

id 由一组数字组成。表示一个查询中各个子查询的执行顺序;

  • id相同执行顺序由上至下。
  • id不同,id值越大优先级越高,越先被执行。
  • id为null时,表示一个合并结果集的操作的执行id为null,常出现在包含union等查询语句中。

select_type 每个子查询的查询类型,一些常见的查询类型。

idselect_typedescription
1SIMPLE不包含任何子查询或union查询
2PRIMARY包含子查询时最外层查询就显示为 PRIMARY
3SUBQUERY在select或where子句中出现的子查询
4DERIVEDfrom字句中出现的子查询
5UNIONunion连接的两个select查询,第一个查询是dervied派生表,除了第一个表外,第二个以后的表select_type都是union。
6UNION RESULT包含union的结果集,在union和union all语句中,因为它不需要参与查询,所以id字段为null
7dependent subquery与dependent union类似,表示这个subquery的查询要受到外部表查询的影响。
8dependent union与union一样,出现在union 或union all语句中,但是这个查询要受到外部查询的影响

table 显示的查询表名,如果查询使用了别名,那么这里显示的是别名。

type访问类型(非常重要,可以看到有没有走索引)

依次从好到差:system,const,eq_ref,ref,fulltext,ref_or_null,unique_subquery,index_subquery,range,index_merge,index,ALL。

除了all之外,其他的type都可以使用到索引,除了index_merge之外,其他的type只可以用到一个索引。

类型描述
system表中只有一行数据或者是空表,且只能用于myisam和memory表。如果是Innodb引擎表,type列在这个情况通常都是all或者index。
const使用唯一索引或者主键,返回记录是1行记录的等值where条件时,通常type是const。其他数据库也叫做唯一索引扫描。
eq_ref出现在要连接多个表的查询计划中,驱动表只返回一行数据,且这行数据是第二个表的主键或者唯一索引,且必须为not null,唯一索引和主键是多列时,只有所有的列都用作比较时才会出现eq_ref。
ref像eq_ref那样要求连接顺序,也没有主键和唯一索引的要求,只要使用相等条件检索时就可能出现,常见于普通索引的等值查找。或者多列主键、唯一索引中,使用第一个列之外的列作为等值查找也会出现,总之,返回数据不唯一的等值查找就可能出现。
fulltext全文索引检索,要注意,全文索引的优先级很高,若全文索引和普通索引同时存在时,mysql不管代价,优先选择使用全文索引。
ref_or_null与ref方法类似,只是增加了null值的比较。实际用的不多。
unique_subquery用于where中的in形式子查询,子查询返回不重复值唯一值。
index_subquery用于in形式子查询使用到了辅助索引或者in常数列表,子查询可能返回重复值,可以使用索引将子查询去重。
range索引范围扫描,常见于使用>,<,is null,between ,in ,like等运算符的查询中。
index_merge表示查询使用了两个以上的索引,最后取交集或者并集。常见and ,or的条件使用了不同的索引,官方排序这个在ref_or_null之后,但是实际上由于要读取多个索引,性能可能都不如range。
index索引全表扫描。把索引从头到尾扫一遍,常见于使用索引列就可以处理,不需要读取数据文件的查询、可以使用索引排序或者分组的查询。
ALL全表扫描数据文件

possible_keys 可能使用的索引,注意不一定会使用。查询涉及到的字段上若存在索引,则该索引将被列出来。当该列为 NULL时就要考虑当前的SQL是否需要优化了。

key 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL。

key_length 索引长度

ref 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值

rows 这里是执行计划中估算的扫描行数,不是精确值。

extra 的信息非常丰富,常见的有:

  1. Using index 使用覆盖索引
  2. Using where 使用了where子句来过滤结果集
  3. Using filesort 使用文件排序,使用非索引列进行排序时出现,非常消耗性能,尽量优化。
  4. Using temporary 使用了临时表

SQL优化的目标可以参考阿里开发手册

【推荐】SQL性能优化的目标:至少要达到 range 级别,要求是ref级别,如果可以是consts最好。
说明:
1) consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
2) ref 指的是使用普通的索引(normal index)。
3) range 对索引进行范围检索。
反例:explain表的结果,type=index,索引物理文件全扫描,速度非常慢,这个index级别比较range还低,与全表扫描是小巫见大巫。
SQL的生命周期?一条SQL查询语句是如何执行的

MySQL 的逻辑架构图

在这里插入图片描述

-- 比如,你有个最简单的表,表里只有一个 ID 字段,在执行下面这个查询语句时:
mysql> select * from T where ID=10;

MySQL的框架有几个组件, 各是什么作用? Server层和存储引擎层各是什么作用?

大体来说,MySQL 可以分为 Server 层和存储引擎层两部分。

Server 层包括连接器、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。

SQL执行流程,SQL的生命周期?

连接器

第一步,客户端与数据库server层的连接器进行连接。连接器负责跟客户端建立连接、获取权限、维持和管理连接。

查询缓存

连接建立完成后,会判断查询缓存是否开启,如果已经开启,会判断sql是select还是update/insert/delete,对于select,尝试去查询缓存,如果命中缓存直接返回数据给客户端, 如果缓存没有命中,或者没有开启缓存, 会进入到下一步分析器。

分析器

分析器进行词法分析和语法分析,分析器先会做“词法分析”,分析SQL中的字符串分别是什么,校验数据库表和字段是否存在,然后进行语法分析,判断SQL是否满足MySQL语法。

优化器

优化器对sql执行计划分析,得到最终执行计划,得到优化后的执行计划交给执行器。

优化器是在表里面有多个索引的时候,决定使用哪个索引,或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。

执行器

开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如果有权限,执行器调用存储引擎api执行sql,得到响应结果, 将结果返回给客户端,如果缓存是开启状态, 会更新缓存。

  1. 连接:应用服务器与数据库服务器建立一个连接

  2. 获得请求SQL:数据库进程拿到请求sql

  3. 查询缓存:如果查询命中缓存则直接返回结果

  4. 语法解析和预处理:

    首先通过mysql关键字将语句解析,会生成一个内部解析树,mysql解析器将对其解析,查看是否是有错误的关键字,关键字顺序是否正确;预处理器则是根据mysql的规则进行进一步的检查,检查mysql语句是否合法,如库表是否存在,字段是否存在,字段之间是否模棱两可等等,预处理器也会验证权限。

  5. 查询优化器:sql语句在优化器中转换成执行计划,一条sql语句可以有多种方式查询,最后返回的结果肯定是相同,但是不同的查询方式效果不同,优化器的作用就是:选择一种合适的执行计划。mysql是基于成本的优化器,他将预测执行此计划的成本,并选择成本最小的那条

  6. 执行计划,执行SQL:在解析和优化后,MySQL将生成查询对应的执行计划,由执行计划调用存储引擎的API来执行查询

  7. 将结果返回给客户端

  8. 关掉连接,释放资源

一条更新语句的执行流程又是怎样的呢?
-- 如果要将 ID=2 这一行的值加 1
mysql> update T set c=c+1 where ID=2;

你执行语句前要先连接数据库,这是连接器的工作。

前面我们说过,在一个表上有更新的时候,跟这个表有关的查询缓存会失效,所以这条语句就会把表 T 上所有缓存结果都清空。这也就是我们一般不建议使用查询缓存的原因。

接下来,分析器会通过词法和语法解析知道这是一条更新语句。优化器决定要使用 ID 这个索引。然后,执行器负责具体执行,找到这一行,然后更新。

与查询流程不一样的是,更新流程还涉及两个重要的日志模块,它们正是我们今天要讨论的主角:redo log(重做日志)和 binlog(归档日志)。

在 MySQL 里也有这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就用了类似酒店掌柜粉板的思路来提升更新效率。

而粉板和账本配合的整个过程,其实就是 MySQL 里经常说到的 WAL 技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。

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

如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,粉板写满了,又怎么办呢?这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。

write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。

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

要理解 crash-safe 这个概念,可以想想我们前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。

重要的日志模块:binlog

前面我们讲过,MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的粉板 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。

我想你肯定会问,为什么会有两份日志呢?

因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。

这两种日志有以下三点不同。

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
  2. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
常用SQL查询语句优化方法
  • 不要使用select * from t,用具体的字段列表代替“*”,使用星号会降低查询效率,如果数据库字段改变,可能出现不可预知隐患。
  • 应尽量避免在where子句中使用!=或<>操作符,避免在where子句中字段进行null值判断,存储引擎将放弃使用索引而进行全表扫描。
  • 避免使用左模糊,左模糊查询将导致全表扫描。
  • IN语句查询时包含的值不应过多,否则将导致全表扫描。
  • 为经常作为查询条件的字段,经常需要排序、分组操作的字段建立索引。
  • 在使用联合索引字段作为条件时,应遵循最左前缀原则。
  • OR前后两个条件都要有索引,整个SQL才会使用索引,只要有一个条件没索引整个SQL就不会使用索引。
  • 尽量用union all代替union,union需要将结果集合并后再进行唯一性过滤操作,这就会涉及到排序,增加大量的CPU运算,加大资源消耗及延迟。

数据库优化

数据库结构优化

一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果。

需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。

将字段很多的表分解成多个表

对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。

因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。

增加中间表

对于需要经常联合查询的表,可以建立中间表以提高查询效率。

通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。

增加冗余字段

设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。

表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。

注意:

冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。

大表怎么优化?某个表有近千万数据,CRUD比较慢,如何优化?分库分表是怎么做的?分表分库了有什么问题?有用到中间件么?他们的原理知道么?

当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:

  1. 限定数据的查询范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:当用户在查询订单历史的时候,我们可以控制在一个月的范围内;
  2. 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
  3. 缓存: 使用MySQL的缓存,另外对重量级、更新少的数据可以考虑使用应用级别的缓存;
  4. 分库分表

分库分表主要有垂直分表和水平分表

  1. 垂直分表:

    垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。如下图所示,这样来说大家应该就更容易理解了。

    适用场景:如果一个表中某些列常用,另外一些列不常用

    垂直拆分的优点:可以使得行数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。

    垂直拆分的缺点:主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。对于应用层来说,逻辑算法增加开发成本。此外,垂直分区会让事务变得更加复杂;

  2. 水平分表:

    保持数据表结构不变,通过某种策略进行存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。水平拆分可以支撑非常大的数据量。

    水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。

    水平拆分可以支持非常大的数据量。需要注意的一点是:水平分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库

    适用场景:支持非常大的数据量存储

    水平拆分优点:支持非常大的数据量存储

    水平拆分缺点:给应用增加复杂度,通常查询时需要多个表名,查询所有数据都需UNION操作;分片事务难以解决 ,跨库join性能较差,逻辑复杂。

《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。

下面补充一下数据库分片的两种常见方案

  • 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
  • 中间件代理: 在应用和数据库中间加了一个代理层。分片逻辑统一维护在中间件服务中。 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。
MySQL的主从复制原理以及流程

主从复制:将主数据库中的DDL和DML操作通过二进制日志(BINLOG)传输到从数据库上,然后将这些日志重新执行,从而使得从数据库的数据与主数据库保持一致。

主从复制的作用

  1. 高可用和故障切换:主数据库出现问题,可以切换到从数据库。
  2. 负载均衡:可以进行数据库层面的读写分离。
  3. 数据备份:可以在从数据库上进行日常备份。

复制过程

Binary log:主数据库的二进制日志

Relay log:从数据库的中继日志

第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。

第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,将这些事件写入到中继日志中。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。

第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。

读写分离有哪些解决方案?

读写分离是依赖于主从复制,而主从复制又是为读写分离服务的。主从复制要求slave不能写只能读

方案一

利用中间件来做代理,使用mysql-proxy代理,负责对数据库的请求识别出读还是写,并分发到不同的数据库中。

优点:直接实现读写分离和负载均衡,不用修改代码,数据库和应用程序弱耦合,master和slave用一样的帐号,mysql官方不建议实际生产中使用

缺点:降低性能, 不支持事务,代理存在性能瓶颈和可靠性风险增加。

方案二

使用AbstractRoutingDataSource+aop+annotation在dao层决定数据源。

如果采用了mybatis, 可以将读写分离放在ORM层,比如mybatis可以通过mybatis plugin拦截sql语句,所有的insert/update/delete都访问master库,所有的select 都访问salve库,这样对于dao层都是透明。plugin实现时可以通过注解或者分析语句是读写方法来选定主从库。

不过这样依然有一个问题, 也就是不支持事务, 所以我们还需要重写一下DataSourceTransactionManager, 将read-only的事务扔进读库, 其余的有读有写的扔进写库。

方案三

使用AbstractRoutingDataSource+aop+annotation在service层决定数据源,可以支持事务

缺点:类内部方法通过this.xx()方式相互调用时,aop不会进行拦截,需进行特殊处理。

大数据量处理

大批量数据删除,怎么一次删除100万条数据

方法一:

在MySQL数据库使用中,有的表存储数据量比较大,达到每天三百万条记录左右,此表中建立了三个索引,这些索引都是必须的,其他程序要使用。由于要求此表中的数据只保留当天的数据,所以每当在凌晨的某一时刻当其他程序处理万其中的数据后要删除该表中昨天以及以前的数据,使用delete删除表中的上百万条记录时,MySQL删除速度非常缓慢每一万条记录需要大概4分钟左右,这样删除所有无用数据要达到八个小时以上,这是难以接受的。

查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的,于是删除掉其中的两个索引后测试,发现此时删除速度相当快,一百万条记录在一分钟多一些,可是这两个索引其他模块在每天一次的数据整理中还要使用,于是想到了一个折中的办法:

在删除数据之前删除这两个索引,此时需要三分钟多一些,然后删除其中无用数据,此过程需要不到两分钟,删除完成后重新创建索引,因为此时数据库中的数据相对较少,约三四十万条记录(此表中的数据每小时会增加约十万条),创建索引也非常快,约十分钟左右。这样整个删除过程只需要约15分钟。对比之前的八个小时,大大节省了时间。

删除大表的部分数据,通常采用以下步骤:

删除大表的多行数据时,会超出innod block table size的限制,最小化的减少锁表时间的方案是:

1、选择不需要删除的数据,并把它们存在一张相同结构的空表里

2、重命名原始表,并给新表命名为原始表名

3、删掉原始表

方法二:

分批删除,如果你要删除一个表里面的前 10000 行数据,有以下三种方法可以做到:

  • 第一种,直接执行 delete from T limit 10000;
  • 第二种,在一个连接中循环执行 20 次 delete from T limit 500;
  • 第三种,在 20 个连接中同时执行 delete from T limit 500。

方案一,单个语句占用锁的时间较长,会导致其他客户端等待资源时间较长。

方案二,串行化执行,将相对长的事务分成多次相对短的事务,则每次事务占用锁的时间相对较短,其他客户端在等待相应资源的时间也较短。这样的操作,同时也意味着将资源分片使用(每次执行使用不同片段的资源),可以提高并发性。

方案三,人为制造锁竞争,加剧并发。

方案二相对比较好,具体还要结合实际业务场景。

MySQL大数据量分页查询方法及其优化

limit偏移量不变,随着查询记录量越来越大,所花费的时间也会越来越多。

limit查询记录数不变,随着查询偏移的增大,尤其查询偏移大于10万以后,查询时间急剧增加。

原因分析
select * from user where sex = 1 limit 100,10

由于 sex 列是索引列,MySQL会走 sex 这棵索引树,命中 sex=1 的数据。

然后又由于非聚簇索引中存储的是主键 id 的值,且查询语句要求查询所有列,所以这里会发生一个回表的情况,在命中 sex 索引树中值为1的数据后,拿着它叶子节点上的值也就是主键 id 的值去主键索引树上查询这一行其他列(name、sex)的值,最后返回到结果集中,这样第一行数据就查询成功了。

最后这句 SQL 要求limit 100, 10,也就是查询第101到110个数据,但是 MySQL 会查询前110行,然后将前100行抛弃,最后结果集中就只剩下了第101到110行,执行结束。

小结一下,在上述的执行过程中,造成 limit 大偏移量执行时间变久的原因有:

  • limit a, b会查询前a+b条数据,然后丢弃前a条数据

MySQL数据库的查询优化器是采用了基于代价的方式,而查询代价的估算是基于CPU代价IO代价。如果MySQL在查询代价估算中,认为全表扫描方式比走索引扫描的方式效率更高的话,就会放弃索引,直接全表扫描。

优化方式

t5表有200万数据,id为主键,text为普通索引

使用覆盖索引

如果一条SQL语句,通过索引可以直接获取查询的结果,不再需要回表查询,就称这个索引为覆盖索引。

在MySQL数据库中使用explain关键字查看执行计划,如果extra这一列显示Using index,就表示这条SQL语句使用了覆盖索引。

让我们来对比一下使用了覆盖索引,性能会提升多少吧。

没有使用覆盖索引

select * from t5 order by text limit 1000000, 10;

这次查询花了3.690秒,让我们看一下使用了覆盖索引优化会提升多少性能吧。

使用了覆盖索引

select id, `text` from t5 order by text limit 1000000, 10;

从上面的对比中,超大分页查询中,使用了覆盖索引之后,花了0.201秒,而没有使用覆盖索引花了3.690秒,提高了18倍多,这在实际开发中,就是一个大的性能优化了。

子查询优化

因为实际开发中,用SELECT查询一两列操作是非常少的,因此上述的覆盖索引的适用范围就比较有限。

所以我们可以通过把分页的SQL语句改写成子查询的方法获得性能上的提升。

select * from t5 where id>=(select id from t5 order by text limit 1000000, 1) limit 10;

其实使用这种方法,提升的效率和上面使用了覆盖索引基本一致。

但是这种优化方法也有局限性:

  • 这种写法要求主键ID必须是连续的
  • Where子句不允许再添加其他条件
延迟关联

和上述的子查询做法类似,我们可以使用JOIN,先在索引列上完成分页操作,然后再回表获取所需要的列。

select a.* from t5 a inner join (select id from t5 order by text limit 1000000, 10) b on a.id=b.id;

MVCC

read view

  • 当前读
    像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
  • 快照读
    像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;读取的是可重复读隔离级别下第一次执行sql读取的数据。

隐式字段

事务id:DB_TRX_ID

隐藏主键:DB_ROW_ID

回滚指针:DB_ROLL_PTR 指向版本链上一个版本的数据

undoLog

行数据的历史记录

redis

为什么redis基于单线程还是很快

  1. I/0多路复用

    是指利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/0多路复用都是采用的epol模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。

  2. Redis网络模型

    就是使用I/0多路复用结合事件的处理器来应对多个Socket请求连接应答处理器命令回复处理器,在Redis6.0之后,为了提升更好的性能,使用了多线程来处理回复事件命令请求处理器,在Redis6.0之后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

Redis的过期键的删除策略

我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一段时间,对一些key进行检查,删除里面过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以使得CPU和内存资源达到最优的平衡效果。

Redis中同时使用了惰性过期和定期过期两种过期策略。通过配合使用这两种过期键的删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。

Redis的内存淘汰策略有哪些

Redis的内存淘汰策略是指在Redis服务器用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的键空间选择性移除

  • allkeys-lru(常用):当内存不足以容纳新写入数据时,在全局键空间中,移除最近最少使用的key。
  • allkeys-random(随机):当内存不足以容纳新写入数据时,在全局键空间中,随机移除某个key。
  • noeviction(直接报错):当内存不足以容纳新写入数据时,新写入操作会报错。

设置过期时间的键空间选择性移除

  • volatile-lru(常用):当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

注意

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

如何保证双写一致性

  1. 延迟双删(先删缓存,再删库,再删缓存。做不到绝对一致性)
  2. redission分布式锁对数据进行更新(强一致读写锁,排它锁)
  3. 异步通知,
    • 通过mq更新redis缓存(最终一致性)代码耦合
    • canal 中间件,利用mysql binlog更新缓存,无侵入

redis主从同步

在这里插入图片描述

repl_backlog原理

master怎么知道slave与自己的数据差异在哪里呢?

这就要说到全量同步时的repl_baklog文件了。

这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。

repl_baklog中会记录Redis处理过的命令日志及offset,包括master当前的offset,和slave已经拷贝到的offset:

在这里插入图片描述

简述全量同步和增量同步区别?

  • 全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。
  • 增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave

什么时候执行全量同步?

  • slave节点第一次连接master节点时
  • slave节点断开时间太久,repl_baklog中的offset已经被覆盖时

什么时候执行增量同步?

  • slave节点断开又恢复,并且在repl_baklog中能找到offset时

哨兵模式

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:

  • 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线

  • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

集群故障恢复原理

一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:

  • 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
  • 然后判断slave节点的slave-priority值(replica-priority 100),越小优先级越高,如果是0则永不参与选举
  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
  • 最后是判断slave节点的运行id大小,越小优先级越高。

分片集群

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 海量数据存储问题
  • 高并发写的问题

分片集群特征:

  • 集群中有多个master,每个master保存不同数据

  • 每个master都可以有多个slave节点

  • master之间通过ping监测彼此健康状态

分片集群原理:

插槽原理

Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,当需要向redis集群中执行一个命令时,比如set num 123 ,这个时候,redis集群会根据发送过来的key 先执行crc16算法得出一个值,并再对16384求余数,那么余数值永远都在0~16383之间,这样得出一个值比如:num—>98,98刚好在0-5601这个插槽范围,而这些插槽在节点1号中。那么这个命令就可以直接在节点1号执行了。

MongoDB

influxDB

步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave

什么时候执行全量同步?

  • slave节点第一次连接master节点时
  • slave节点断开时间太久,repl_baklog中的offset已经被覆盖时

什么时候执行增量同步?

  • slave节点断开又恢复,并且在repl_baklog中能找到offset时

哨兵模式

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:

  • 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线

  • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

集群故障恢复原理

一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:

  • 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
  • 然后判断slave节点的slave-priority值(replica-priority 100),越小优先级越高,如果是0则永不参与选举
  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
  • 最后是判断slave节点的运行id大小,越小优先级越高。

分片集群

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 海量数据存储问题
  • 高并发写的问题

分片集群特征:

  • 集群中有多个master,每个master保存不同数据

  • 每个master都可以有多个slave节点

  • master之间通过ping监测彼此健康状态

分片集群原理:

插槽原理

Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,当需要向redis集群中执行一个命令时,比如set num 123 ,这个时候,redis集群会根据发送过来的key 先执行crc16算法得出一个值,并再对16384求余数,那么余数值永远都在0~16383之间,这样得出一个值比如:num—>98,98刚好在0-5601这个插槽范围,而这些插槽在节点1号中。那么这个命令就可以直接在节点1号执行了。

MongoDB

influxDB

;