面试-数据库

1. 什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员?

  • 数据库 : 数据库(DataBase 简称 DB)就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。
  • 数据库管理系统 : 数据库管理系统(Database Management System 简称 DBMS)是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。
  • 数据库系统 : 数据库系统(Data Base System,简称 DBS)通常由软件、数据库和数据管理员(DBA)组成。
  • 数据库管理员 : 数据库管理员(Database Administrator, 简称 DBA)负责全面管理和控制数据库系统。

2. 什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性?

  • 元组:元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。
  • :码就是能唯一标识实体的属性,对应表中的列。
  • 候选码:若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。
  • 主码 : 主码也叫主键。主码是从候选码中选出来的。 一个实体集中只能有一个主码,但可以有多个候选码。
  • 外码 : 外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。
  • 主属性:候选码中出现过的属性称为主属性。比如关系 工人(工号,身份证号,姓名,性别,部门). 显然工号和身份证号都能够唯一标示这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。
  • 非主属性: 不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。

3. 什么是 ER 图?

我们做一个项目的时候一定要试着画 ER 图来捋清数据库设计,这个也是面试官问你项目的时候经常会被问到的。

ER 图 全称是 Entity Relationship Diagram(实体联系图),提供了表示实体类型、属性和联系的方法。

ER 图由下面 3 个要素组成:

  • 实体:通常是现实世界的业务对象,当然使用一些逻辑对象也可以。比如对于一个校园管理系统,会涉及学生、教师、课程、班级等等实体。在 ER 图中,实体使用矩形框表示。
  • 属性:即某个实体拥有的属性,属性用来描述组成实体的要素,对于产品设计来说可以理解为字段。在 ER 图中,属性使用椭圆形表示。
  • 联系:即实体与实体之间的关系,在 ER 图中用菱形表示,这个关系不仅有业务关联关系,还能通过数字表示实体之间的数量对照关系。例如,一个班级会有多个学生就是一种实体间的联系。

下图是一个学生选课的 ER 图,每个学生可以选若干门课程,同一门课程也可以被若干人选择,所以它们之间的关系是多对多(M: N)。另外,还有其他两种实体之间的关系是:1 对 1(1:1)、1 对多(1: N)。

4. 数据库范式了解吗?

数据库范式有 3 种:

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

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

5. 主键和外键有什么区别?

  • **主键(主码)**:主键用于唯一标识一个元组,不能有重复,不允许为空。一个表只能有一个主键。
  • **外键(外码)**:外键用来和其他表建立联系用,外键是另一表的主键,外键是可以有重复的,可以是空值。一个表可以有多个外键。

6. drop、delete 与 truncate 区别?

6.1 用法不同

  • drop(丢弃数据): drop table 表名 ,直接将表都删除掉,在删除表的时候使用。
  • truncate (清空数据) : truncate table 表名 ,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。
  • delete(删除数据) : delete from 表名 where 列名=值,删除某一行的数据,如果不加 where 子句和truncate table 表名作用类似。

6.2 属于不同的数据库语言

truncatedrop 属于 DDL(数据定义语言)语句,操作立即生效,原数据不放到 rollback segment 中,不能回滚,操作不触发 trigger。而 delete 语句是 DML (数据库操作语言)语句,这个操作会放到 rollback segment 中,事务提交之后才生效。

DML 语句和 DDL 语句区别:

  • DML 是数据库操作语言(Data Manipulation Language)的缩写,是指对数据库中表记录的操作,主要包括表记录的插入、更新、删除和查询,是开发人员日常使用最频繁的操作。
  • DDL (Data Definition Language)是数据定义语言的缩写,简单来说,就是对数据库内部的对象进行创建、删除、修改的操作语言。它和 DML 语言的最大区别是 DML 只是对表内部数据的操作,而不涉及到表的定义、结构的修改,更不会涉及到其他对象。DDL 语句更多的被数据库管理员(DBA)所使用,一般的开发人员很少使用。
  • 另外,由于select不会对表进行破坏,所以有的地方也会把select单独区分开叫做数据库查询语言 DQL(Data Query Language)。

6.3 执行速度不同

一般来说:drop > truncate > delete

  • delete命令执行的时候会产生数据库的binlog日志,而日志记录是需要消耗时间的,但是也有个好处方便数据回滚恢复。
  • truncate命令执行的时候不会产生数据库日志,因此比delete要快。除此之外,还会把表的自增值重置和索引恢复到初始大小等。
  • drop命令会把表占用的空间全部释放掉。

7. 数据库设计通常分为哪几步?

  1. 需求分析 : 分析用户的需求,包括数据、功能和性能需求。
  2. 概念结构设计 : 主要采用 E-R 模型进行设计,包括画 E-R 图。
  3. 逻辑结构设计 : 通过将 E-R 图转换成表,实现从 E-R 模型到关系模型的转换。
  4. 物理结构设计 : 主要是为所设计的数据库选择合适的存储结构和存取路径。
  5. 数据库实施 : 包括编程、测试和试运行
  6. 数据库的运行和维护 : 系统的运行与数据库的日常维护。

8. NoSQL

8.1 NoSQL是什么?

  • 概念:
    • NoSQL(Not Only SQL 的缩写)泛指非关系型的数据库,主要针对的是键值对、文档以及图形类型数据存储。并且,NoSQL 数据库天生支持分布式,数据冗余和数据分片等特性,旨在提供可扩展的高可用高性能数据存储解决方案。
  • NoSQL 能否存储关系型数据?
    • NoSQL 数据库可以存储关系型数据,它们与关系型数据库的存储方式不同。
  • NoSQL 数据库代表:HBase、Cassandra、MongoDB、Redis。

8.2 SQL 和 NoSQL 有什么区别?

