Database基础

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

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

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

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

什么是 ER 图?

ER 图 全称是 Entity Relationship Diagram(实体联系图),提供了表示实体类型、属性和联系的方法。ER 图由下面 3 个要素组成:

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

数据库范式

  • 1NF(第一范式):属性不可再分。
  • 2NF(第二范式):1NF 的基础之上,消除了非主属性对于码的部分函数依赖。
  • 3NF(第三范式):3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。
  • 函数依赖(functional dependency):若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。
  • 部分函数依赖(partial functional dependency):如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖于(学号,身份证号);
  • 完全函数依赖(Full functional dependency):在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R(学号,班级,姓名)假设不同的班级学号有相同的,班级内学号不能相同,在 R 关系中,(学号,班级)->(姓名),但是(学号)->(姓名)不成立,(班级)->(姓名)不成立,所以姓名完全函数依赖与(学号,班级);
  • 传递函数依赖:在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物,因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。

主键和外键有什么区别?

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

为什么不推荐使用外键与级联?

  • 增加了复杂性: a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
  • 增加了额外工作:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗资源;(个人觉得这个不是不用外键的原因,因为即使你不使用外键,你在应用层面也还是要保证的。所以,我觉得这个影响可以忽略不计。)
  • 对分库分表不友好:因为分库分表下外键是无法生效的。

什么是存储过程?

  • 存储过程是一些 SQL 语句的集合,中间加了点逻辑控制语句。

drop、delete 与 truncate 区别?

  • 用法不同
    • drop(丢弃数据): drop table 表名 ,直接将表都删除掉,在删除表的时候使用。
    • truncate (清空数据) : truncate table 表名 ,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。
    • delete(删除数据) : delete from 表名 where 列名=值,删除某一行的数据,如果不加 where 子句和truncate table 表名作用类似
  • 属于不同的数据库语言
    • truncate 和 drop 属于 DDL(数据定义语言)语句,操作立即生效,原数据不放到 rollback segment 中,不能回滚,操作不触发 trigger。而 delete 语句是 DML (数据库操作语言)语句,这个操作会放到 rollback segment 中,事务提交之后才生效。
  • 执行速度不同
    • drop > truncate > delete

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

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

Mysql

什么是 MySQL?

  • MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息。

MySQL 字段类型


MySQL 基础架构

  • 连接器: 身份认证和权限相关(登录 MySQL 的时候)。
  • 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
  • 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
  • 优化器: 按照 MySQL 认为最优的方案去执行。
  • 执行器: 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。
  • 插件式存储引擎:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。

执行Select语句的过程

  • 连接器。建立连接,TCP,校验用户名和密码。
  • 查询缓存。
  • 解析SQL。词法分析、语法分析。
  • 执行SQl。
    • 预处理。查询字段是否存在,扩展*。
    • 优化器。
    • 执行器。主键索引查询、全表扫描、索引下推。

MySQL 存储引擎

  • MySQL5.5.5之前默认引擎是MyISAM,之后是InnoDB。

MySQL 存储引擎架构了解吗?

  • MySQL 存储引擎采用的是 插件式架构 ,支持多种存储引擎,我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。存储引擎是基于表的,而不是数据库。

MyISAM 和 InnoDB 有什么区别?

  • 是否支持行级锁:InnoDB 支持行级别的锁粒度,MyISAM 不支持,只支持表级别的锁粒度。
  • 是否支持事务:MyISAM 不提供事务支持。InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别。
  • 是否支持外键:MyISAM 不支持外键,而 InnoDB 支持。
  • 是否支持数据库异常崩溃后的安全恢复:MyISAM 不支持数据库异常崩溃后的安全恢复,而 InnoDB 支持。
  • 是否支持 MVCC:MyISAM 不支持 MVCC,而 InnoDB 支持。
  • 索引实现不一样:虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。
  • 性能有差别:InnoDB 的性能比 MyISAM 更强大。