SQL 数据库NoSQL 数据库
数据存储模型结构化存储,具有固定行和列的表格非结构化存储。文档:JSON 文档,键值:键值对,宽列:包含行和动态列的表,图:节点和边
发展历程开发于 1970 年代,重点是减少数据重复开发于 2000 年代后期,重点是提升可扩展性,减少大规模数据的存储成本
例子Oracle、MySQL、Microsoft SQL Server、PostgreSQL文档:MongoDB、CouchDB,键值:Redis、DynamoDB,宽列:Cassandra、 HBase,图表:Neo4j、 Amazon Neptune、Giraph
ACID 属性提供原子性、一致性、隔离性和持久性 (ACID) 属性通常不支持 ACID 事务,为了可扩展、高性能进行了权衡,少部分支持比如 MongoDB 。不过,MongoDB 对 ACID 事务 的支持和 MySQL 还是有所区别的。
性能性能通常取决于磁盘子系统。要获得最佳性能,通常需要优化查询、索引和表结构。性能通常由底层硬件集群大小、网络延迟以及调用应用程序来决定。
扩展垂直(使用性能更强大的服务器进行扩展)、读写分离、分库分表横向(增加服务器的方式横向扩展,通常是基于分片机制)
用途普通企业级的项目的数据存储用途广泛比如图数据库支持分析和遍历连接数据之间的关系、键值数据库可以处理大量数据扩展和极高的状态变化
查询语法结构化查询语言 (SQL)数据访问语法可能因数据库而异

8.3 NoSQL 数据库有什么优势?

  • 灵活性: NoSQL 数据库通常提供灵活的架构,以实现更快速、更多的迭代开发。灵活的数据模型使 NoSQL 数据库成为半结构化和非结构化数据的理想之选。
  • 可扩展性: NoSQL 数据库通常被设计为通过使用分布式硬件集群来横向扩展,而不是通过添加昂贵和强大的服务器来纵向扩展。
  • 高性能: NoSQL 数据库针对特定的数据模型和访问模式进行了优化,这与尝试使用关系数据库完成类似功能相比可实现更高的性能。
  • 强大的功能: NoSQL 数据库提供功能强大的 API 和数据类型,专门针对其各自的数据模型而构建。

8.4 NoSQL 数据库有哪些类型?

NoSQL 数据库主要可以分为下面四种类型:

  • 键值:键值数据库是一种较简单的数据库,其中每个项目都包含键和值。这是极为灵活的 NoSQL 数据库类型,因为应用可以完全控制 value 字段中存储的内容,没有任何限制。Redis 和 DynanoDB 是两款非常流行的键值数据库。
  • 文档:文档数据库中的数据被存储在类似于 JSON(JavaScript 对象表示法)对象的文档中,非常清晰直观。每个文档包含成对的字段和值。这些值通常可以是各种类型,包括字符串、数字、布尔值、数组或对象等,并且它们的结构通常与开发者在代码中使用的对象保持一致。MongoDB 就是一款非常流行的文档数据库。
  • 图形:图形数据库旨在轻松构建和运行与高度连接的数据集一起使用的应用程序。图形数据库的典型使用案例包括社交网络、推荐引擎、欺诈检测和知识图形。Neo4j 和 Giraph 是两款非常流行的图形数据库。
  • 宽列:宽列存储数据库非常适合需要存储大量的数据。Cassandra 和 HBase 是两款非常流行的宽列存储数据库。

9. 字符集

MySQL 字符编码集中有两套 UTF-8 编码实现:**utf8** 和 **utf8mb4**。

如果使用 utf8 的话,存储 emoji 符号和一些比较复杂的汉字、繁体字就会出错。

9.1 何为字符集?

字符是各种文字和符号的统称,包括各个国家文字、标点符号、表情、数字等等。 字符集 就是一系列字符的集合。字符集的种类较多,每个字符集可以表示的字符范围通常不同,就比如说有些字符集是无法表示汉字的。

9.2 有哪些常见的字符集?

  • 常见的字符集有 ASCII、GB2312、GBK、UTF-8……。
  • 不同的字符集的主要区别在于:
    • 可以表示的字符范围
    • 编码方式

9.3 ASCII

  • ASCII (American Standard Code for Information Interchange,美国信息交换标准代码) 是一套主要用于现代美国英语的字符集(这也是 ASCII 字符集的局限性所在)。
  • ASCII 码长度是一个字节也就是 8 个 bit,ASCII 字符集共定义了 128 个字符,其中有 33 个控制字符(比如回车、删除)无法显示。
  • 0(48),A(65),a(97)

9.4 GB2312

  • 对于英语字符,GB2312 编码和 ASCII 码是相同的,1 字节编码即可。对于非英字符,需要 2 字节编码。
  • GB2312 字符集是一种对汉字比较友好的字符集,共收录 6700 多个汉字,基本涵盖了绝大部分常用汉字。不过,GB2312 字符集不支持绝大部分的生僻字和繁体字。

9.5 GBK

  • GBK 字符集可以看作是 GB2312 字符集的扩展,兼容 GB2312 字符集,共收录了 20000 多个汉字。

9.6 GB18030

  • GB18030 完全兼容 GB2312 和 GBK 字符集,纳入中国国内少数民族的文字,且收录了日韩汉字,是目前为止最全面的汉字字符集,共收录汉字 70000 多个。

9.7 BIG5

  • BIG5 主要针对的是繁体中文,收录了 13000 多个汉字。

9.8 UTF-8

  • UTF-88-bit Unicode Transformation Format)
  • UTF-8 使用 1 到 4 个字节为每个字符编码,
  • UTF-8 是目前使用最广的一种字符编码。

10. MySQL 字符集

  • 通常情况下,我们建议使用 UTF-8 作为默认的字符编码方式。
  • MySQL 字符编码集中有两套 UTF-8 编码实现:
    • **utf8**:utf8编码只支持1-3个字节 。 在 utf8 编码中,中文是占 3 个字节,其他数字、英文、符号占一个字节。但 emoji 符号占 4 个字节,一些较复杂的文字、繁体字也是 4 个字节。
    • **utf8mb4**:UTF-8 的完整实现,正版!最多支持使用 4 个字节表示字符,因此,可以用来存储 emoji 符号。
    • 因此,如果你需要存储emoji类型的数据或者一些比较复杂的文字、繁体字到 MySQL 数据库的话,数据库的编码一定要指定为utf8mb4 而不是utf8 ,要不然存储的时候就会报错了。

11. SQL语句知识点总结

11.1 SQL 分类

  • 数据定义语言(DDL)

    • 概念:数据定义语言(Data Definition Language,DDL)是 SQL 语言集中负责数据结构定义与数据库对象定义的语言。
    • 功能:定义数据库对象
    • 核心指令: CREATEALTERDROP
  • 数据操纵语言(DML)

    • 概念:数据操纵语言(Data Manipulation Language, DML)是用于数据库操作,对数据库其中的对象和数据运行访问工作的编程语句。
    • 功能:访问数据,因此其语法都是以读写数据库为主。
    • 核心指令:INSERTUPDATEDELETESELECT。这四个指令合称 CRUD(Create, Read, Update, Delete),即增删改查。
  • 事务控制语言(TCL)

    • 概念:事务控制语言 (Transaction Control Language, TCL) 用于管理数据库中的事务。这些用于管理由 DML 语句所做的更改。它还允许将语句分组为逻辑事务。
    • 核心指令: COMMITROLLBACK
  • 数据控制语言(DCL)

    • 概念:数据控制语言 (Data Control Language, DCL) 是一种可对数据访问权进行控制的指令,它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。
    • 核心指令: GRANTREVOKE

11.2 增删改查

  • 插入数据:INSERT INTO 语句用于向表中插入新记录。

    1
    2
    INSERT INTO user(username, password, email)
    VALUES ('admin', 'admin', 'xxxx@163.com');
  • 更新数据:UPDATE 语句用于更新表中的记录。

    1
    2
    3
    UPDATE user
    SET username='robot', password='robot'
    WHERE username = 'root';
  • 删除数据

    • DELETE 语句用于删除表中的记录。
      1
      2
      DELETE FROM user
      WHERE username = 'robot';
    • TRUNCATE TABLE 可以清空表,也就是删除所有行。
      1
      TRUNCATE TABLE user;
  • 查询数据

    • 关键字:

      • SELECT 语句用于从数据库中查询数据。
      • DISTINCT 用于返回唯一不同的值。它作用于所有列,也就是说所有列的值都相同才算相同。
      • LIMIT 限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。
      • ASC:升序(默认)
      • DESC:降序
    • 查询单列

      1
      2
      SELECT prod_name
      FROM products;
    • 查询多列

      1
      2
      SELECT prod_id, prod_name, prod_price
      FROM products;
    • 查询所有列

      1
      2
      SELECT *
      FROM products;
    • 查询不同的值

      1
      2
      SELECT DISTINCT
      vend_id FROM products;
    • 限制查询结果

      1
      2
      3
      4
      5
      -- 返回前 5 行
      SELECT * FROM mytable LIMIT 5;
      SELECT * FROM mytable LIMIT 0, 5;
      -- 返回第 3 ~ 5 行
      SELECT * FROM mytable LIMIT 2, 3;
  • 排序

  • order by 用于对结果集按照一个列或者多个列进行排序。默认按照升序对记录进行排序,如果需要按照降序对记录进行排序,可以使用 desc 关键字。

  • order by 对多列排序的时候,先排序的列放前面,后排序的列放后面。并且,不同的列可以有不同的排序规则。

    1
    2
    SELECT * FROM products
    ORDER BY prod_price DESC, prod_name ASC;
  • 分组:**group by**:

    • 分组

      1
      2
      SELECT cust_name, COUNT(cust_address) AS addr_num
      FROM Customers GROUP BY cust_name;
    • 分组后排序

      1
      2
      3
      SELECT cust_name, COUNT(cust_address) AS addr_num
      FROM Customers GROUP BY cust_name
      ORDER BY cust_name DESC;
    • **having**:

      • having 用于对汇总的 group by 结果进行过滤。

      • having 一般都是和 group by 连用。

      • wherehaving 可以在相同的查询中。

      • 使用 WHERE 和 HAVING 过滤数据

        1
        2
        3
        4
        5
        SELECT cust_name, COUNT(*) AS num
        FROM Customers
        WHERE cust_email IS NOT NULL
        GROUP BY cust_name
        HAVING COUNT(*) >= 1;
      • **having vs where**:

        • where:过滤过滤指定的行,后面不能加聚合函数(分组函数)。wheregroup by 前。
        • having:过滤分组,一般都是和 group by 连用,不能单独使用。havinggroup by 之后。

12. SQL面试题总结

13. MySQL索引详解

13.1 索引介绍

  • 概念:索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。
  • 作用:索引的作用就相当于书的目录。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
  • 数据结构:索引底层数据结构存在很多种类型,常见的索引结构有: B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了B+树作为索引结构。

13.2 索引的优缺点

  • 优点

    • 使用索引可以大大加快数据的检索速度(大大减少检索的数据量,这也是创建索引的最主要的原因。
    • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
  • 缺点

    • 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
    • 索引需要使用物理文件存储,也会耗费一定空间。
  • 使用索引一定能提高查询性能吗?

    • 大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。

13.3 索引的分类

  • 按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。
  • 按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
  • 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
  • 按「字段个数」分类:单列索引、联合索引。

13.4 MySQL索引使用有哪些注意事项呢

  • 哪些情况会导致索引失效:

    • like通配符可能导致索引失效。
    • 查询条件包含or,可能导致索引失效
    • 如何字段类型是字符串,where时一定用引号括起来,否则索引失效
    • 对索引列运算(如,+、-、*、/),索引失效。
    • 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
    • 在索引列上使用mysql的内置函数,索引失效。
    • 索引字段上使用is null, is not null,可能导致索引失效。
    • 索引字段上使用(!= 或者 < >,not in)时,可能会导致索引失效。
    • 左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。
    • mysql估计使用全表扫描要比使用索引快,则不使用索引。
  • 索引不适合哪些场景

    • 数据量少的不适合加索引
    • 更新比较频繁的也不适合加索引
    • 区分度低的字段不适合加索引(如性别)

13.5 数据库索引结构为什么要用B+树,为什么不用二叉树?

  • MySQL 是会将数据持久化在硬盘,而存储功能是由 MySQL 存储引擎实现的,所以讨论 MySQL 使用哪种数据结构作为索引,实际上是在讨论存储引使用哪种数据结构作为索引,InnoDB 是 MySQL 默认的存储引擎,它就是采用了 B+ 树作为索引的数据结构。

  • 要设计一个 MySQL 的索引数据结构,不仅仅考虑数据结构增删改的时间复杂度,更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘,硬盘是一个非常慢的存储设备,我们在查询数据的时候,最好能在尽可能少的磁盘 I/0 的操作次数内完成。

  • 二分查找树:二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n)。

  • 自平衡二叉树:为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。

  • B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:

    • B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
    • B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
    • B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