Mysql索引

  • 索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。常见的索引结构有: B 树, B+树 和 Hash、红黑树。
  • 索引的优缺点
    • 优点:
      • 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
      • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
    • 缺点:
      • 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
      • 索引需要使用物理文件存储,也会耗费一定空间。
  • 底层数据结构
  • Hash表:不支持顺序和范围查询
  • B树&B+树:多路平衡查找树,大多数MySSQL存储引擎默认B+树。
    • B 树的所有节点既存放键(key) 也存放 数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
    • B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
    • B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
  • 索引类型(字段特性)
    • 主键索引
    • 唯一索引
    • 前缀索引
  • 物理存储分类:聚簇索引与非聚簇索引
    • 聚簇索引:索引结构和数据存放在一起,如InnoDB中的主键索引。
      • 优点:查询速度快;对排序查找和范围查找优化。
      • 缺点:依赖于有序的数据;更新代价大。
    • 非聚簇索引:索引结构和数据分开存放,如二级索引。非聚簇索引的叶子节点中存放主键的值,然后再到主键中寻找。
      • 优点:更新代价小。
      • 缺点:依赖于有序的数据;可能存在二次查询。
  • 覆盖索引和联合索引
    • 覆盖索引:需要查询的字段正好是索引的字段。
    • 联合索引:使用表中的多个字段创建索引。
    • 最左前缀匹配原则:在使用联合索引时,MySQL 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询,如 ><between以%开头的like查询 等条件,才会停止匹配。
  • 索引下推
    • 索引下推(Index Condition Pushdown) 是 MySQL 5.6 版本中提供的一项索引优化功能,可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。
  • 正确使用索引的建议
    • 选择合适的字段创建索引。
      • 不为NULL的字段。
      • 被频繁查询的字段。
      • 被作为条件查询的字段。
      • 频繁需要排序的字段。
      • 被频繁用于连接的字段。
    • 频繁更新的字段应该谨慎创建索引。
    • 尽可能考虑建立联合索引而不是单列索引。
    • 避免冗余索引。
    • 考虑在字符串类型的字段上使用前缀索引代替普通索引。
    • 避免索引失效。
    • 删除长期未使用的索引。

Mysql日志

  • binlog
  • redo log
  • undo log