14. 执行一条 select 语句,期间发生了什么?

14.1 MySQL 的架构

  • MySQL 的架构共分为两层:Server 层和存储引擎层,
  • Server 层负责建立连接、分析和执行 SQL。
  • 存储引擎层负责数据的存储和提取。

14.2 SQL的select语句的执行流程

  • 连接器:建立连接,管理连接、校验用户身份;
  • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
  • 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
  • 执行 SQL:执行 SQL 共有三个阶段:
    • 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。
    • 优化阶段:基于查询成本的考虑,选择查询成本最小的执行计划;
    • 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;

15. MySQL事务

15.1 事务是什么

  • 就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。

15.2 MySQL事务四大特性(ACID):

  • 原子性(Atomicity):一个事务被作为一个整体执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。不会结束在中间某个环节。事务在执行过程中如果发生异常,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性(Consistency):指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转100块钱,不管成功与否,A和B的总金额是不变的。
  • 隔离性(Isolation):多个事务并发访问时,事务之间是相互隔离的,即一个事务不影响其它事务运行效果。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)
  • 持久性(Durability):表示事务完成以后,该事务对数据库所作的操作更改,将持久地保存在数据库之中,即便系统故障也不会丢失。

15.3 MySQL事务四大特性(ACID)实现原理:

  • 原子性:是使用 undo log来实现的,如果事务执行过程中出错或者用户执行了rollback,系统通过undo log日志返回事务开始的状态。
  • 持久性:使用 redo log来实现,只要redo log日志持久化了,当系统崩溃,即可通过redo log把数据恢复。
  • 隔离性:通过锁以及MVCC,使事务相互隔离开。
  • 一致性:通过回滚、恢复,以及并发情况下的隔离性,从而实现一致性。

15.4 MySQL数据隔离级别

  • MySQL 里有四个隔离级别:Read uncommttied(可以读取未提交数据)、Read committed(可以读取已提交数据)、Repeatable read(可重复读)、Serializable(可串行化)。
  • 在 InnoDB 中,默认为 Repeatable 级别,InnoDB 中使用一种被称为 next-key locking 的策略来避免幻读(phantom)现象的产生。
  • 不同的事务隔离级别会导致不同的问题:
隔离级别脏读不可重复读幻读
读未提交(Read uncommttied)&#10004;&#10004;&#10004;
读已提交(Read committed)&#10008;&#10004;&#10004;
可重复读(Repeatable read)&#10008;&#10008;&#10004;
可串行化(Serializable)&#10008;&#10008;&#10008;

15.5 MVCC原理

参考:https://www.modb.pro/db/40241

  • 概念:MVCC(Multi-Version Concurrency Control)多版本并发控制,是数据库控制并发访问的一种手段。MVCC只在 读已提交(RC) 和 可重复度(RR) 这两种事务隔离级别下才有效。MVCC是数据库引擎(InnoDB)层面实现的,用来处理读写冲突的手段(不用加锁),提高访问性能
  • 实现: InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。
  • MVCC 最大的好处:读不加锁,读写不冲突,极大地增加了 MySQL 的并发性。通过 MVCC,保证了事务 ACID 中的 I(隔离性)特性。

详细描述MVCC:

  • 创建一张测试表并写入测试数据,进行实验:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    create database likecolumn;

    use likecolumn;

    CREATE TABLE `t1` (

    `id` int(11) NOT NULL AUTO_INCREMENT,

    `a` int(11) NOT NULL,

    `b` int(11) NOT NULL,

    PRIMARY KEY (`id`),

    KEY `idx_c` (`a`)

    ) ENGINE=InnoDB CHARSET=utf8mb4;

    insert into t1(a,b) values (1,1),(2,2);
序号session 1session 2
1set session transaction_isolation=’READCOMMITTED’; /* 设置会话隔离级别为 RC*/set session transaction_isolation=’READCOMMITTED’;/* 设置会话隔离级别为 RC*/
2select * from t1;
3begin;
4update t1 set b=666 where a=1;
5begin;
6select * from t1;
7commit;
8select * from t1;
9commit;

这里解释一下上面的实验过程,在 session1 开启一个事务更新了 a=1 这行记录,但还没提交的情况下,在 session2 中,满足 a=1 这条记录,b 的值还是原始值 1,而不是 session1 更新之后的 666,那么在数据库层面,这是怎么实现的呢?

其实 InnoDB 就是通过 MVCC 和 UNDO LOG 来实现的。

  • MVCC 的实现原理

    • 对于 InnoDB ,聚簇索引记录中包含 3 个隐藏的列:

      • ROW ID:隐藏的自增 ID,如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树。
      • 事务 ID:记录最后一次修改该记录的事务 ID。
      • 回滚指针:指向这条记录的上一个版本。
    • 我们拿上面的例子,对应解释下 MVCC 的实现原理,如下图:

    • 如图,首先 insert 语句向表 t1 中插入了一条数据,a 字段为 1,b 字段为 1, ROW ID 也为 1 ,事务 ID 假设为 1,回滚指针假设为 null。当执行 update t1 set b=666 where a=1 时,大致步骤如下:
      • 数据库会先对满足 a=1 的行加排他锁;
      • 然后将原记录复制到 undo 表空间中;
      • 修改 b 字段的值为 666,修改事务 ID 为 2;
      • 并通过隐藏的回滚指针指向 undo log 中的历史记录;
      • 事务提交,释放前面对满足 a=1 的行所加的排他锁。
    • 在前面实验的第 6 步中,session2 查询的结果是 session1 修改之前的记录,这个记录就是来自 undolog 中。
  • 因此可以总结出 MVCC 实现的原理大致是:
    InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。
    MVCC 最大的好处是读不加锁,读写不冲突,极大地增加了 MySQL 的并发性。通过 MVCC,保证了事务 ACID 中的 I(隔离性)特性。

16. 说一下什么是幻读,脏读,不可重复读?

16.1 脏读:

脏读是指一个事务中访问到了另外一个事务未提交的数据,如下所示:

会话1会话2
beginbegin
update table set age=10 where id=1
select age from table where id=1
commitcommit

如果会话 2 更新 age 为 10,但是在 commit 之前,会话 1 希望得到 age,那么会获得的值就是更新前的值。或者如果会话 2 更新了值但是执行了 rollback,而会话 1 拿到的仍是 10。这就是脏读。

16.2 不可重复读:

一个事务读取同一条记录2次,得到的结果不一致:

会话1会话2
beginbegin
select age from table where id=1
update table set age=10 where id=1
commit
select age from table where id=1
commit

由于在读取中间变更了数据,所以会话 1 事务查询期间的得到的结果就不一样了。

16.3 幻读:

一个事务读取2次,得到的记录条数不一致:

会话1会话2
beginbegin
select age from table where id>2
insert into table(id, age) values(5, 10)
commit
select age from table where id>2
commit

上图很明显的表示了这个情况,由于在会话 1 之间插入了一个新的值,所以得到的两次数据就不一样了。

17. 说一下什么是内连接、外连接、交叉连接、笛卡尔积呢?

例子图示:

17.1 内连接(inner join):

  • 概念:取得两张表中满足存在连接匹配关系的记录。
  • 例子:
    1
    2
    select * from Student s
    inner join Course c on s.C_S_Id=c.C_Id

17.2 外连接(outer join):

  • 概念:取得两张表中满足存在连接匹配关系的记录,以及某张表(或两张表)中不满足匹配关系的记录。
  • 分类:左外连接(left join / left outer join)、右外连接(right join / right outer join)和全外连接(full join / full outer join)
  • 左外连接:满足on条件表达式,左外连接是以左表为准,返回左表所有的数据,与右表匹配的则有值,没有匹配的则以空(null)取代。
    1
    2
    select * from Student s
    left join Course c on s.C_S_Id=c.C_Id

  • 右外连接:满足on条件表达式,右外连接是以右表为准,返回右表所有的数据,与左表匹配的则有值,没有匹配的则以空(null)取代。
    1
    2
    select * from Student s
    right join Course c on s.C_S_Id=c.C_Id

  • 全外连接:满足on条件表达式,返回两个表符合条件的所有行,a表没有匹配的则a表的列返回null,b表没有匹配的则b表的列返回null,即返回的是左连接和右连接的并集。
    1
    2
    select * from Student s
    full join Course c on s.C_S_Id=c.C_Id

17.3 交叉连接(cross join):

  • 概念:显示两张表所有记录一一对应,没有匹配关系进行筛选,也被称为:笛卡尔积。比如A表有10条记录,B表有100条记录,那么笛卡尔积关联查询结果就是10*100=1000条记录,一般我们要避免笛卡尔积的出现。
  • 例子:
    1
    2
    select * from Student s
    cross join Course c
1
2
3
4
// 加上条件返回满足条件表达式的两个表的行:
select * from Student s
cross join Course c
where s.C_S_Id=c.C_Id

18. SQL优化

18.1 整体思路

  • 查看执行计划 explain
  • 如果有告警信息,查看告警信息 show warnings;
  • 查看SQL涉及的表结构和索引信息
  • 根据执行计划,思考可能的优化点
  • 按照可能的优化点执行表结构变更、增加索引、SQL改写等操作
  • 查看优化后的执行时间和执行计划
  • 如果优化效果不明显,重复第四步操作