Mysql事务

  • 事务是逻辑上的一组操作,要么都执行,要么都不执行。事物都有ACID特性:
    • 原子性:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
    • 一致性:执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
    • 隔离性:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
    • 持久性:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
  • 并发事务带来了哪些问题?
    • 脏读:一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据。
    • 丢失修改:在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失。
    • 不可重复读:指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况。
    • 幻读:幻读与不可重复读类似。它发生在一个事务读取某个范围的数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样。
  • 不可重复读和幻读有什么区别?
    • 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
    • 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
  • 并发事务的控制方式有哪些?
    • 锁——可看作悲观的模式;MVCC——可看作乐观的模式。
    • 共享锁(S锁):也叫读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
    • 排他锁(X锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
    • MVCC:多版本并发控制,对一份数据存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的。
  • 事务隔离级别
    • MySQL 的隔离级别基于锁和 MVCC 机制共同实现的,InnoDB默认支持的隔离级别为RR。
    • READ-UNCOMMITTED(读取未提交) : 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
    • READ-COMMITTED(读取已提交) : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
    • REPEATABLE-READ(可重复读) : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
    • SERIALIZABLE(可串行化) : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

MySQL 锁

  • 表级锁和行级锁了解吗?有什么区别?
    • 表级锁:对当前操作的整张表加锁,加锁快,不会出现死锁。但极其容易出发锁冲突,高并发下效率极低。
    • 行级锁:针对索引字段的锁,只针对当前操作的行记录加锁。加锁粒度小,并发度高,会出现死锁。
  • InnoDB有哪几种行级锁?
    • 记录锁(Record Lock) :也被称为记录锁,属于单个行记录上的锁,锁的是索引。
    • 间隙锁(Gap Lock) :锁定一个范围,不包括记录本身。
    • 临键锁(Next-Key Lock) :Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
  • 意向锁
    • 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
    • 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
  • 当前读和快照读有什么区别?
    • 快照读是指普通的SELECT语句,由于MVCC的存在,不需要加锁,适合对数据一致性要求不太高但要求性能的业务。
    • 当前读要给行记录加锁。

InnoDB对MVCC的实现

  • MVCC 的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_IDRead View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改。

  • 隐藏字段

    • DB_TRX_ID(6字节):表示最后一次插入或更新该行的事务 id。此外,delete 操作在内部被视为更新,只不过会在记录头 Record header 中的 deleted_flag 字段将其标记为已删除
    • DB_ROLL_PTR(7字节) 回滚指针,指向该行的 undo log 。如果该行未被更新,则为空
    • DB_ROW_ID(6字节):如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引
  • ReadView:用于可见性判断,保存了当前对本事务不可见的其他活跃事务。包含当前系统未提交的事务列表,以及该列表的最小值和最大值。
  • undo log:当事务回滚时用于将数据恢复到修改前的样子;当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读。

  • 数据可见性算法

    • 执行每个select语句前,都会先创建一个快照,保存当前数据库系统中处于活跃的事务的ID,也就是说,保存的是系统中当前不应该被本事务看到的其他事务ID列表。
      • TRX_ID < TRX_ID_MIN,表示该数据行快照是在当前所有未提交事务之前进行更改的,因此可以使用。
      • TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。
      • TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断:
        • 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。
        • 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。
    • 在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。

RC 和 RR 隔离级别下 MVCC 的差异

  • 在 RC 隔离级别下的 每次select 查询前都生成一个Read View (m_ids 列表)——导致不可重复读
  • 在 RR 隔离级别下只在事务开始后 第一次select 数据前生成一个Read View(m_ids 列表)

MVCC➕Next-key-Lock 防止幻读

  • 执行普通select,以MVCC快照读的方式解决幻读问题:在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”。
  • 执行 select…for update/lock in share mode、insert、update、delete 等当前读:当执行当前读时,在锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读。

封锁协议

  • 三级封锁协议
    • 一级封锁协议:修改数据必须加X锁,直到该事务结束。可以解决丢失修改问题。
    • 二级封锁协议:读取数据时必须加S锁,读取完马上释放。可以解决读取脏数据的问题。
    • 三级封锁协议:读取数据时必须加S锁,直到事务结束才释放。可以解决不可重复读的问题。
  • 两端锁协议——加锁和解锁分两个阶段进行。

查询性能优化

  • 使用Explain进行分析
    • Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。
    • 比较重要的字段有:
      • select_type : 查询类型,有简单查询、联合查询、子查询等
        • key : 使用的索引
        • rows : 扫描的行数
  • 优化数据访问
    • 减少请求的数据量:只访问必要的列、必要的行,缓存重复查询的数据
    • 减少服务器端扫描的行数:使用索引覆盖查询。
  • 重构查询方式
    • 切分大查询
    • 分解大连接查询

切分

  • 水平切分
    • 将同一个表中的记录拆分到多个结构相同的表中。
  • 垂直切分
    • 垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。

复制

  • 主从复制
    • 主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。
      • binlog 线程 :负责将主服务器上的数据更改写入二进制日志(Binary log)中。
      • I/O 线程 :负责从主服务器上读取二进制日志,并写入从服务器的中继日志(Relay log)。
      • SQL 线程 :负责读取中继日志,解析出主服务器已经执行的数据更改并在从服务器中重放(Replay)。
  • 读写分离
    • 主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。
    • 读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。

Redis

什么是Redis?

  • Redisopen in new window (REmote DIctionary Server)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。

Redis 为什么这么快?

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

分布式缓存常见的技术选型方案?

  • Dragonfly:一种针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。
  • KeyDB:Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。

Redis 和 Memcached 的区别和共同点

  • 共同点:
    • 都是基于内存的数据库,一般都用来当做缓存使用。
    • 都有过期策略。
    • 两者的性能都非常高。
  • 区别:
  • Redis 支持更丰富的数据类型(支持更复杂的应用场景)。
  • Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。
  • Redis 有灾难恢复机制。
  • Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
  • Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的。
  • Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。
  • Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
  • Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。

为什么要用 Redis/为什么要用缓存?

  • 高性能
    • 假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
  • 高并发一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 Redis 的情况,Redis 集群的话会更高)。

常见的缓存读写策略

  • Cache Aside Pattern(旁路缓存模式)
  • Read/Write Through Pattern(读写穿透)
  • Write Behind Pattern(异步缓存写入)
    • Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。

Redis 应用

  • 分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。
  • 限流:一般是通过 Redis + Lua 脚本的方式来实现限流。
  • 消息队列:Redis 自带的 List 数据结构可以作为一个简单的队列使用。
  • 延时队列:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
  • 分布式 Session :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。

Redis5 种基础数据类型

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

Redis 3 种特殊数据类型

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