18.2 SQL慢查询优化案例

  • 表结构:

    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
    CREATE TABLE `a`
    (
    `id` int(11) NOT NULLAUTO_INCREMENT,
    `seller_id` bigint(20) DEFAULT NULL,
    `seller_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
    `gmt_create` varchar(30) DEFAULT NULL,
    PRIMARY KEY (`id`)
    );
    CREATE TABLE `b`
    (
    `id` int(11) NOT NULLAUTO_INCREMENT,
    `seller_name` varchar(100) DEFAULT NULL,
    `user_id` varchar(50) DEFAULT NULL,
    `user_name` varchar(100) DEFAULT NULL,
    `sales` bigint(20) DEFAULT NULL,
    `gmt_create` varchar(30) DEFAULT NULL,
    PRIMARY KEY (`id`)
    );
    CREATE TABLE `c`
    (
    `id` int(11) NOT NULLAUTO_INCREMENT,
    `user_id` varchar(50) DEFAULT NULL,
    `order_id` varchar(100) DEFAULT NULL,
    `state` bigint(20) DEFAULT NULL,
    `gmt_create` varchar(30) DEFAULT NULL,
    PRIMARY KEY (`id`)
    );
  • 有问题的查询SQL: a,b,c 三张表关联,查询用户17 在当前时间前后10个小时的订单情况,并根据订单创建时间升序排列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    select a.seller_id,
    a.seller_name,
    b.user_name,
    c.state
    from a,
    b,
    c
    where a.seller_name = b.seller_name
    and b.user_id = c.user_id
    and c.user_id = 17
    and a.gmt_create
    BETWEEN DATE_ADD(NOW(), INTERVAL – 600 MINUTE)
    AND DATE_ADD(NOW(), INTERVAL 600 MINUTE)
    order by a.gmt_create;
  • 先查看各表数据量

    1
    2
    3
    4
    5
    select 'a', count(*) from a
    union
    select 'b', count(*) from b
    union
    select 'c', count(*) from c;

  • 查看原sql执行时间
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    select a.seller_id,
    a.seller_name,
    b.user_name,
    c.state
    from a,
    b,
    c
    where a.seller_name = b.seller_name
    and b.user_id = c.user_id
    and c.user_id = 17
    and a.gmt_create
    BETWEEN DATE_ADD(NOW(), INTERVAL – 600 MINUTE)
    AND DATE_ADD(NOW(), INTERVAL 600 MINUTE)
    order by a.gmt_create;

  • 查看执行计划 explain
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    explain select 
    a.seller_id,
    a.seller_name,
    b.user_name,
    c.state
    from a,
    b,
    c
    where a.seller_name = b.seller_name
    and b.user_id = c.user_id
    and c.user_id = 17
    and a.gmt_create
    BETWEEN DATE_ADD(NOW(), INTERVAL – 600 MINUTE)
    AND DATE_ADD(NOW(), INTERVAL 600 MINUTE)
    order by a.gmt_create;

执行计划中几个重要字段的解释说明

字段解释
id每个被独立执行的操作标识,标识对象被操作的顺序,id值越大,先被执行,如果相同,执行顺序从上到下
select_type查询中每个select 字句的类型
table被操作的对象名称,通常是表名,但有其他格式
partitions匹配的分区信息(对于非分区表值为NULL)
type连接操作的类型
possible_keys可能用到的索引
key优化器实际使用的索引(最重要的列) 从最好到最差的连接类型为const、eq_reg、ref、range、index和ALL。当出现ALL时表示当前SQL出现了“坏味道”
key_len被优化器选定的索引键长度,单位是字节
ref表示本行被操作对象的参照对象,无参照对象为NULL
rows查询执行所扫描的元组个数(对于innodb,此值为估计值)
filtered条件表上数据被过滤的元组个数百分比
extra执行计划的重要补充信息,当此列出现Using filesort , Using temporary 字样时就要小心了,很可能SQL语句需要优化
  • 通过观察执行计划和SQL语句,确定初步优化方案

    • SQL中where条件字段类型要跟表结构一致,表中user_id为varchar(50)类型,实际SQL用的int类型,存在隐式转换,也未添加索引。将b和c表 user_id 字段改成int类型。
    • 因存在b表和c表关联,将b和c表user_id创建索引
    • 因存在a表和b表关联,将a和b表 seller_name字段创建索引
    • 利用复合索引消除临时表和排序
      1
      2
      3
      4
      5
      alter table b modify `user_id` int(10) DEFAULT NULL;
      alter table c modify `user_id` int(10) DEFAULT NULL;
      alter table c add index `idx_user_id`(`user_id`);
      alter table b add index `idx_user_id_sell_name`(`user_id`,`seller_name`);
      alter table a add index `idx_sellname_gmt_sellid`(`gmt_create`,`seller_name`,`seller_id`);
  • 查看优化后的执行时间

  • 继续查看优化后的执行计划

这里只看到查询需要扫描的元素比较大,不过还看到了有两处告警信息,直接查看告警信息

1
show warnings;

Cannot use range access on index ‘idx_sellname_gmt_sellid’ due to type or collation conversion on field ‘get_create’,这句话是告诉你由于gmt_create列发生了类型转换所以无法走索引。
查看SQL建表语句发现gmt_create字段被设计成了varchar类型,在SQL查询时需要转化成时间格式做查询,确实不能走索引。所以需要调整一下gmt_create字段格式

1
alter table a modify "gmt_create" datetime DEFAULT NULL;
  • 修改字段后再来查看执行时间

  • 再观察优化后的执行计划

18.3 SQL优化要点

  1. 在表中建立索引,优先考虑wheregroup by使用到的字段。

  2. 尽量避免使用select *,返回无用的字段会降低查询效率,如:SELECT * FROM t

    • 优化方式:使用具体的字段代替*,只返回使用到的字段
  3. 尽量避免使用innot in,会导致数据库引擎放弃索引进行全表扫描,如:SELECT * FROM t WHERE id IN (2,3)SELECT * FROM t1 WHERE username IN (SELECT username FROM t2)

    • 优化方式:
      • 如果是连续数值,可以用between代替,如:SELECT * FROM t WHERE id BETWEEN 2 AND 3
      • 如果是子查询,可以用exists代替,如:SELECT * FROM t1 WHERE EXISTS (SELECT * FROM t2 WHERE t1.username = t2.username)
  4. 尽量避免使用or,会导致数据库引擎放弃索引进行全表扫描。如:SELECT * FROM t WHERE id = 1 OR id = 3

    • 优化方式:可以用union代替or,如:SELECT * FROM t WHERE id = 1 UNION SELECT * FROM t WHERE id = 3
    • 默认地,UNION 操作符选取不同的值。如果允许重复的值,请使用 UNION ALL。

      (PS:如果or两边的字段是同一个,如例子中这样。貌似两种方式效率差不多,即使union扫描的是索引,or扫描的是全表)

  5. 尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描。如:SELECT * FROM t WHERE username LIKE '%li%'

    • 优化方式:尽量在字段后面使用模糊查询。如:SELECT * FROM t WHERE username LIKE 'li%'
  6. 尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描。如:SELECT * FROM t WHERE score IS NULL

    • 优化方式:可以给字段添加默认值0,对0值进行判断。如:SELECT * FROM t WHERE score = 0
  7. 尽量避免在where条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描。如:SELECT * FROM t2 WHERE score/10 = 9SELECT * FROM t2 WHERE SUBSTR(username,1,2) = 'li'

    • 优化方式:可以将表达式、函数操作移动到等号右侧。如:SELECT * FROM t2 WHERE score = 10*9SELECT * FROM t2 WHERE username LIKE 'li%'
  8. 当数据量大时,避免使用where 1=1的条件。通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描。如:SELECT * FROM t WHERE 1=1

    • 优化方式:用代码拼装sql时进行判断,没where加where,有where加and。

19. 分库分表

19.1 什么是分库分表:

分库分表是在海量数据下,由于单库、表数据量过大,导致数据库性能持续下降的问题,演变出的技术方案。

通过一定的规则,将原本数据量大的数据库拆分成多个单独的数据库,将原本数据量大的表拆分成若干个数据表,使得单一的库、表性能达到最优的效果(响应速度快),以此提升整体数据库性能。

19.2 分库分表方案:

  • 垂直分库:垂直分库一般来说按照业务和功能的维度进行拆分,将不同业务数据分别放到不同的数据库中。垂直分库把一个库的压力分摊到多个库,提升了一些数据库性能,但并没有解决由于单表数据量过大导致的性能问题,所以就需要配合后边的分表来解决。

    • 例子:按业务类型对数据分离,剥离为多个数据库,像订单、支付、会员、积分相关等表放在对应的订单库、支付库、会员库、积分库。不同业务禁止跨库直连,获取对方业务数据一律通过API接口交互,这也是微服务拆分的一个重要依据。
  • 垂直分表:垂直分表针对业务上字段比较多的大表进行的,一般是把业务宽表中比较独立的字段,或者不常用的字段拆分到单独的数据表中,是一种大表拆小表的模式。

    • 例子:一张t_order订单表上有几十个字段,其中订单金额相关字段计算频繁,为了不影响订单表t_order的性能,就可以把订单金额相关字段拆出来单独维护一个t_order_price_expansion扩展表,这样每张表只存储原表的一部分字段,通过订单号order_no做关联,再将拆分出来的表路由到不同的库中。
  • 水平分库:水平分库是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,以此实现水平扩展,是一种常见的提升数据库性能的方式。

    • 例子:db_orde_1db_order_2两个数据库内有完全相同的t_order表,我们在访问某一笔订单时可以通过对订单的订单编号取模的方式 订单编号 mod 2 (数据库实例数) ,指定该订单应该在哪个数据库中操作。
  • 水平分表:水平分表是在同一个数据库内,把一张大数据量的表按一定规则,切分成多个结构完全相同表,而每个表只存原表的一部分数据。

    • 例子:例如:一张t_order订单表有900万数据,经过水平拆分出来三个表,t_order_1t_order_2t_order_3,每张表存有数据300万,以此类推。

19.3 数据存在哪个库的表

分库分表以后会出现一个问题,一张表会出现在多个数据库里,到底该往哪个库的哪个表里存呢?

常见的有 取模算法范围限定算法范围+取模算法预定义算法

  • 取模算法:

    • 概念:关键字段取模(对hash结果取余数 hash(XXX) mod N),N为数据库实例数或子表数量)是最为常见的一种路由方式。
    • 例子:以t_order订单表为例,先给数据库从 0 到 N-1进行编号,对 t_order订单表中order_no订单编号字段进行取模hash(order_no) mod N,得到余数i。i=0存第一个库,i=1存第二个库,i=2存第三个库,以此类推。
    • 缺点:取模算法对集群的伸缩支持不太友好,集群中有N个数据库实例hash(user_id) mod N,当某一台机器宕机,本应该落在该数据库的请求就无法得到处理,这时宕掉的实例会被踢出集群。此时机器数减少算法发生变化hash(user_id) mod N-1,同一用户数据落在了在不同数据库中,等这台机器恢复,用user_id作为条件查询用户数据就会少一部分。
  • 范围限定算法:

    • 概念:范围限定算法以某些范围字段,如时间或ID区拆分。
    • 例子:用户表t_user被拆分成t_user_1、t_user_2、t_user_3三张表,后续将user_id范围为1 ~ 1000w的用户数据放入t_user_1,1000~ 2000w放入t_user_2,2000~3000w放入t_user_3,以此类推。按日期范围划分同理。
    • 缺点:由于连续分片可能存在数据热点,比如按时间字段分片时,如果某一段时间(双11等大促)订单骤增,存11月数据的表可能会被频繁的读写,其他分片表存储的历史数据则很少被查询,导致数据倾斜,数据库压力分摊不均匀。
  • 范围+取模算法:

    • 为了避免热点数据的问题,我们可以对上范围算法优化一下。
    • 这次我们先通过范围算法定义每个库的用户表t_user只存1000w数据,第一个db_order_1库存放userId从1 ~ 1000w,第二个库10002000w,第三个库20003000w,以此类推。
    • 每个库里再把用户表t_user拆分成t_user_1、t_user_2、t_user_3等,对userd进行取模路由到对应的表中。
    • 有效的避免数据分布不均匀的问题,数据库水平扩展也简单,直接添加实例无需迁移历史数据。
  • 地理位置分片:

    • 地理位置分片其实是一个更大的范围,按城市或者地域划分,比如华东、华北数据放在不同的分片库、表。
  • 预定义算法:

    • 预定义算法是事先已经明确知道分库和分表的数量,可以直接将某类数据路由到指定库或表中,查询的时候亦是如此。

20. MySQL都有哪些锁呢?

  • 按照 锁的粒度 划分可以分成:
    • 表锁:表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。
    • 页锁:页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。
    • 行锁:行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁。
  • 按照 使用的方式 划分可以分为:
    • 共享锁:又叫做读锁。当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
    • 排它锁:又叫做写锁。当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。
  • 按照 思想 的划分:
    • 乐观锁
    • 悲观锁

21. Redis基础

21.1 什么是Redis

与传统数据库不同的是,Redis 的数据是存在内存中的,读写速度非常快,被广泛应用于缓存。

21.2 Redis为什么这么快?

  1. Redis 基于内存,内存的访问速度是磁盘的上千倍;
  2. Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用;
  3. Redis 内置了多种优化过后的数据结构实现,性能非常高。

22. Redis应用

  • 缓存
  • 分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。
  • 消息队列:Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。

23. Redis数据结构

23.1 基本数据结构

  • Redis 共有 5 种基本数据结构:String(字符串)、Hash(散列)、List(列表)、Set(集合)、Zset(有序集合)。

  • String(字符串):

    • String 是 Redis 中最简单同时也是最常用的一个数据结构。
    • 应用场景:把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
  • Hash(哈希)

    • 概念:Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象。
    • 应用场景:对象数据存储场景,比如用户信息、商品信息、文章信息等。
  • List(列表)

    • 概念:Redis用双端链表实现List
    • 应用场景:信息流展示(最新文章、最新动态);消息队列(功能过于简单且存在很多缺陷,不建议这样做)。
  • Set(集合)

    • 概念:Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet 。当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择。
    • 应用场景:需要存放的数据不能重复的场景,点赞,或点踩,收藏等,可以放到set中实现
  • Zset(有序集合)

    • 概念:Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMapTreeSet 的结合体。
    • 应用场景:各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜等等。
  • 总结

数据类型说明
String一种二进制安全的数据结构,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
Hash一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。
ListRedis 的 List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
Set无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet
Zset和 Set 相比,Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMapTreeSet 的结合体。

23.2 特殊数据结构

除了 5 种基本的数据结构之外,Redis 还支持 3 种特殊的数据结构:Bitmap、HyperLogLog、GEO。

  • Bitmap

    • 概念:Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
    • 应用场景:需要保存状态信息(0/1 即可表示)的场景。举例:用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
  • HyperLogLog

    • 概念:HyperLogLog 是一种有名的基数计数概率算法。
    • 应用场景:数量量巨大(百万、千万级别以上)的计数场景。举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计。
  • Geospatial index

    • 概念:Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。
    • 应用场景:需要管理使用地理空间数据的场景。举例:附近的人。
  • 总结

数据类型说明
Bitmap你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
HyperLogLogRedis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64个不同元素。不过,HyperLogLog 的计数结果并不是一个精确值,存在一定的误差(标准误差为 0.81% )。
Geospatial indexGeospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。

24. Redis持久化机制

24.1 概念:

redis是一个内存数据库,一旦服务器宕机,内存中的数据将全部丢失。所以,对 Redis 来说,实现数据的持久化,避免从后端数据库中进行恢复,是至关重要的。

24.2 分类

经典的redis的持久化机制分为两种:快照(snapshotting,RDB)和只追加文件(append-only file, AOF);4.0之后,redis又提供了一种RDB和AOF的混合持久化机制。

24.3 各自的实现机制,优缺点

  • rdb就是把某时刻的数据以二进制的形式固化到磁盘上,优点是二进制存储,结构压缩紧凑,方便传输,适用于备份容灾,缺点是实时性不高,单纯rdb,很难保证数据的可靠性,容易造成数据丢失;
  • aof则是以日志的形式记录数据的变更信息,相对于rdb,其数据实时性较高,一定程度上保证了数据的安全可靠,但是,其文件大小容易暴增,如果aof刷盘频繁的话,还会影响redis性能。
  • 而混合型则是在一个文件中同时使用rdb和aof格式,集上述两种的优势于一身

24.4 RDB备份一般什么时候进行

  • rdb有三种触发方式。
  • 第一种,save命令触发,这种是堵塞式的,一般不推荐使用;
  • 第二种是bgsave命令触发,执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求;
  • 第三种就是根据业务情况自动触发,在某个固定时间段内,如果出现了指定次数的数据变更则进行rdb。

24.5 AOF重写机制

  • 当有命令触发或AOF达到一定大小后,会触发AOF文件重写。重写时,redis会fork一个子进程出来,在子进程中,将内存中所有数据通过redis写命令写入到一个新的AOF文件;AOF写完后,再删除旧的AOF文件,将新的AOF文件更名为旧的AOF文件。
  • 在fork后,子进程负责将内存中的数据写入新的AOF文件;同时,父进程中,如果有新的数据变更,会将其变更记录写到rewrite_buf中;待子进程写完后,通过信号通知父进程,父进程收到通知后,继续将rewrite_buf中的变更信息写入到新的AOF文件,这样新的aof文件就不会缺失数据了。

25. Redis线程模型

25.1 线程模型

redis 内部使用文件事件处理器 file event handler,它是单线程的,所以redis才叫做单线程模型。它采用IO多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中

25.2 redis是单线程为什么效率还这么高

  • 纯内存操作。
  • 核心是基于非阻塞的 IO 多路复用机制。
  • C 语言实现,语言更接近操作系统,执行速度相对会更快。
  • 单线程反而避免了多线程的频繁上下文切换问题,避免了多线程可能产生的竞争问题。

26. Redis内存管理

Redis主要通过控制内存上限回收策略实现内存管理。

26.1 设置内存上限

  • Redis使用maxmemory参数限制最大可用内存。
  • 限制内存的目的主要有:
    • 用于缓存场景, 当超出内存上限maxmemory时使用LRU等删除策略释放空间。
    • 防止所用内存超过服务器物理内存。

26.2 内存回收策略

  • 删除过期键对象
    • 惰性删除
    • 定期删除
  • 内存使用达到maxmemory上限时触发内存溢出控制策略。

27. Redis事务

27.1 Redis是否支持事务?

redis是支持事务的,他能保证一系列的命令要么全部执行,要么全部不执行

27.2 Redis隔离级别

  • 传统的数据库事务的隔级别分为读未提交、读提交、可重复读、可序化四个级别;其他,读提交解决了脏读问题,可重复读解决了不可重复度问题,可序化解决了幻读问题。
  • 在redis中,他的事务实现机制跟传统的数据库差异较大,如果一定要对接这几种隔离级别的话,我觉得是可序化。
  • 在redis中,其事务是将多个redis命令通过multi指示命令入队,然后通过exec命令逐个执行队列中的所有命令。
  • 首先脏读,在redis事务中,如果事务没提交,即事务队列中的命令没执行的话,该事务对数据的改变不会在redis内存数据中体现出来,这样,别的事务就不会读到未提交的数据,即不可能出现脏读;
  • 再考虑到redis的单线程出来模型,通过exec执行事务队列中多条命令时,不会存在其他redis命令的并发执行,这样的话,在redis事务中,天然解决了不可重复度问题。
  • 同时,也由于redis的单线程模型,不存在多事务并发执行,所以,幻读问题在redis中也是不存在的。
  • 所以说,如果要对接传统数据库事务隔离级别的话,redis实现了可序化的隔离级别。

27.3 相比传统数据库,redis的事务有什么不足的地方

  • 相比传统数据库,redis事务最大的不足,我觉得是不支持回滚;在传统数据库中,如果事务中途执行错误,是支持回滚的,在redis中,这个不支持。

  • redis不支持回滚,那么如果事务中途出现错误了会怎么样呢?

    • 这里有两种情况:第一种,命令入队时,如果redis命令格式错误,则会导致整个事务都不执行;第二种,如果执行命令时,其中某个命令出错,会继续执行完其他所有命令
  • 你怎么看待redis事务不支持回滚特性的?

    • 这里比较赞同redis作者的解析,redis作者认为,事务中途的错误都是由于开发者的编码错误造成的;而同时,没有任何机制能够避免程序员自己造成的错误,所以redis采用了简单、高效的无回滚事务处理机制。

27.4 redis事务中,有类似传统数据库中的锁机制吗?

在redis事务中,通过watch实现了CAS式的乐观锁

28. Redis生产问题

28.1 缓存穿透

  • 概念:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

  • 发生的情况:

    • 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
    • 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
  • 解决方案:

    • 限制非法请求;
    • 设置缓存空值或者默认值;
    • 使用过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。

28.2 缓存击穿

  • 概念:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

  • 解决方案:
    • 互斥锁方案,保证同一时间只有一个业务线程更新缓存
    • 不给热点数据设置过期时间,由后台异步更新缓存。

28.3 缓存雪崩

  • 概念:当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

  • 发生原因:

    • 大量数据同时过期
    • Redis 故障宕机
  • 解决方案:

    • 大量数据同时过期
      • 均匀设置过期时间
      • 互斥锁
    • Redis 故障宕机
      • 服务熔断或请求限流机制;
      • 构建 Redis 缓存高可靠集群;

31. 主从同步

32. 部署方式