Redis持久化机制

  • RDB 持久化
    • Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
    • Redis 提供了两个命令来生成 RDB 快照文件:
      • save : 同步保存操作,会阻塞 Redis 主线程;
      • bgsave : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
  • AOF 持久化

    • 开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。

    • AOF 持久化方式有哪些?

      • appendfsync always:主线程调用 write 执行写操作后,后台线程( aof_fsync 线程)立即会调用 fsync 函数同步 AOF 文件(刷盘),fsync 完成后线程返回,这样会严重降低 Redis 的性能(write + fsync)。
      • appendfsync everysec:主线程调用 write 执行写操作后立即返回,由后台线程( aof_fsync 线程)每秒钟调用 fsync 函数(系统调用)同步一次 AOF 文件(write+fsync,fsync间隔为 1 秒)
      • appendfsync no:主线程调用 write 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write但不fsync,fsync 的时机由操作系统决定)。
    • AOF 为什么是在执行完命令之后记录日志?
      • 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
      • 在命令执行完之后再记录,不会阻塞当前的命令执行。
    • AOF 重写
      • 当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
    • AOF 校验机制
      • AOF 校验机制是 Redis 在启动时对 AOF 文件进行检查,以判断文件是否完整,是否有损坏或者丢失的数据。这个机制的原理其实非常简单,就是通过使用一种叫做 校验和(checksum) 的数字来验证 AOF 文件。这个校验和是通过对整个 AOF 文件内容进行 CRC64 算法计算得出的数字。如果文件内容发生了变化,那么校验和也会随之改变。因此,Redis 在启动时会比较计算出的校验和与文件末尾保存的校验和(计算的时候会把最后一行保存校验和的内容给忽略点),从而判断 AOF 文件是否完整。如果发现文件有问题,Redis 就会拒绝启动并提供相应的错误信息。AOF 校验机制十分简单有效,可以提高 Redis 数据的可靠性。

Redis 线程模型

  • Redis 单线程模型
    • Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型 ,这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
    • Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
  • Redis6.0 之前为什么不使用多线程
    • 单线程编程容易并且更容易维护;
    • Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
  • Redis6.0 之后为何引入了多线程?
    • Redis6.0 引入多线程主要是为了提高网络 IO 读写性能
  • Redis 后台线程
    • 通过 bio_close_file 后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。
    • 通过 bio_aof_fsync 后台线程调用 fsync 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。
    • 通过 bio_lazy_free后台线程释放大对象(已删除)占用的内存空间。

Redis 内存管理

  • Redis 给缓存数据设置过期时间有啥用
    • 有助于缓解内存的消耗
  • Redis 是如何判断数据是否过期的呢
    • Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
  • 过期的数据的删除策略
    • 惰性删除:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
    • 定期删除:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
  • Redis 内存淘汰机制
    • volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
    • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
    • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
    • allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
    • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
    • no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

Redis事务

  • Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
  • 如何使用 Redis 事务?
    • MULTI, EXEC
    • DISCARD
    • WATCH
  • Redis 事务不支持原子性
  • Redis 事务支持持久性

Redis 性能优化

  • 使用批量操作减少网络传输
    • 原生批量操作命令
    • pipeline
    • Lua 脚本
  • 大量 key 集中过期问题
    • 给 key 设置随机过期时间。
  • Redis bigkey(大 Key)
    • bigkey 会消耗更多的内存空间和带宽,还会造成阻塞问题。
  • Redis hotkey(热 Key)
    • 处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。
  • 慢查询命令
    • Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。
  • Redis 内存碎片
    • 为什么会有 Redis 内存碎片?
      • Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
      • 频繁修改 Redis 中的数据也会产生内存碎片。
    • 如何查看 Redis 内存碎片的信息?
      • 使用 info memory 命令即可查看 Redis 内存相关的信息。
    • 如何清理 Redis 内存碎片?
      • 直接通过 config set 命令将 activedefrag 配置项设置为 yes 即可。

Redis 生产问题

  • 缓存穿透
    • 大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
    • 解决办法
      • 缓存无效 key
      • 布隆过滤器
  • 缓存击穿
    • 缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
    • 解决办法
      • 设置热点数据永不过期或者过期时间比较长。
  • 缓存雪崩
    • 缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
    • 解决办法
      • 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
      • 设置不同的失效时间比如随机设置缓存的失效时间。

如何保证缓存和数据库数据的一致性?

  • 缓存失效时间变短(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。增加 cache
  • 更新重试机制(常用):如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。

Redis常见阻塞原因

  • O(n) 命令
  • SAVE 创建 RDB 快照
  • AOF 日志记录阻塞
  • AOF 刷盘阻塞
  • AOF 重写阻塞
  • 大 Key
  • CPU 竞争