数据库
本文参考 JavaGuide
SQL/NoSQL基础
什么是SQL
SQL 是一种结构化查询语言(Structured Query Language),提供一种从数据库中读写数据的简单有效的方法。几乎所有的主流关系数据库都支持 SQL ,适用性非常强。一些非关系型数据库也兼容 SQL 或者使用类似于 SQL 的查询语言。
DB/DBMS/DBS/DBA
- 数据库:数据库(DataBase,DB)是由数据库管理系统管理的数据的集合。
- 数据库管理系统:数据库管理系统(Database Management System,DBMS)是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。
- 数据库系统:数据库系统(Data Base System,DBS)通常由软件、数据库和数据管理员(DBA)组成。
- 数据库管理员:数据库管理员(Database Administrator,DBA)负责全面管理和控制数据库系统。
数据库术语
- 数据库(
database):保存有组织的数据的容器(通常是一个文件或一组文件)。 - 数据表(
table):某种特定类型数据的结构化清单。 - 模式(
schema):关于数据库和表的布局及特性的信息。模式定义了数据在表中如何存储,包含存储什么样的数据,数据如何分解,各部分信息如何命名等信息。数据库和表都有模式。 - 列(
column):表中的一个字段。所有表都是由一个或多个列组成的。 - 行(
row):表中的一个记录。 - 主键(
primary key):一列(或一组列),其值能够唯一标识表中每一行。
元组/码/候选码/主码/外码/主属性/非主属性概念
- 元组:元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。
- 码:码就是能唯一标识实体的属性,对应表中的列。
- 候选码:若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何子集都不能再标识,则称该属性组为候选码。例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。
- 主码:主码也叫主键。主码是从候选码中选出来的。 一个实体集中只能有一个主码,但可以有多个候选码。
- 外码:外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。
- 主属性:候选码中出现过的属性称为主属性。比如关系 工人(工号,身份证号,姓名,性别,部门). 显然工号和身份证号都能够唯一标示这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。
- 非主属性:不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。
ER图
ER 图(Entity Relationship Diagram,实体联系图),提供了表示实体类型、属性和联系的方法。ER图三要素:
- 实体:通常是现实世界的业务对象,也可以使用一些逻辑对象。比如对于一个校园管理系统,会涉及学生、教师、课程、班级等等实体。在 ER 图中,实体使用矩形框表示。
- 属性:即某个实体拥有的属性,属性用来描述组成实体的要素,在 ER 图中,属性使用椭圆形表示。
- 联系:即实体与实体之间的关系,在 ER 图中用菱形表示,这个关系不仅有业务关联关系,还能通过数字表示实体之间的数量对照关系。例如,一个班级会有多个学生就是一种实体间的联系。
数据库三范式
数据库设计的三个范式是指数据库设计的规范,是为了减少冗余数据,提高数据的存储效率。数据库设计的三个范式分别是:
- 1NF(第一范式):属性不可再分。
- 1NF 是所有关系型数据库的最基本要求 ,关系型数据库中创建的表一定满足第一范式。
- 2NF(第二范式): 1NF 的基础之上,消除了非主属性对于码的部分函数依赖。
- 3NF(第三范式): 3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖。
- 符合 3NF 要求的数据库设计,基本上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。
- 函数依赖:在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。
- 例子:学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖于(学号,身份证号);
- 部分函数依赖:如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。
- 例子:学生基本信息表 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(学号,姓名,系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。
主键/外键区别
主键(主码):主键用于唯一标识一个元组,不能有重复,不允许为空。一个表只能有一个主键。
外键(外码):外键用来和其他表建立联系用,外键是另一表的主键,外键是可以有重复的,可以是空值。一个表可以有多个外键。
为什么不推荐使用外键&级联
以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
- 增加了复杂型:每次做
DELETE或者UPDATE都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; - 增加了资源消耗:做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗资源;
- 对分库分表的支持不友好:分库分表时需要考虑外键的约束,很麻烦。
外键好处
- 保证了数据库数据的一致性和完整性;
- 级联操作方便,减轻了程序代码量;
存储过程
可以把存储过程看成是一些 SQL 语句的集合,中间加了点逻辑控制语句。存储过程是一种预编译的 SQL 语句,存储在数据库中,可以被多次调用。存储过程可以接收参数,可以返回结果,可以包含流程控制语句,可以实现一些复杂的逻辑。
存储过程一旦调试完成通过后就能稳定运行,另外,使用存储过程比单纯 SQL 语句执行要快,因为存储过程是预编译过的。但存储过程在互联网公司应用不多,因为存储过程难以调试和扩展,而且没有移植性,还会消耗数据库资源。
DROP/TRUNCATE/DELETE区别
- 用法不同
DROP(丢弃数据):DROP table 表名,直接将表都删除掉,在删除表的时候使用。TRUNCATE(清空数据):TRUNCATE table 表名,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。DELETE(删除数据):DELETE FROM 表名 WHERE 列名=值,删除某一行的数据,如果不加WHERE子句和TRUNCATE table 表名作用类似。
TRUNCATE 和 DELETE 只删除数据不删除表的结构(定义),DROP会删除表的结构。
- 属于不同的数据库语言
DROP和TRUNCATE是DDL(Data Definition Language)数据定义语言,DELETE是DML(Data Manipulation Language)数据操纵语言。
- 执行速度不同:
DROP > TRUNCATE > DELETE。
DELETE命令执行的时候会产生数据库的binlog日志,而日志记录是需要消耗时间的,但是也有个好处方便数据回滚恢复。TRUNCATE命令执行的时候不会产生数据库日志,因此比DELETE要快。但其需要把表的自增值重置和索引恢复到初始大小等。DROP命令会把表占用的空间全部释放掉。
数据库设计分为哪几个步
- 需求分析:分析用户的需求,包括数据、功能和性能需求。
- 概念结构设计:主要采用 E-R 模型进行设计,包括画 E-R 图。
- 逻辑结构设计:通过将 E-R 图转换成表,实现从 E-R 模型到关系模型的转换。
- 物理结构设计:主要是为所设计的数据库选择合适的存储结构和存取路径。
- 数据库实施:包括编程、测试和试运行
- 数据库的运行和维护:系统的运行与数据库的日常维护。
什么是NoSQL
NoSQL(Not Only SQL)泛指非关系型的数据库,主要针对的是键值、文档以及图形类型数据存储。并且,NoSQL 数据库天生支持分布式,数据冗余和数据分片等特性,旨在提供可扩展的高可用高性能数据存储解决方案。
一个常见的误解是 NoSQL 数据库或非关系型数据库不能很好地存储关系型数据。NoSQL 数据库可以存储关系型数据,只是与关系型数据库的存储方式不同。
NoSQL 数据库代表:HBase、Cassandra、MongoDB、Redis。
SQL/NoSQL区别
| SQL 数据库 | NoSQL 数据库 | |
|---|---|---|
| 数据存储模型 | 结构化存储,具有固定行和列的表格 | 非结构化存储。文档:JSON 文档,键值:键值对,宽列:包含行和动态列的表,图:节点和边 |
| ACID 属性 | 提供原子性、一致性、隔离性和持久性 (ACID) 属性 | 通常不支持 ACID 事务,为了可扩展、高性能进行了权衡,少部分支持比如 MongoDB 。不过,MongoDB 对 ACID 事务 的支持和 MySQL 还是有所区别的。 |
| 性能 | 性能通常取决于磁盘子系统。要获得最佳性能,通常需要优化查询、索引和表结构。 | 性能通常由底层硬件集群大小、网络延迟以及调用应用程序来决定。 |
| 扩展 | 垂直(使用性能更强大的服务器进行扩展)、读写分离、分库分表 | 横向(增加服务器的方式横向扩展,通常是基于分片机制) |
| 用途 | 普通企业级的项目的数据存储 | 用途广泛比如图数据库支持分析和遍历连接数据之间的关系、键值数据库可以处理大量数据扩展和极高的状态变化 |
| 查询语法 | 结构化查询语言 (SQL) | 数据访问语法可能因数据库而异 |
| 发展历程 | 开发于 1970 年代,重点是减少数据重复 | 开发于 2000 年代后期,重点是提升可扩展性,减少大规模数据的存储成本 |
| 例子 | Oracle、MySQL、Microsoft SQL Server、PostgreSQL | 文档:MongoDB、CouchDB,键值:Redis、DynamoDB,宽列:Cassandra、 HBase,图表:Neo4j、 Amazon Neptune、Giraph |
NoSQL优劣
优点
- 灵活性:提供灵活的架构,以实现更快速、更多的迭代开发,是存储半结构化和非结构化数据的理想之选。
- 可扩展性:通常被设计为通过使用分布式硬件集群来横向扩展,而不是通过添加昂贵和强大的服务器来纵向扩展。
- 高性能:NoSQL 数据库通常是为了提供高性能而设计的,因为它们通常是基于键值对的,而不是基于关系的。
- 功能强大:提供功能强大的 API 和数据类型,专门针对其各自的数据模型而构建。
缺点
- 一致性:NoSQL 数据库通常不支持 ACID 属性,因此在某些情况下可能会导致数据不一致。
- 缺乏标准化:NoSQL 数据库通常没有标准化的查询语言,这可能会导致开发人员需要学习多种不同的查询语言。
NoSQL分类
- 键值:键值数据库是一种较简单的数据库,其中每个项目都包含键和值。这是极为灵活的 NoSQL 数据库类型,因为应用可以完全控制
value字段中存储的内容,没有任何限制。Redis 和 DynanoDB 是两款非常流行的键值数据库。 - 文档:文档数据库中的数据被存储在类似于 JSON(JavaScript 对象表示法)对象的文档中,非常清晰直观。每个文档包含成对的字段和值。这些值通常可以是各种类型,包括字符串、数字、布尔值、数组或对象等,并且它们的结构通常与开发者在代码中使用的对象保持一致。MongoDB 就是一款非常流行的文档数据库。
- 图形:图形数据库旨在轻松构建和运行与高度连接的数据集一起使用的应用程序。图形数据库的典型使用案例包括社交网络、推荐引擎、欺诈检测和知识图形。Neo4j 和 Giraph 是两款非常流行的图形数据库。
- 宽列:宽列存储数据库非常适合需要存储大量的数据。Cassandra 和 HBase 是两款非常流行的宽列存储数据库。
数据库分类
DDL/DML/DCL/TCL
DDL(Data Definition Language)数据定义语言:用来定义数据库对象:数据库、表、列等。关键字:CREATE、ALTER、DROP、TRUNCATE。- DDL 不涉及表中数据的操作,只是对表的定义、结构的修改。
DML(Data Manipulation Language)数据操纵语言:用来操作数据库中的记录,对数据库其中的对象和数据运行访问工作的编程语句。核心指令:INSERT、UPDATE、DELETE、SELECT,这四个指令合称CRUD(Create, Read, Update, Delete),即增删改查。- DML 只是对表内部数据的操作,而不涉及到表的定义、结构的修改,更不会涉及到其他对象。
TCL(Transaction Control Language)事务控制语言:用于管理数据库中的事务。关键字:COMMIT、ROLLBACK。DCL(Data Control Language)数据控制语言:用来控制数据库用户的访问权限。关键字:GRANT、REVOKE。
SQL三种注释方式
- 单行注释:
-- 注释内容或# 注释内容。 - 多行注释:
/* 注释内容 */。 - 行尾注释:
SELECT * FROM table_name; -- 注释内容。
DML语句✅
DML(Data Manipulation Language) 数据操纵语言:用来操作数据库中的记录,对数据库其中的对象和数据运行访问工作的编程语句。核心指令:INSERT、UPDATE、DELETE、SELECT,这四个指令合称CRUD(Create, Read, Update, Delete),即增删改查。
增删改查CRUD
增删改查,又称为 CRUD,数据库基本操作中的基本操作。
插入数据INSERT INTO 语句用于向表中插入新记录。
1 | -- 插入完整的行 |
更新数据UPDATE 语句用于更新表中的记录。
1 | UPDATE user |
删除数据
DELETE语句用于删除表中的记录。TRUNCATE TABLE可以清空表,也就是删除所有行。
1 | -- 删除表中的指定数据 |
查询数据
SELECT语句用于从数据库中查询数据。DISTINCT用于返回唯一不同的值。它作用于所有列,也就是说所有列的值都相同才算相同。LIMIT限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。ASC:升序(默认)DESC:降序
1 | -- 查询单列 |
排序
ORDER BY用于对结果集按照一个列或者多个列进行排序。默认按照升序对记录进行排序,如果需要按照降序对记录进行排序,可以使用DESC关键字。ORDER BY对多列排序的时候,先排序的列放前面,后排序的列放后面。并且,不同的列可以有不同的排序规则。
1 | SELECT * FROM products |
分组
GROUP BY子句将记录分组到汇总行中。GROUP BY为每个组返回一个记录。GROUP BY通常还涉及聚合COUNT,MAX,SUM,AVG等。GROUP BY可以按一列或多列进行分组。GROUP BY按分组字段进行排序后,ORDER BY可以以汇总字段来进行排序。
1 | -- 分组 |
HAVING
HAVING用于对汇总的GROUP BY结果进行过滤。HAVING一般都是和GROUP BY连用。WHERE和HAVING可以在相同的查询中。
1 | -- 使用 WHERE 和 HAVING 过滤数据 |
WHERE/HAVING对比
WHERE:过滤过滤指定的行,后面不能加聚合函数(分组函数)。WHERE在GROUP BY前。HAVING:过滤分组,一般都是和GROUP BY连用,不能单独使用。HAVING在GROUP BY之后。
子查询
MySQL4.1才开始支持子查询(子查询需要放入括号()内)
子查询是嵌套在较大查询中的 SQL 查询,也称内部查询或内部选择,包含子查询的语句也称为外部查询或外部选择。简单来说,子查询就是指将一个 SELECT 查询(子查询)的结果作为另一个 SQL 语句(主查询)的数据来源或者判断条件。
子查询可以嵌入 SELECT、INSERT、UPDATE 和 DELETE 语句中,也可以和 =、<、>、IN、BETWEEN、EXISTS 等运算符一起使用。
子查询常用在 WHERE 子句和 FROM 子句后边:
- 当用于
WHERE子句时,根据不同的运算符,子查询可以返回单行单列、多行单列、单行多列数据。子查询就是要返回能够作为WHERE子句查询条件的值。1
2
3
4
5
6SELECT column_name [, column_name ]
FROM table1 [, table2 ]
WHERE column_name operator
(SELECT column_name [, column_name ]
FROM table1 [, table2 ]
[WHERE]) - 当用于
FROM子句时,一般返回多行多列数据,相当于返回一张临时表,这样才符合FROM后面是表的规则。这种做法能够实现多表联合查询。1
2
3
4
5SELECT column_name [, column_name ]
FROM (SELECT column_name [, column_name ]
FROM table1 [, table2 ]
[WHERE]) as temp_table_name
WHERE condition
子查询的子查询
首先会执行子查询,然后将子查询的结果作为外部查询的条件,再执行外部查询。
1 | SELECT cust_name, cust_contact |
WHERE
WHERE子句用于过滤记录,即缩小访问数据的范围。WHERE后跟一个返回true或false的条件。WHERE可以与SELECT,UPDATE和DELETE一起使用。1
2
3
4
5
6
7
8
9
10
11
12-- SELECT语句中的WHERE
SELECT * FROM Customers
WHERE cust_name = 'Kids Place';
-- UPDATE语句中的WHERE
UPDATE Customers
SET cust_name = 'Jack Jones'
WHERE cust_name = 'Kids Place';
-- DELETE语句中的WHERE
DELETE FROM Customers
WHERE cust_name = 'Kids Place';- 可以在
WHERE子句中使用的操作符。运算符 描述 =等于 <>不等于。注释:在 SQL 的一些版本中,该操作符可被写成 !=>大于 <小于 >=大于等于 <=小于等于 BETWEEN在某个范围内 LIKE搜索某种模式 IN指定针对某个列的多个可能值
IN/BETWEEN
IN操作符在WHERE子句中使用,作用是在指定的几个特定值中任选一个值。BETWEEN操作符在WHERE子句中使用,作用是选取介于某个范围内的值。
1 | -- IN示例 |
AND/OR/NOT
AND、OR、NOT是用于对过滤条件的逻辑处理指令。AND优先级高于OR,为了明确处理顺序,可以使用()。AND操作符表示左右条件都要满足。OR操作符表示左右条件满足任意一个即可。NOT操作符用于否定一个条件。
1 | -- AND示例 |
LIKE
LIKE操作符在WHERE子句中使用,作用是确定字符串是否匹配模式。- 只有字段是文本值时才使用
LIKE。 LIKE支持两个通配符匹配选项:%和_。- 不要滥用通配符,通配符位于开头处匹配会非常慢。
%表示任何字符出现任意次数。_表示任何字符出现一次。
1 | -- %示例 |
连接JOIN
JOIN 子句用于将两个或者多个表联合起来进行查询。连接表时需要在每个表中选择一个字段,并对这些字段的值进行比较,值相同的两条记录将合并为一条。连接表的本质就是将不同表的记录合并起来,形成一张新表(临时表,仅存在于本次查询中)。
语法:
1 | SELECT table1.column1, table2.column2... |
当两个表中有同名的字段时,为了帮助数据库引擎区分是哪个表的字段,在书写同名字段名时需要加上表名。如果书写的字段名在两个表中是唯一的,也可以不使用以上格式,只写字段名即可。
如果两张表的关联字段名相同,也可以使用 USING子句来代替 ON。
1 | # JOIN....ON |
内连接/外连接
内连接:INNER JOIN或者JOIN,只返回两个表中满足连接条件的行,是连接表的默认方式。外连接:左外连接:LEFT JOIN/LEFT OUTER JOIN,返回左表中所有记录和右表中满足连接条件的记录。右外连接:RIGHT JOIN/RIGHT OUTER JOIN,返回右表中所有记录和左表中满足连接条件的记录。全外连接:FULL JOIN/FULL OUTER JOIN,返回左表和右表中所有记录,如果没有匹配的记录,结果集中会包含NULL值。
1 | /* |
ON/WHERE区别
ON子句是在执行JOIN操作时使用的,它指定了两个表之间的连接条件,决定临时表的生成。WHERE子句是在从临时表中检索数据时使用的,它指定了检索数据的条件。
组合UNION
UNION 运算符将两个或更多查询的结果组合起来,并生成一个结果集,其中包含来自 UNION 中参与查询的提取行。
UNION 基本规则:
- 所有查询的列数和列顺序必须相同。
- 每个查询中涉及表的列的数据类型必须相同或兼容。
- 通常返回的列名取自第一个查询。
UNION默认选取不同的值。如果允许重复的值,用UNION ALL。
1 | SELECT column1, column2, ... |
WHERE,GROUP BY,HAVING,ORDER BY执行顺序
- FROM:首先,指定从哪个表(或表的联接结果)中选择数据。
- WHERE:接着,根据WHERE子句的条件过滤掉不满足条件的行。这一步是在选择列(SELECT)之前进行的,因为选择哪些行显然会影响最终的结果集。
- GROUP BY:然后,如果查询中包含了GROUP BY子句,那么结果集会根据指定的列或列的组合进行分组。这一步是在选择列(SELECT)和可能的聚合函数(如SUM、AVG等)计算之后进行的,但在HAVING子句(如果有的话)之前。
- HAVING(如果存在):HAVING子句用于过滤分组后的结果集,其逻辑类似于WHERE子句,但它是应用于分组结果的。
- SELECT:此时,根据SELECT子句指定的列或表达式,从前面步骤得到的结果集中选择数据。如果查询中包含了聚合函数,那么这些函数现在会被计算。
- DISTINCT(如果存在):如果查询中使用了DISTINCT关键字,那么结果集中的重复行将被移除。
- ORDER BY:最后,根据ORDER BY子句指定的列或表达式对结果集进行排序。需要注意的是,ORDER BY是在所有其他处理(除了LIMIT和OFFSET)完成后才进行的,因为排序是基于完整的、已经过滤和分组(如果有的话)的结果集进行的。
- LIMIT/OFFSET(如果存在):最后,根据LIMIT和OFFSET子句(如果有的话)限制结果集的大小或跳过结果集中的某些行。
虽然这个顺序是从逻辑上理解的,但重要的是要记住,实际的执行计划可能会根据查询优化器的决策而有所不同。优化器会考虑多种因素,如索引的存在与否、数据的统计信息、表的大小和形状等,以选择最高效的执行路径。
DDL语言✅
DDL(Data Definition Language) 数据定义语言:用来定义数据库对象:数据库、表、列等。关键字:CREATE、ALTER、DROP、TRUNCATE。
数据库DATABSE
- 创建数据库:
CREATE DATABASE 数据库名; - 删除数据库:
DROP DATABASE 数据库名; - 选择数据库:
USE 数据库名; - 查看数据库:
SHOW DATABASES;
数据表TABLE
1 | -- 普通创建 |
视图VIEW
在SQL中,视图VIEW是一种虚拟表,作用类似于一个保存了查询结果的表。视图并不实际存储数据,而是存储一个查询,当访问视图时,数据库会动态地执行这个查询来生成结果。
作用:
- 简化复杂查询:视图可以将复杂的查询封装起来,使得用户可以通过简单的SELECT语句来访问复杂的数据。
- 提高数据安全性:用来限制用户访问敏感数据。
- 数据抽象和逻辑独立性:视图提供了一层抽象,使得底层表的结构变化不会影响到用户。
- 重用SQL逻辑:可以将经常使用的查询逻辑存储在视图中,从而避免在多个地方重复相同的查询逻辑。
1 | -- 创建视图 |
索引INDEX
索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。
优点
- 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
缺点
- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
- 索引需要使用物理文件存储,也会耗费一定空间。
1 | -- 创建索引 |
约束
SQL 约束用于规定表中的数据规则。如果存在违反约束的数据行为,行为会被约束终止。约束可以在创建表时规定(通过 CREATE TABLE 语句),或者在表创建之后规定(通过 ALTER TABLE 语句)。
约束类型
NOT NULL:指示某列不能存储NULL值。UNIQUE:保证某列的每行必须有唯一的值。PRIMARY KEY:NOT NULL和UNIQUE的结合。确保某列(或两个列多个列的结合)有唯一标识,有助于更容易更快速地找到表中的一个特定的记录。FOREIGN KEY:保证一个表中的数据匹配另一个表中的值的参照完整性。CHECK:保证列中的值符合指定的条件。DEFAULT:规定没有给列赋值时的默认值。
1 | CREATE TABLE Users ( |
TCL语句✅
TCL(Transaction Control Language) 事务控制语言:用于管理数据库中的事务。关键字:COMMIT、ROLLBACK。
事务TRANSACTION
事务是一个操作序列,这些操作要么都执行,要么都不执行,是数据库管理系统(DBMS)执行的一个操作单元。事务是数据库维护完整性的单位,在事务中的所有操作要么全部完成,要么全部不完成。
事务的四个特性(ACID)
原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败回滚。一致性(Consistency):执行事务前后,数据保持一致。隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。持久性(Durability):事务成功结束后,对数据库的修改是永久的,即使数据库发生故障也不应该丢失。
MySQL 默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
注意:不能回退
CREATE或DROP等 DDL 语句。也不能回退SELECT语句,回退也没意义。针对每个连接,通过设置set autocommit=0取消自动提交,set autocommit=1才会自动提交。
1 | -- 开始事务 |
DCL语句✅
DCL(Data Control Language) 数据控制语言:用来控制数据库用户的访问权限。关键字:GRANT、REVOKE。
权限管理
- 授予用户帐户权限,用
GRANT命令。- 在
GRANT关键字后指定一个或多个权限。如果授予用户多个权限,则每个权限由逗号分隔。 ON privilege_level确定权限应用级别。MySQL 支持 global(*.*),database(database.*),table(database.table)和列级别。如果使用列权限级别,必须在每个权限之后指定一个或逗号分隔列的列表。user是要授予权限的用户。如果用户已存在,则GRANT语句将修改其权限。否则,GRANT语句将创建一个新用户。可选子句IDENTIFIED BY允许您为用户设置新的密码。REQUIRE tsl_option指定用户是否必须通过 SSL,X059 等安全连接连接到数据库服务器。- 可选
WITH GRANT OPTION子句允许您授予其他用户或从其他用户中删除您拥有的权限。此外,您可以使用WITH子句分配 MySQL 数据库服务器的资源,例如,设置用户每小时可以使用的连接数或语句数。这在 MySQL 共享托管等共享环境中非常有用。
- 在
- 撤销用户的权限,用
REVOKE命令。- 在
REVOKE关键字后面指定要从用户撤消的权限列表。您需要用逗号分隔权限。 - 指定在
ON子句中撤销特权的特权级别。 - 指定要撤消
FROM子句中的权限的用户帐户。
- 在
GRANT 和 REVOKE 可在几个层次上控制访问权限:
- 整个服务器,使用
GRANT ALL和REVOKE ALL; - 整个数据库,使用
ON database.*; - 特定的表,使用
ON database.table; - 特定的列;
- 特定的存储过程。
1 | -- GRANT用法 |
游标CURSOR
游标(cursor)是一个存储在 DBMS 服务器上的数据库查询,它不是一条 SELECT 语句,而是被该语句检索出来的结果集。在存储过程中使用游标可以对一个结果集进行移动遍历。 游标主要用于交互式应用,其中用户需要滚动屏幕上的数据,并对数据进行浏览或做出更改。
触发器TRIGGER
触发器是一种与表操作有关的数据库对象,当触发器所在表上出现指定事件时,将调用该对象,即表的操作事件触发表上的触发器的执行。触发器是一种特殊的存储过程,它是由一个事件触发的,一个事件可以是一个 INSERT、UPDATE 或 DELETE 语句。
优点
- SQL 触发器提供了另一种检查数据完整性的方法。
- SQL 触发器可以捕获数据库层中业务逻辑中的错误。
- SQL 触发器提供了另一种运行计划任务的方法。通过使用 SQL 触发器,不必等待运行计划任务,因为在对表中的数据进行更改之前或之后会自动调用触发器。
- SQL 触发器对于审计表中数据的更改非常有用。
缺点
- SQL 触发器可能会增加数据库服务器的开销。
- 从客户端应用程序调用和执行 SQL 触发器是不可见的,因此很难弄清楚数据库层中发生了什么。
在 MySQL 5.7.2 版之前,可以为每个表定义最多六个触发器,之后版本可为同一触发事件和操作时间定义多个触发器。
BEFORE INSERT:在将数据插入表格之前激活。AFTER INSERT:将数据插入表格后激活。BEFORE UPDATE:在更新表中的数据之前激活。AFTER UPDATE:更新表中的数据后激活。BEFORE DELETE:在从表中删除数据之前激活。AFTER DELETE:从表中删除数据后激活。
NEW 和 OLD:
- MySQL 中定义了
NEW和OLD关键字,用来表示触发器的所在表中,触发了触发器的那一行数据。 - 在
INSERT型触发器中,NEW用来表示将要(BEFORE)或已经(AFTER)插入的新数据; - 在
UPDATE型触发器中,OLD用来表示将要或已经被修改的原数据,NEW用来表示将要或已经修改为的新数据; - 在
DELETE型触发器中,OLD用来表示将要或已经被删除的原数据; - 使用方法:
NEW.columnName(columnName为相应数据表某一列名)
触发器语法
1 | -- 创建触发器 |
说明:
trigger_name:触发器名trigger_time:触发器的触发时机。取值为BEFORE或AFTER。trigger_event:触发器的监听事件。取值为INSERT、UPDATE或DELETE。table_name:触发器的监听目标。指定在哪张表上建立触发器。FOR EACH ROW:行级监视,Mysql 固定写法,其他 DBMS 不同。trigger_statements:触发器执行动作。是一条或多条 SQL 语句的列表,列表内的每条语句都必须用分号;来结尾。
当触发器的触发条件满足时,将会执行 BEGIN 和 END 之间的触发器执行动作。
1 | -- 创建触发器 |
SQL注入
SQL 注入(SQL Injection)是一种常见的网络安全漏洞,它通过在应用程序的输入字段中插入恶意的SQL代码,使得攻击者可以操纵数据库查询,获取、修改或删除数据库中的数据。
SQL 注入通常发生在应用程序未对用户输入进行充分的验证或转义的情况下。攻击者可以通过在输入字段中输入特定的SQL语句,来改变原本预期的SQL查询,从而达到攻击的目的。
SQL 注入的类型
- 基于错误的SQL注入(Error-based SQL Injection):利用数据库返回的错误信息来推断数据库的结构。
- 基于联合的SQL注入(Union-based SQL Injection):使用
UNION操作符将恶意查询与合法查询组合,以获取数据库中的更多数据。 - 基于布尔的盲注(Boolean-based Blind SQL Injection):通过观察应用程序响应的变化来推断数据库中的信息。
- 基于时间的盲注(Time-based Blind SQL Injection):通过在SQL语句中加入延迟操作,根据响应时间推断数据库中的信息。
SQL 注入的防护措施
- 参数化查询:使用参数化查询(Prepared Statements),确保SQL语句与用户输入分离。
- 输入验证和转义:对用户输入进行严格的验证和转义,防止恶意代码注入。
- 最小权限原则:限制数据库用户的权限,确保即使发生注入攻击,攻击者也无法获得超出必要范围的权限。
- 使用ORM框架:使用对象关系映射(ORM)框架,减少直接编写SQL查询的机会。
示例
以下是一个简单的SQL注入示例:
假设有一个登录页面,用户输入用户名和密码,然后系统会执行如下查询:
1 | SELECT * FROM users WHERE username = 'user' AND password = 'pass'; |
如果用户输入 user' OR '1'='1 作为用户名,输入 pass 作为密码,那么查询变成了:
1 | SELECT * FROM users WHERE username = 'user' OR '1'='1' AND password = 'pass'; |
由于 '1'='1' 永远为真,这条查询将返回所有用户的数据,从而绕过了认证。
结论
SQL 注入是一种非常危险的攻击手段,但通过采取适当的防护措施,如参数化查询和严格的输入验证,可以有效地防止这种攻击。
SQL查询场景题
一张学生表和成绩表,关系是一对一,但是有的同学没有成绩,查出没有成绩的同学。
1 | SELECT s.* |
学生成绩表,查找第三名,可能会有并列第三
1 | SELECT student_id, score |
MySQL
MySQL 是一种关系型数据库,主要用于持久化存储系统中的一些数据比如用户信息。MySQL 是开源免费并且比较成熟的数据库,因此被大量使用在各种系统中。任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL 的默认端口号是3306。
MySQL基础✅
MySQL命名规范
MySQL的命名规范对于数据库的设计、维护以及团队协作来说非常重要。一个清晰、一致的命名规范可以提高数据库的可读性、可维护性和可扩展性。
- 数据库命名
- 使用小写字母:虽然MySQL在Linux环境下对大小写不敏感,但在Windows环境下是敏感的。为了保持一致性,建议使用小写字母。
- 简短且描述性强:数据库名应该简短且能描述数据库的内容。
- 避免使用MySQL保留字:如user、select等。
- 表命名
- 使用复数形式:表名通常表示的是集合,因此使用复数形式更为直观。
- 小写字母加下划线:如users、order_details。
- 避免使用MySQL保留字。
- 表名前缀:对于大型项目,可以使用前缀来区分不同模块或功能的表,如sys_users、prd_products。
- 字段命名
- 小写字母加下划线:如user_id、first_name。
- 避免使用MySQL保留字。
- 使用有意义的名称:字段名应该能够清晰地描述字段的内容。
- 布尔类型字段:可以使用is_或has_作为前缀,如is_active、has_permission。
- 索引命名
- 主键索引:通常使用pk_作为前缀,后跟表名(或表名的缩写)和字段名,如pk_users_id。
- 唯一索引:使用uq_作为前缀,后跟表名和字段名,如uq_users_email。
- 普通索引:可以使用idx_作为前缀,后跟表名和字段名,如idx_products_category_id。
- 全文索引:使用ft_作为前缀,后跟表名和字段名,如ft_articles_content。
- 视图、存储过程和函数命名
- 视图:使用v_作为前缀,后跟视图名,如v_user_info。
- 存储过程:使用sp_作为前缀,后跟过程名,如sp_get_user_by_id。
- 函数:使用fn_作为前缀,后跟函数名,如fn_calculate_age。
- 注意事项
- 避免使用空格、特殊字符和MySQL保留字。
- 保持命名的一致性:在整个项目中,对于相同类型的对象(如表、字段、索引等),应该使用相同的命名规则和风格。
- 考虑可读性:命名应该清晰、简洁,并尽可能描述对象的内容或用途。
MySQL优点
- 成熟稳定,功能完善。开源免费。文档丰富,既有详细的官方文档,又有非常多优质文章可供参考学习。
- 开箱即用,操作简单,维护成本低。
- 兼容性好,支持常见的操作系统,支持多种开发语言。
- 社区活跃,生态完善。事务支持优秀, InnoDB 存储引擎默认使用 REPEATABLE-READ 并不会有任何性能损失,并且,InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的。
- 支持分库分表、读写分离、高可用。
什么是关系型数据库
关系型数据库(RDB,Relational Database)是一种建立在关系模型基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。关系型数据库中,数据都被存放在了各种表中(比如用户表),表中的每一行就存放着一条数据(比如一个用户的信息)。
大部分关系型数据库都使用 SQL 来操作数据库中的数据。并且,大部分关系型数据库都支持事务的四大特性(ACID)。常见关系型数据库:MySQL、PostgreSQL、Oracle、SQL Server、SQLite(微信本地的聊天记录的存储就是用的 SQLite) ……
MySQL字段类型
简单分为三大类:数值类型、字符串类型、日期和时间类型。
- 数值类型:
- 整形:
TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT。 - 浮点型:
FLOAT、DOUBLE。 - 定点数:
DECIMAL。
- 整形:
- 字符串类型:
- 常用:
CHAR、VARCHAR、TEXT。 - 不常用:
TINYTEXT、MEDIUMTEXT、LONGTEXT、TINYBLOB、BLOB、MEDIUMBLOB和LONGBLOB等。
- 常用:
- 日期和时间类型:
DATE、TIME、YEAR、DATETIME、TIMESTAMP等。
整数类型的UNSIGNED属性
整数类型可以使用可选的 UNSIGNED 属性来表示不允许负值的无符号整数。使用 UNSIGNED 属性可以将正整数的上限提高一倍,因为它不需要存储负数值。
TINYINT UNSIGNED取值范围是 0 ~ 255,TINYINT是 -128 ~ 127。INT UNSIGNED取值范围是 0 ~ 4,294,967,295,INT是 -2,147,483,648 ~ 2,147,483,647。
对于从 0 开始递增的 ID 列,使用 UNSIGNED 属性可以非常适合,因为不允许负值并且可以拥有更大的上限范围,提供了更多的 ID 值可用。
CHAR/VARCHAR区别
CHAR是一种固定长度的类型,VARCHAR是一种可变长度的类型。CHAR在存储时会在右边填充空格以达到指定的长度,检索时会去掉空格;VARCHAR在存储时需要使用 1 或 2 个额外字节记录字符串的长度,检索时不需要处理。CHAR(M)和VARCHAR(M)的 M 都代表能够保存的字符数的最大值,无论是字母、数字还是中文,每个都只占用一个字符。CHAR适合存储长度较短或者长度都差不多的字符串,例如 Bcrypt 算法、MD5 算法加密后的密码、身份证号码。VARCHAR适合存储长度不确定或者差异较大的字符串,例如用户昵称、文章标题等。
VARCHAR(100)/VARCHAR(10)区别
- 前者最大长度为 100,后者最大长度为 10。
- 二者存储相同的字符串所占用的存储空间是一样的。
VARCHAR类型在内存中操作时,通常会分配固定大小的内存块来保存值,即使用字符类型中定义的长度。VARCHAR(100)在内存中操作时会消耗更多内存,例如在排序的时候,每个元素都占用 100字节内存。
DECIMAL和FLOAT/DOUBLE区别
DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。
为什么不推荐使用TEXT/BLOB类型
TEXT用于存储长文本数据,如博客数据。BLOB用于存储二进制大对象,例如图片、音视频等文件。
这两种类型有如下缺陷:
- 不能有默认值。
- 在使用临时表时无法使用内存临时表,只能在磁盘上创建临时表。
- 检索效率较低。
- 不能直接创建索引,需要指定前缀长度。
- 可能会消耗大量的网络和 IO 带宽。
- 可能导致表上的 DML 操作变慢。
DATETIME/TIMESTAMP区别
DATETIME没有时区信息,TIMESTAMP和时区有关。TIMESTAMP只需要使用 4 个字节的存储空间,但是DATETIME需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,TIMESTAMP表示的时间范围更小。DATETIME:1000-01-01 00:00:00 ~ 9999-12-31 23:59:59TIMESTAMP:1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07 UTC
TIMESTAMP在存储时会自动转换为 UTC(Coordinated Universal Time, 全球标准时间) 时间,检索时会转换为当前时区的时间。
NULL和’’区别
NULL表示未知的值,''表示空字符串。SELECT NULL=NULL的结果为false,但在使用DISTINCT,GROUP BY,ORDER BY时又认为NULL是相等的。
''长度为0,不占用空间,NULL占用空间。- 查询
NULL值时,必须使用IS NULL或IS NOT NULL来判断,而不能使用=、!=、<、>之类的比较运算符。而''是可以使用这些比较运算符的。 NULL会影响聚合函数的结果。SUM、AVG、MIN、MAX等聚合函数会忽略NULL值。COUNT的处理方式取决于参数的类型。如果参数是*(COUNT(*)),则会统计所有的记录数,包括NULL值;如果参数是某个字段名(COUNT(列名)),则会忽略NULL值,只统计非空值的个数。
不推荐使用NULL作为列默认值。
Boolean类型如何在MySQL中存储
MySQL 中没有布尔类型,可以使用 TINYINT 类型来存储布尔值。TINYINT(1) 可以存储 0 或 1,分别表示 false 和 true。
MySQL连接池✅
MySQL连接池(Connection Pool)是一种提高数据库访问效率和资源利用率的技术。它通过在应用程序和数据库之间维护一个预先建立的数据库连接集合,来减少创建和销毁连接的开销,同时限制并发连接数,从而优化数据库性能。有以下有点:
- 减少连接开销:通过重用现有的连接,避免了每次请求时建立和销毁连接的开销。
- 提高响应速度:由于减少了连接的时间,因此可以更快地响应数据库请求。
- 资源控制:通过限制并发连接数,可以保护数据库服务器,防止因过多的连接请求而导致的资源耗尽。
- 提高数据库性能:通过优化连接管理,可以减轻数据库服务器的负载,从而提高整体性能。
MySQL基础架构✅
MySQL 基础架构主要分为 Server 层和存储引擎层:
- Server 层:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块
binlog日志模块。 - 存储引擎:主要负责数据的存储和读取,采用可以替换的插件式架构,支持
InnoDB、MyISAM、Memory等多个存储引擎,其中InnoDB引擎有自有的日志模块redolog模块。现在最常用的存储引擎是InnoDB,它从 MySQL 5.5 版本开始就被当做默认存储引擎了。

- 连接器:身份认证和权限相关(登录 MySQL 的时候)。
- 查询缓存:执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
- 分析器:没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
- 优化器:按照 MySQL 认为最优的方案去执行。
- 执行器:执行语句,然后从存储引擎返回数据。
- 插件式存储引擎:主要负责数据的存储和读取,采用的是插件式架构,支持
InnoDB、MyISAM、Memory等多种存储引擎。
连接器。
主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作,如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开,即使管理员修改了该用户的权限,该用户也是不受影响的。查询缓存(MySQL 8.0 版本后移除)。
主要用来缓存所执行的SELECT语句以及该语句的结果集。连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 SQL 语句是否执行过,以Key-Value的形式缓存在内存中,Key是查询语句,Value是结果集。如果缓存Key被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。
MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁,假如对一个表更新的话,这个表上的所有的查询缓存都会被清空。对于不经常更新的数据来说,使用缓存还是可以的。分析器。
MySQL 没有命中缓存,那么就会进入分析器,分析器主要是用来分析 SQL 语句是来干嘛的。分两步:- 第一步,词法分析,一条 SQL 语句有多个字符串组成,首先要提取关键字,比如
SELECT,提出查询的表,提出字段名,提出查询条件等等。做完这些操作后,就会进入第二步。 - 第二步,语法分析,主要就是判断输入的 SQL 是否正确,是否符合 MySQL 的语法。
- 第一步,词法分析,一条 SQL 语句有多个字符串组成,首先要提取关键字,比如
优化器。
优化器以它认为的最优的执行方案去执行,比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等,经过了优化器之后语句具体该如何执行就已经定下来。执行器。
当选择了执行方案后,MySQL 就准备开始执行。首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。
SQL语句在MySQL中的执行过程
查询语句
查询语句的执行流程如下:权限校验—>(如果命中缓存)查询缓存—>分析器—>优化器—>权限校验—>执行器—>引擎
1 | SELECT * FROM tb_student A WHERE A.age='18' AND A.name='张三'; |
- 检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 SQL 语句为
key在内存中查询是否有结果,如果有直接返回,如果没有,执行下一步。 - 通过分析器进行词法分析,提取 SQL 语句的关键元素,比如提取上面这个语句是查询
SELECT,提取需要查询的表名为tb_student,需要查询所有的列,查询条件是这个表的 id=’1’。然后判断这个 SQL 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。 - 优化器进行确定执行方案,上面的 SQL 语句,可以有两种执行方案:
- a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。
- b.先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。
- 进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。
更新语句(增加,修改,删除)
更新语句执行流程如下:分析器—->权限校验—->执行器—>引擎—redo log(prepare 状态)—>binlog—>redo log(commit 状态)
1 | UPDATE tb_student A SET A.age='19' WHERE A.name=' 张三 '; |
MySQL自带的日志模块是 binlog,以 InnoDB 作为存储引擎介绍,其自带了一个redo log 日志模块,用来保证数据的一致性的。
- 先查询到张三这一条数据,不会走查询缓存,因为更新语句会导致与该表相关的查询缓存失效。
- 拿到查询的语句,把
age改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB引擎把数据保存在内存中,同时记录redo log,此时redo log进入prepare状态,然后告诉执行器,执行完成了,随时可以提交。 - 执行器收到通知后记录
binlog,然后调用引擎接口,提交redo log为提交状态。 - 更新完成。
MySQL存储引擎✅
MySQL支持哪些存储引擎?
MySQL 支持多种存储引擎,常见的存储引擎有 InnoDB、MyISAM、Memory、CSV、Archive、Blackhole、NDB、Merge、Federated、Example 等。
MySQL 5.5.5 之前,MyISAM 是默认存储引擎。5.5.5 版本之后,InnoDB 是默认存储引擎。
MySQL存储引擎架构
MySQL 存储引擎采用的是插件式架构,支持多种存储引擎,甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。存储引擎是基于表的,而不是数据库。也可以自定义存储引擎。
MyISAM/InnoDB存储引擎区别
MyISAM只支持表级锁,而InnoDB支持行级锁和表级锁,默认为行级锁。MyISAM不提供事务支持。InnoDB提供事务支持,实现了 SQL 标准定义了四个隔离级别,具有提交(commit)和回滚(rollback)事务的能力。并且,InnoDB默认使用的REPEATABLE-READ(可重读)隔离级别是可以解决幻读问题发生的(基于MVCC和Next-Key Lock)。MyISAM不支持外键,而InnoDB支持。外键对于维护数据一致性非常有帮助,但是对性能有一定的损耗,因此一般不建议使用。MyISAM不支持数据库异常崩溃后的安全恢复,而InnoDB支持,恢复过程依赖于redo log。InnoDB支持MVCC,MyISAM不支持。MVCC可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能。InnoDB和MyISAM都使用B+Tree作为索引结构,但二者实现方式不一样。InnoDB引擎中,其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。InnoDB的性能比MyISAM更强大,随着 CPU 核数的增加,InnoDB的读写能力呈线性增长。- 数据缓存策略和机制实现不同。
InnoDB使用缓冲池(Buffer Pool)缓存数据页和索引页,MyISAM使用键缓存(Key Cache)仅缓存索引页而不缓存数据页。
MySQL为什么减少IO次数可以加快查询
减少I/O(输入/输出)次数能加快MySQL查询的原因在于I/O操作通常是数据库系统中最耗时的部分。具体原因如下:
- 磁盘访问速度慢:尽管现代磁盘技术有所提升,但相比于CPU和内存的速度,磁盘的访问速度仍然非常慢。每次从磁盘读取数据都涉及到磁头寻道、旋转延迟等操作,这些物理操作的耗时远远大于从内存中读取数据的耗时。因此,减少磁盘I/O次数可以显著提高查询速度。
- 缓存机制的利用:MySQL有多级缓存机制,如InnoDB的Buffer Pool,用来缓存常用的数据页。减少I/O次数意味着更多的数据可以直接从缓存中读取,而不需要从磁盘加载,这样能极大提升查询性能。
- 减少上下文切换:每次进行I/O操作,系统需要进行上下文切换,保存当前的状态,并等待I/O操作完成后再恢复。减少I/O次数能够减少这种上下文切换,从而减少CPU开销,提升查询效率。
- 批量处理:MySQL通过减少I/O次数,可以更好地利用批量处理和预读技术,将多个查询操作合并到一次I/O操作中完成,这样可以大大减少总的I/O开销,提高性能。
优化I/O是数据库优化的一个关键部分,通过合理设计索引、使用合适的存储引擎、优化查询语句等方式,可以有效地减少I/O次数,提升MySQL的查询性能。
MySQL索引✅
索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。索引底层数据结构存在很多种类型,常见的索引结构有: B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyISAM,都使用了 B+树 作为索引结构。
优点
- 大大加快数据的检索速度(大大减少检索的数据量), 减少 IO 次数,这也是创建索引的最主要的原因。
- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
缺点
- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
- 索引需要使用物理文件存储,也会耗费一定空间。
MySQL用B+树的特点
- B+树只有叶子节点存放
key和data,其他内节点只存放key。 - B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
- B+树的检索效率很稳定,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
- B+树的范围查询,只需要对链表进行遍历即可。
MySQL用B+树有什么优点
- B+树只有叶子节点存放
key和data,其他内节点只存放key。所有查询都会落到叶子节点,保证查询长度一致。 - B+树是自平衡树,确保树的高度相对较低,使得从根节点到叶子节点的查找路径相对较短且长度一致,保证查询效率的稳定。
- B+树的叶子节点之间通过指针相互连接,形成了一个有序链表。这使得在执行范围查询和顺序访问(全表扫描)的时候直接在叶子节点链表上进行,而无需回溯到内部节点,提高了范围查询和顺序访问的效率。
- 局部性原理:由于B+树的叶子节点通常存储在磁盘上,而磁盘访问时间远大于内存访问时间,因此磁盘I/O是数据库操作的主要瓶颈。B+树通过将相关记录聚集在相邻的叶子节点中,并利用了磁盘的局部性原理(即当访问某个数据项时,其附近的数据项也很有可能被访问),减少了磁盘I/O的次数,提高了查询效率。
索引底层数据类型选型
Hash索引InnoDB存储引擎不直接支持常规的哈希索引,但存在一种特殊的“自适应哈希索引”(Adaptive Hash Index),结合了哈希索引和 B+ 树索引的优点,自适应哈希索引的每个哈希桶实际上是一个小型的B+Tree结构(不是传统的链表+红黑树),这个B+Tree结构可以存储多个键值对,而不仅仅是一个键。Hash索引不支持顺序和范围查询,因此 MySQL 没有使用其作为索引的数据结构。二叉查找树
二叉查找树中,左子树所有节点的值均小于根节点的值,右子树所有节点的值均大于根节点的值,左右子树也分别为二叉查找树。
当二叉查找树是平衡的时候(每个节点左右子树深度相差不超过 1 ),查询的时间复杂度为O(log2(N)),具有比较高的效率。但其在最坏情况下(有序插入数据)会退化成线性链表,查询效率会降低到O(N)。二叉查找树的性能非常依赖于它的平衡程度,不适合作为数据库索引。AVL树
AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。在使用 AVL 树时,每个树节点仅存储一个数据,每次磁盘 IO 只能读取一个节点的数据,如果要查询的数据在多个节点上,就需要进行多次磁盘 IO。磁盘 IO 是一项耗时的操作,在设计数据库索引时,优先需要考虑如何最大限度地减少磁盘 IO 操作的次数。因此,实际应用中 AVL 树并不常用作数据库索引。红黑树
红黑树并不追求严格的平衡,而是大致的平衡。因此,红黑树的查询效率稍有下降,因为红黑树的平衡性相对较弱,导致树的高度较高,这可能会导致一些数据需要进行多次磁盘 IO 操作才能查询到,这也是 MySQL 没有选择红黑树的主要原因。红黑树在插入和删除节点时只需进行O(1)次数的旋转和变色操作,即可保持基本平衡状态,而不需要像 AVL 树一样进行O(logn)次数的旋转操作,其插入和删除操作效率大大提高。B树&B+树
B 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 Balanced(平衡)的意思。
目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。
B树&B+树区别
- B 树的所有节点既存放键(
key) 也存放数据(data),而 B+树只有叶子节点存放key和data,其他内节点只存放key。 - B 树的叶子节点都是独立的; B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
- 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+树的范围查询,只需要对链表进行遍历即可。
综上,B+树与B树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。
在什么时候需要创建索引
在MySQL中,创建索引是为了提高查询效率,但并不是所有的列都需要创建索引。以下是一些需要考虑创建索引的场景:
- 主键:主键默认会创建索引,确保数据的唯一性,并加快查询速度。
- 频繁作为查询条件的列:
- 对于经常在
WHERE子句中使用的列(特别是大数据量的表),创建索引可以显著提高查询速度。 - 在连接操作中的
JOIN条件列上创建索引,可以加快连接的速度。
- 对于经常在
- 排序和分组列:如果查询语句中使用了
ORDER BY或GROUP BY,在这些列上创建索引有助于加快排序和分组操作。 - 多表查询的关联列:在进行多表连接查询时,应该在连接条件涉及的列上创建索引,以提高连接效率。
- 覆盖索引:如果一个查询只需要访问索引中的列数据而不需要访问表中的其他列,可以考虑创建覆盖索引(包括所有查询需要的列)。这可以减少磁盘I/O,提高查询速度。
- 频繁更新且查询速度要求较高的列:对于经常更新并且在查询中常用的列,可以权衡创建索引。如果更新频率高且查询优化效果明显,创建索引可能是有益的。
注意:并非所有情况都需要创建索引。过多的索引会导致写操作(如
INSERT、UPDATE、DELETE)的性能下降,因为每次写操作都需要更新相关的索引。因此,创建索引时应考虑查询频率与写操作性能的平衡。
索引分类
按照数据结构维度划分:
BTree索引:MySQL 里默认和最常用的索引类型。- 哈希索引:类似键值对的形式,一次即可定位。
按照底层存储方式角度划分:
- 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。
- 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。
MyISAM引擎,不管主键还是非主键,使用的都是非聚簇索引。
按照应用维度划分:
- 主键索引:加速查询 + 列值唯一(不可以有
NULL)+ 表中只有一个。 - 普通索引:仅加速查询。
- 唯一索引:加速查询 + 列值唯一(可以有
NULL)。 - 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。
- 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
- 全文索引:对文本的内容进行分词,进行搜索。目前只有
CHAR、VARCHAR,TEXT列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如ElasticSearch代替。
MySQL 8.x 中实现的索引新特性:
- 隐藏索引:也称为不可见索引,不会被优化器使用,但是仍然需要维护,通常会软删除和灰度发布的场景中使用。主键不能设置为隐藏(包括显式设置或隐式设置)。
- 降序索引:之前的版本就支持通过
DESC来指定索引为降序,但实际上创建的仍然是常规的升序索引。直到 MySQL 8.x 版本才开始真正支持降序索引,且不再对GROUP BY语句进行隐式排序。 - 函数索引:从 MySQL 8.0.13 版本开始支持在索引中使用函数或者表达式的值,也就是在索引中可以包含函数或者表达式。
MyISAM/InnoDB索引区别
MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但实现方式不太一样:
MyISAM引擎中,B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址读取相应的数据记录。这被称为“非聚簇索引(非聚集索引)”。InnoDB引擎中,B+Tree叶节点的data域存放的是完整的数据记录。这个数据记录其实就是数据表的主键索引,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(聚集索引)”。
聚簇索引/非聚簇索引
✨聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。
优点:
- 查询速度非常快:聚簇索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引,聚簇索引少了一次读取数据的 IO 操作。
- 对排序查找和范围查找优化:聚簇索引对于主键的排序查找和范围查找速度非常快。
缺点:
- 依赖于有序的数据:因为 B+树是多路平衡树,如果插入的索引无序,需要在插入时排序,像字符串/UUID这类又长又难比较的数据,插入/查找的速度较慢。
- 更新代价大:对索引列数据修改时,对应的索引也将会被修改,且聚簇索引的叶子节点还存放数据,修改代价较大,所以对于主键索引来说,主键一般都是不可被修改的。
✨非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
优点:
更新代价比聚簇索引要小(非聚簇索引的叶子节点不存放数据)。
缺点:
- 依赖于有序的数据
- 可能会二次查询(回表):非聚簇索引最大的缺点。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
回表
在 InnoDB 存储引擎中,非主键索引的叶子节点包含的是主键的值。当使用非主键索引进行查询时,数据库会先找到对应的主键值,然后再通过主键索引来定位和检索完整的行数据。这个过程被称为“回表”。
主键索引
数据表的主键列使用的就是主键索引(主键索引的 data 域值是完整的数据行)。 一张数据表有只能有一个主键,并且主键不能为 null,不能重复。
在 MySQL 的 InnoDB 的表中,当没有显示的指定表的主键时,InnoDB 会自动先检查表中是否有唯一索引且不允许存在 null 值的字段,如果有,则选择该字段为默认的主键,否则 InnoDB 将会自动创建一个 6Byte 的自增主键。
二级索引
二级索引的叶子节点存储的数据是主键的值(其实存储的是索引列的值和主键值),也就是说,通过二级索引可以定位主键的位置,二级索引又称为辅助索引/非主键索引。
唯一索引,普通索引,前缀索引等索引都属于二级索引。
- 唯一索引(
Unique Key):唯一索引的属性列不能出现重复的数据,但是允许数据为NULL,一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。 - 普通索引(
Index):普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和NULL。 - 前缀索引(
Prefix):前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。 - 全文索引(
Full Text):全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有MYISAM引擎支持全文索引,5.6 之后InnoDB也支持了全文索引。
覆盖索引/联合索引
- 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。当一个查询语句只需要从索引中就能够取得所需的数据,而不需要回表查询,这种情况就称为覆盖索引。
- 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
联合索引最左前缀匹配原则
在使用联合索引时,MySQL 会根据索引中的字段顺序,从左到右依次匹配查询条件中的字段。如果查询条件与索引中的最左侧字段相匹配,那么 MySQL 就会使用最左侧索引来过滤数据,这样可以提高查询效率。在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
索引下推ICP
索引下推(Index Condition Pushdown,ICP) 是 MySQL 5.6 版本中提供的一项索引优化功能,它允许存储引擎在索引遍历过程中,执行部分 WHERE语句中的判断条件,直接过滤掉不满足条件的记录,从而减少回表次数,提高查询效率,减少存储引擎层和 Server 层的数据传输量。
原理
MySQL 分为 Server 层和存储引擎层这两层。Server 层处理查询解析、分析、优化、缓存以及与客户端的交互等操作,而存储引擎层负责数据的存储和读取。
索引下推的下推其实就是指将部分上层(Server 层)负责的事情,交给下层(存储引擎层)去处理。
1 | SELECT * FROM user WHERE zipcode = '431200' AND MONTH(birthdate) = 3; |
- 没有索引下推,存储引擎层
zipcode字段利用索引可以快速定位到zipcode = '431200'的用户的主键ID,然后二次回表获取完整用户数据;存储引擎层把获取到的完整用户数据全交给 Server 层,Server 层根据MONTH(birthdate) = 3这一条件再进一步做筛选。 - 有索引下推,存储引擎层根据
zipcode索引字段找到所有zipcode = '431200'的用户,然后直接判断MONTH(birthdate) = 3,筛选出符合条件的主键ID;然后二次回表查询,根据符合条件的主键ID去获取完整的用户数据;最后把符合条件用户数据全部交给 Server 层。
索引下推适用于 InnoDB 引擎和 MyISAM 引擎,且存储过程不能使用索引下推,因为存储引擎无法调用存储函数。
索引失效场景
联合索引中,不符合最左前缀匹配原则。
1
2
3-- 复合索引 (a, b, c)
SELECT * FROM table WHERE b = 1 AND c = 2; -- 索引失效
SELECT * FROM table WHERE a = 1 AND c = 2; -- 部分索引生效索引必须从最左边的列开始使用,跳过第一列或中间的列都会导致索引失效。
在联合索引中,如果某个查询条件是范围查询 (
BETWEEN、> <、IN),后续的列将无法使用索引。例如:1
2-- 复合索引 (a, b, c)
SELECT * FROM table WHERE a = 1 AND b > 10 AND c = 5;因为
b > 10是范围查询,导致c列上的索引失效。在
WHERE子句中对索引字段使用函数或表达式时,MySQL 无法使用索引。例如:1
SELECT * FROM users WHERE LEFT(name, 3) = 'Tom';
因为
LEFT(name, 3)需要对字段进行操作,索引失效。使用
OR且OR前后条件没有都使用索引。如果OR两侧的条件中有一侧没有索引,整个查询无法使用索引。1
SELECT * FROM users WHERE id = 1 OR name = 'Tom';
其中
id列有索引,而name没有索引,则索引失效。类型不一致导致隐式转换。当索引列的类型和查询条件的类型不一致时,MySQL 会对数据进行隐式转换,从而导致索引用不上。例如:
1
SELECT * FROM users WHERE phone = 1234567890;
如果
phone字段是VARCHAR类型,而查询条件是数字,MySQL 会对phone列进行隐式转换,索引失效。LIKE 模式不以常量开头。对于
LIKE查询,只有在匹配模式不以通配符%开头时,索引才能生效。例如:1
2SELECT * FROM users WHERE name LIKE 'Tom%'; -- 索引有效
SELECT * FROM users WHERE name LIKE '%Tom'; -- 索引失效
MySQL查询缓存✅
执行查询语句时,MySQL 会先检查查询缓存,若之前执行过相同的查询,且查询缓存中有这个查询的结果,那么 MySQL 就会直接返回查询缓存中的结果,而不会再去执行查询。MySQL 8.0 版本后移除,因为这个功能不太实用
查询缓存不命中的情况:
- 两个查询在任何字符上的不同都会导致缓存不命中。
- 若查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表,其查询结果也不会被缓存。
- 缓存建立之后,查询缓存系统会跟踪查询中涉及的每张表,若这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。
缓存虽然能够提升数据库的查询性能,但也带来了额外的开销,每次查询后都要做一次缓存操作,失效后要销毁。
MySQL三大日志✅
MySQL比较重要的日志有二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。
MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。MySQL 数据库的数据备份、主备、主主、主从都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。
redo log
redo log(重做日志)是 InnoDB 存储引擎独有的物理日志,用于实现数据的持久性和崩溃恢复。记录内容是数据页的物理修改操作,比如某个数据页的某个偏移量被改为了什么值。它让 MySQL 拥有了崩溃恢复能力。若 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据,保证数据的持久性与完整性。
查询或更新一条数据时,会从硬盘把一页数据加载出来,存入 Buffer Pool中,后续查找先查找Buffer Pool,如果没有找到再去硬盘加载。更新数据的时候,若 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer)里,接着刷盘到 redo log 文件里。
刷盘时机
InnoDB 在多种情况下将 redo log 刷到磁盘上,以保证数据的持久性和一致性。
- 事务提交:当事务提交时,
redo log buffer里的redo log会被刷新到磁盘的redo log文件里。 redo log buffer空间不足时:redo log buffer中缓存的redo log已占了redo log buffer总容量的大约一半左右,就需要把这些日志刷新到磁盘上。Checkpoint(检查点):InnoDB定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。- 正常关闭服务器:MySQL 关闭的时候,
redo log都会刷入到磁盘里去。 - 后台线程:
InnoDB有一个后台线程,每隔1 秒,就会把redo log buffer中的内容写到文件系统缓存(page cache),然后调用fsync刷盘。
刷盘策略
innodb_flush_log_at_trx_commit参数设置三种刷盘策略:
- 0,每次事务提交时不进行刷盘操作。这种方式性能最高,也最不安全,因为如果 MySQL 挂了或宕机了,可能会丢失最近 1 秒内的事务。
- 1,每次事务提交时都将进行刷盘操作。这种方式性能最低,也最安全,因为只要事务提交成功,
redo log记录就一定在磁盘里,无论MySQL 挂了或宕机都不会有任何数据丢失。 - 2,每次事务提交时都只把
log buffer里的redo log内容写入page cache(文件系统缓存)。page cache是专门用来缓存文件的,这里被缓存的文件就是redo log文件。这种方式的性能和安全性都介于前两者中间。如果 MySQL 挂了不会有任何数据丢失,但是宕机可能会有1秒数据的丢失。
默认策略为第二种。
redo log两阶段提交
redo log(重做日志)让 InnoDB 存储引擎拥有了崩溃恢复能力。binlog(归档日志)保证了 MySQL 集群架构的数据一致性。
redo log 与 binlog 的写入时机不一样,redo log 是事务执行过程中写入,binlog 是事务提交后写入。
写入机制不一样会导致日志不一致:
若某一事务(更新某一行的a值,从 0->1)执行过程中写完 redo log 日志后,binlog 日志写期间发生了异常,写入失败。此时使用redo log恢复的数据中,a值已经变为1,而binlog中记录的是a值为0,这样就导致了数据不一致。
为了解决数据不一致问题,InnoDB 存储引擎使用两阶段提交:将 redo log 的写入拆成了两个步骤prepare和commit。
prepare:在事务执行过程中,先写入redo log,但是不提交事务。commit:在事务提交后,再写入binlog,然后提交redo log。
使用两阶段提交后,写入 binlog 时发生异常也不会有影响。此时 MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于prepare阶段,并且没有对应 binlog 日志,就会回滚该事务。
如果redo log 在 commit阶段发生异常,虽然 redo log 处于prepare阶段,但是能通过事务id找到对应的 binlog 日志,所以 MySQL 认为是完整的,就会提交事务恢复数据。
binlog
binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志,且是顺序写。
MySQL 数据库的数据备份、主备、主主、主从都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。
记录格式
binlog 有三种记录格式:
Statement:记录的是 SQL 语句,如UPDATE t SET c=c+1 WHERE id=2。- 同步数据时,会执行记录的SQL语句,比如获取系统时间、随机数等,会导致主从数据不一致。
Row:记录的是行数据,如id=2 c=2。- 这里记录的内容不再是简单的SQL语句了,还包含操作的具体数据,记录内容如下。比如上面问题,会记录具体系统时间、随机数等。
Mixed:记录的是上面两种的混合模式。- MySQL 会判断这条SQL语句是否可能引起数据不一致,如果是,就用
row格式,否则就用statement格式。
- MySQL 会判断这条SQL语句是否可能引起数据不一致,如果是,就用
写入机制
事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到 binlog 文件中。一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。
通过 write 将binlog cache中的 binlog写入文件系统缓存(page cache),然后调用 fsync 刷盘。
write 和 fsync 时机 由参数sync_binlog控制,默认是1。
- 0,每次提交事务都只
write,由系统自行判断什么时候执行fsync。- 虽然性能最高,但是机器宕机,
page cache里面的binlog会丢失。
- 虽然性能最高,但是机器宕机,
- 1,每次提交事务都会执行
write+fsync,和redo log日志刷盘流程一样。 - N(>1),每次提交事务都会执行
write,但是累计N次事务后才执行fsync。- 在出现 IO 瓶颈的场景里,将
sync_binlog设置成一个比较大的值,可以提升性能。 但若机器宕机,会丢失最近N个事务的binlog日志。
- 在出现 IO 瓶颈的场景里,将
write指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快。fsync是指将数据持久化到磁盘的操作
undo log
undo log用于实现事务回滚(Rollback)和多版本并发控制(MVCC)。当一个事务失败或被显式回滚时,可以通过 undo log 将数据恢复到事务开始前的状态。其属于逻辑日志,记录的是 SQL 语句,比如说事务执行一条 DELETE 语句,那 undo log 就会记录一条相对应的 INSERT 语句。每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。
undo log 本身是会被删除清理的,例如 INSERT 操作,在事务提交之后就可以清除掉了;UPDATE/DELETE 操作在事务提交不会立即删除,会加入 history list,由后台线程 purge 进行清理。
undo log 采用 segment(段)进行记录,事务开始时,需要为其分配一个 rollback segment。每个 rollback segment 有 1024 个 undo log segment(每个 undo 操作在记录的时候占用一个 undo log segment,多个有助于管理多个并发事务) 和 一个 rollback segment header(负责管理rollback segment),history list 是rollback segment header的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的 undo log,history list列表使得 purge 线程能够找到并清理那些不再需要的 undo log 记录。
MySQL事务✅
事务是逻辑上的一组操作,要么都执行,要么都不执行。
MySQL的事务是通过InnoDB存储引擎实现的,因为MyISAM等引擎不支持事务。
事务的四大特性
- 原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败回滚。
- 原子性是通过
undo log实现的。当事务执行时,MySQL会为每个操作生成一条回滚日志(undo log)。如果事务失败,MySQL可以利用回滚日志撤销所有已经执行的操作,从而保证原子性。
- 原子性是通过
- 一致性(Consistency):执行事务前后,数据保持一致。
- 一致性主要依赖于数据库的 约束(如外键约束、唯一性约束、检查约束等),以及应用层逻辑来确保数据的完整性。一致性是应用程序和数据库系统共同维护的,MySQL通过在事务中执行的过程中遵守这些约束来确保数据库在任何时候都保持一致。
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
- MySQL通过 锁机制 和 多版本并发控制(MVCC)来实现隔离性。
- 锁机制 通过不同粒度的锁(如行级锁、表级锁)来防止事务之间的干扰。
- MVCC 通过为每个事务维护不同的版本快照,使得读取数据时无需加锁,从而提高了并发性能。在InnoDB存储引擎中,MVCC是通过
undo log和事务的版本控制来实现的。
- 持久性(Durability):事务成功结束后,对数据库的修改是永久的,即使数据库发生故障也不应该丢失。
- 持久性是通过
redo log实现的。当事务进行写操作时,InnoDB会将这些修改首先记录到redo log(预写日志机制,Write-Ahead Logging),并在适当的时候将其写入磁盘。这使得即使系统在事务提交后崩溃,MySQL也可以通过redo log恢复数据,确保持久性。
- 持久性是通过
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
并发事务存在的问题
脏读
一个事务读取到了另一个事务未提交的数据,这个数据称为脏数据。
一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这也就是脏读的由来。
解决:
- 设置事务隔离级别为
Read Committed或更高。Read Committed确保事务只能读取已经提交的数据,从而避免脏读。
丢失修改
两个事务同时对同一数据进行修改,其中一个事务的修改被另一个事务覆盖,导致一个事务的修改被丢失。
在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
解决:
- 乐观锁(Optimistic Locking): 在提交时检查数据的版本号或时间戳,如果版本号或时间戳已变化,则说明数据已经被修改过,此时事务需要重新读取数据并进行处理。
- 悲观锁(Pessimistic Locking): 在读取数据时对数据加锁,直到事务结束才能释放锁,从而避免其他事务同时修改数据。
不可重复读
一个事务内多次读取同一数据,由于另一个事务的修改,导致多次读取的数据不一致。
一个事务在读取某个数据后的某个时间,另外一个事务对这个数据进行了修改,那么第一个事务再次读取这个数据时,数据已经发生了变化,导致多次读取的数据不一致,这就是不可重复读。
解决:
- 执行
DELETE和UPDATE操作的时候,可以直接对记录加锁,保证事务安全。 - 设置事务隔离级别为 Repeatable Read 或更高。Repeatable Read 确保事务内多次读取的数据一致,避免不可重复读。
幻读
一个事务读取到了另一个事务插入的数据,导致多次查询的结果不一致。
一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
解决:
- 执行
INSERT操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行INDERT操作的时候需要依赖Next-Key Lock(Record Lock+Gap Lock)进行加锁来保证不出现幻读。 - 事务隔离级别: 设置事务隔离级别为 Serializable。Serializable 级别确保事务完全隔离,避免幻读。
不可重复读和幻读有什么区别
- 不可重复读:多次读取同一数据,发现数据不一致。
- 幻读:多次执行同一条查询语句,发现查到的记录增加了。
幻读其实可以看作是不可重复读的一种特殊情况,单独把幻读区分出来的原因主要是解决幻读和不可重复读的方案不一样。
- 解决不可重复读:执行
DELETE和UPDATE操作的时候,可以直接对记录加锁,保证事务安全。 - 解决幻读:执行
INDERT操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行INDERT操作的时候需要依赖Next-Key Lock(Record Lock+Gap Lock)进行加锁来保证不出现幻读。
并发事务的控制方式有哪些
MySQL 中并发事务的控制方式无非就两种:锁 和 MVCC。锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。
- 锁:
- 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。
MVCC是多版本并发控制方法,即对一份数据会存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的。
读写锁可以做到读读并行,但是无法做到写读、写写并行。根据根据锁粒度的不同,又被分为 表级锁(table-level locking) 和 行级锁(row-level locking) 。InnoDB 默认为行级锁,也支持表级锁,所以并发性能比较高。
注意:不论是表级锁还是行级锁,都存在共享锁(
Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类。
MVCC 在 MySQL 中实现所依赖的手段主要是:隐藏字段、read view、undo log。
undo log:undo log用于记录某行数据的多个版本的数据。read view和 隐藏字段: 用来判断当前版本数据的可见性。
事务隔离级别✅
READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
|---|---|---|---|---|
| READ-UNCOMMITTED | √ | √ | √ | / |
| READ-COMMITTED | × | √ | √ | 基于 MVCC/锁 |
| REPEATABLE-READ | × | × | √ | 基于 MVCC/锁(当前读情况下需要使用加锁读来保证不会出现幻读) |
| SERIALIZABLE | × | × | × | 基于 锁 |
MySQL默认隔离级别是 REPEATABLE-READ。
InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的,主要有下面两种情况:
- 快照读:由
MVCC机制来保证不出现幻读。 - 当前读:使用
Next-Key Lock进行加锁来保证不出现幻读,Next-Key Lock是行锁(Record Lock)和间隙锁(Gap Lock)的结合,行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。
因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED ,不过 InnoDB 存储引擎默认使用 REPEATABLE-READ,这并不会有任何性能损失。InnoDB 存储引擎在分布式事务的情况下一般会用到 SERIALIZABLE 隔离级别。
MVCC✅
多版本并发控制(Multi-Version Concurrency Control, MVCC) 用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。
读操作(
SELECT)
当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的,因此事务不会读取其他事务尚未提交的修改。具体情况如下:- 对读取操作,事务会查找符合条件的数据行,并选择符合其事务开始时间的数据版本进行读取。
- 如果数据行有多个版本,事务选择不晚于其开始时间的最新版本,确保事务只读取在它开始之前已经存在的数据。
- 事务读取的是快照数据,因此其他并发事务对数据行的修改不会影响当前事务的读取操作。
写操作(
INSERT、UPDATE、DELETE)
当事务执行写操作时,会生成一个新的数据版本,并将修改后的数据写入数据库。具体情况如下:- 对于写操作,事务为要修改的数据行创建一个新的版本,并将修改后的数据写入新版本。
- 新版本的数据会带有当前事务的版本号,以便其他事务能够正确读取相应版本的数据。
- 原始版本的数据仍然存在,供其他事务使用快照读取,这保证了其他事务不受当前事务的写操作影响。
事务提交和回滚
- 当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。
- 当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。
版本回收
为了防止数据库中的版本无限增长,MVCC会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。
MVCC 通过创建数据的多个版本和使用快照读取来实现并发控制。读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。这样,不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。
一致性非锁定读/快照读
一致性非锁定读的实现,通常是加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1 或者更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见。
InnoDB中,MVCC就是对一致性非锁定读的实现。如果读取的行正在执行 DELETE 或 UPDATE 操作,则读取操作不会等待行锁释放,而是会读取行的一个快照数据,这种读取历史数据的方式叫快照读(snapshot read)。
在 Repeatable Read 和 Read Committed 两个隔离级别下,如果是执行普通的 SELECT 语句(不包括 SELECT ... lock in share mode ,SELECT ... FOR UPDATE)则会使用一致性非锁定读(MVCC)。Repeatable Read 下 MVCC 实现了可重复读和防止部分幻读。
锁定读/当前读
在锁定读下,读取的是数据的最新版本。下面语句都是锁定读/当前读,会对读取到的记录加锁。
SELECT ... lock in share mode: 对读取到记录加共享锁,其他事务也可以加共享锁,加排他锁会阻塞。SELECT ... FOR UPDATE: 对读取到记录加排他锁,其他事务不能加任何锁。INDERT、UPDATE、DELETE操作:对读取到记录加排他锁,其他事务不能加任何锁。
当前读 ,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以 InnoDB 在实现Repeatable Read 时,如果执行的是当前读,则会对读取的记录使用 Next-key Lock ,来防止其它事务在间隙间插入数据。
InnoDB的MVCC实现
MVCC 的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行隐藏字段的 DB_TRX_ID 和 Read View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改。
隐藏字段InnoDB 为每行数据添加了三个隐藏字段:
DB_TRX_ID:表示最后一次更新或插入操作的事务ID。DELETE操作也被视为更新。DB_ROLL_PTR:指向该行undo log日志的指针。DB_ROW_ID:表示行的物理地址。若没有设置主键且该表没有唯一非空索引,则会使用DB_ROW_ID作为聚簇索引。
Read ViewRead View 主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务” 的 ID。
m_low_limit_id(低限事务ID):目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见m_up_limit_id(上限事务ID):活跃事务列表m_ids中最小的事务 ID,如果m_ids为空,则m_up_limit_id为m_low_limit_id。小于这个 ID 的数据版本均可见m_ids(活跃事务列表):Read View创建时其他未提交的活跃事务 ID 列表。创建Read View时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids不包括当前事务自己和已提交的事务(正在内存中)m_creator_trx_id:创建该Read View的事务 ID
undo log
其有两个作用:
- 事务回滚时用于将数据恢复到修改前的样子
MVCC,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过undo log读取之前版本的快照数据,以此实现非锁定读
RC和RR隔离级别下MVCC的差异
Read Committed:MVCC会在每次SELECT读取数据时创建一个新的Read View(m_ids列表),这样可以确保读取到的数据是最新的。但是由于每次读取都需要创建新的Read View,因此会导致一些不可重复读问题。Repeatable Read:MVCC会在事务开始后第一次SELECT创建一个Read View,并在事务结束时销毁。这样可以确保事务内的读取操作都是一致的,这样解决了不可重复读,但是可能会导致一些幻读问题。
RR隔离级别下的MVCC如何解决幻读
MVCC+Next-key-Lock 防止幻读
- 执行普通
SELECT,此时会以MVCC快照读的方式读取数据- 在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成
Read View,并使用至事务提交。所以在生成Read View之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”
- 在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成
- 执行
SELECT...FOR UPDATE/lock in share mode、INDERT、UPDATE、DELETE等当前读- 当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!
InnoDB使用Next-key Lock来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据,这样就不会发生幻读。
- 当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!
MySQL锁✅
表级锁/行级锁区别
表级锁:MySQL 中锁定粒度最大的一种锁(全局锁除外),对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,
MyISAM和InnoDB引擎都支持表级锁。行级锁: MySQL 中锁定粒度最小的一种锁,是针对索引字段加的锁,只针对当前操作的行记录进行加锁。行级锁能大大减少数据库操作的冲突,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。
MyISAM仅仅支持表级锁,一锁就锁整张表,这在并发写的情况下性非常差。InnoDB不光支持表级锁和行级锁(默认),仅对一行记录上锁,所以InnoDB的并发性能更高
InnoDB的行级锁
- 记录锁(
Record Lock):也被称为记录锁,属于单个行记录上的锁。 - 间隙锁(
Gap Lock):锁定一个范围,不包括记录本身。 - 临键锁(
Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
共享锁/排他锁
表级锁和行级锁都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)两类。
- 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。
排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。
由于有MVCC机制,对于一般的 SELECT 语句,InnoDB 不会加任何锁。
意向锁
意向锁是为了解决行级锁的问题,当一个事务要对某一行加排他锁时,需要先对整张表加一个意向排他锁,这样可以避免多个事务同时对一行数据加排他锁。
意向锁是表级锁,有两种:
- 意向共享锁(
Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。 - 意向排他锁(
Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
意向锁由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InnoDB 会先获取该数据行所在在数据表的对应意向锁。
意向锁之间是互相兼容的。意向共享锁与表级共享锁兼容,和表级排他锁是不兼容的。意向排他锁与表级共享锁和表级排他锁都是不兼容的。
当前读/快照读区别
- 当前读(一致性锁定读):执行
SELECT...FOR UPDATE/lock in share mode、INDERT、UPDATE、DELETE- 当前读下, 就是给行记录加 X 锁或 S 锁,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!
InnoDB使用Next-key Lock来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据,这样就不会发生幻读。
- 当前读下, 就是给行记录加 X 锁或 S 锁,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!
- 快照读(一致性非锁定读):执行普通
SELECT,此时会以MVCC快照读的方式读取数据- 在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成
Read View,并使用至事务提交。所以在生成Read View之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”
- 在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成
MySQL性能优化✅
慢查询原因及解决
MySQL 慢查询(slow query)是指执行时间超过预设阈值的查询。这种查询会导致数据库性能下降,影响应用程序的响应速度。慢查询的原因可以有很多,常见的包括:
- 缺乏索引:没有为查询涉及的字段创建合适的索引,导致全表扫描。
- 索引使用不当:索引没有被正确使用,例如查询条件中使用了函数或类型转换,导致索引失效。
- 数据量大:表的数据量过大,查询需要扫描大量的数据。
- 复杂的查询:查询语句过于复杂,包含多个表的连接(JOIN)、子查询等,导致执行时间长。
- 硬件资源不足:服务器的CPU、内存或磁盘IO性能不足,无法快速处理查询。
- 数据库配置不当:MySQL的配置参数没有优化,例如
innodb_buffer_pool_size、query_cache_size等。 - 锁争用:高并发环境下,表或行被频繁锁定,导致查询等待时间增加。
- 网络延迟:网络延迟也会影响查询的响应时间,尤其是在分布式数据库系统中。
- 不合理的查询设计:如没有合理的分页查询、重复查询等。
解决慢查询问题的思路可以包括:
- 分析查询日志:使用MySQL的慢查询日志(
slow query log)找出执行时间长的查询。 - 使用
EXPLAIN命令:分析查询的执行计划,找到可能的问题。 - 优化索引:根据查询条件创建合适的索引,避免全表扫描。
- 调整查询语句:简化查询语句,避免不必要的复杂操作。
- 优化数据库配置:调整MySQL的配置参数,提升性能。
- 分区和分表:对于大表,可以考虑分区或分表,减少单次查询的数据量。
- 提升硬件性能:增加服务器的硬件资源,如CPU、内存、磁盘等。
通过这些方法,可以有效地减少MySQL慢查询,提升数据库的整体性能。
十亿条评论如何找出top10的词
- 首先十亿条评论数据量巨大,直接在MySQL中进行复杂的文本处理和分词可能会非常慢并且效率低下。
- 由于MySQL不擅长直接进行分词和复杂的字符串处理,可以将评论数据导出到外部脚本进行处理。可以使用Python或其他编程语言来实现分词和词频统计。
- 将所有评论数据采用批量处理的方式导出到一个外部文件或者直接传给处理脚本,这样可以减少数据库负载。
- 在脚本中,对评论数据进行清洗,去除无意义的字符,如标点符号、空格等。
- 对清洗后的文本进行分词,统计词频。
- 将词和词频存储到一个新表,将处理结果导入到Mysql 中,采用批量插入的方法来提高效率。
- 在MySQL中使用一个简单的查询来获取词频最高的前十个词。例如,使用ORDER BY count DESC LIMIT 10来排序并限制结果数量。
如此大量的数据,也可以使用分布式数据库如Apache Hadoop或Apache Spark来处理。
1TB的文件如何快速从中找到想要的数据
- 可以将文件分块,多线程处理。如要查找的数据是1MB,则可以每块都检查前后1MB范围内的数据,确保数据不会被分割。
- 分布式计算。可以借助于 Hadoop 或 Spark 来进行并行化处理。这些框架能够将数据分片分发到多个节点上进行并行处理,大幅度提高数据处理速度。
- 流式处理。不需要把完整的内容加载到内存,只需要按需处理。可以每次从硬盘中读取一部分,然后检索。这样可以避免内存溢出、以及可以使用多线程加快处理速度。
- 如果要查找的内容是字符串,还可以考虑使用 KMP 等高效的字符串匹配算法。
- 如果文件数据是按照某种顺序如ID、时间戳排列,考虑使用二分查找加速定位数据。
- 另外可以借助一些数据结构如HashMap,前提是可以建立HashMap,这样可以将查询复杂度降低到O(1)。
- 处理中,磁盘 I/O 可能成为瓶颈,可以考虑将数据放入固态硬盘,加快读取速度。
SELECT已经建立索引,为什么仍然走全表查询
select * from log where level = 1,已经在level上建立索引,为什么仍然走全表查询
- 索引未生效:检查是否确实已经在
level列上成功创建了索引。1
SHOW INDEX FROM log;
- 数据分布问题:如果
level列的数据分布非常不均衡,比如大部分行的level值都为 1,数据库可能认为使用索引并不会比全表扫描更有效,因此选择了全表扫描。 - 查询返回的记录过多:如果查询返回的记录占表中记录总数的较大比例,数据库优化器可能会选择全表扫描而不是使用索引。因为扫描较少的记录才是索引的优势,而返回大量数据时,索引可能不如全表扫描高效。
解决方案建议:
- 更新统计信息(
ANALYZE TABLE log;)。 - 检查索引的存在性和有效性(
SHOW INDEX FROM log;)。 - 考虑数据的分布,调整查询策略。
可以执行 EXPLAIN 语句来查看查询执行计划,确定是否使用了索引。
1 | EXPLAIN SELECT * FROM log WHERE level = 1; |
可以用MySQL存储文件吗?
可以直接存储文件对应的二进制数据即可。不过不建议在数据库中存储文件,会严重影响数据库性能,消耗过多存储空间。可以使用云服务厂商提供的开箱即用的文件存储服务,如阿里云的OSS、腾讯云的COS等。然后在数据库中存储文件的 URL 地址。
MySQL如何存储IP地址
可以将 IP 地址转换成整形数据存储,性能更好,占用空间也更小。MySQL提供了两个方法来处理 ip 地址:
INET_ATON():把 ip 转为无符号整型 (4-8 位)INET_NTOA():把整型的 ip 转为地址
存储数据前,先用 INET_ATON() 把 ip 地址转为整型,取出数据时,使用 INET_NTOA() 把整型的 ip 地址转为地址显示即可。
如何分析SQL性能
使用 EXPLAIN 命令来分析 SQL 的 执行计划,该命令并不会真的去执行相关的语句,而是通过 查询优化器 对语句进行分析,找出最优的查询方案,并显示对应的信息。
MySQL执行计划✅
执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。
1 | mysql> EXPLAIN SELECT * FROM dept_emp WHERE emp_no IN (SELECT emp_no FROM dept_emp GROUP BY emp_no HAVING COUNT(emp_no)>1); |
各列含义如下:
| 列名 | 含义 |
|---|---|
| id | SELECT 查询的序列标识符 |
| select_type | SELECT 关键字对应的查询类型 |
| table | 用到的表名 |
| partitions | 匹配的分区,对于未分区的表,值为 NULL |
| type | 表的访问方法 |
| possible_keys | 可能用到的索引 |
| key | 实际用到的索引 |
| key_len | 所选索引的长度 |
| ref | 当使用索引等值查询时,与索引作比较的列或常量 |
| rows | 预计要读取的行数 |
| filtered | 按表条件过滤后,留存的记录数的百分比 |
| Extra | 附加信息 |
type(重要)
查询执行的类型,描述了查询是如何执行的。所有值的顺序从最优到最差排序为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
常见的几种类型具体含义如下:
system:如果表使用的引擎对于表行数统计是精确的(如:MyISAM),且表中只有一行记录的情况下,访问方法是system,是const的一种特例。const:表中最多只有一行匹配的记录,一次查询就可以找到,常用于使用主键或唯一索引的所有字段作为查询条件。eq_ref:当连表查询时,前一张表的行在当前这张表中只有一行与之对应。是除了system与const之外最好的JOIN方式,常用于使用主键或唯一索引的所有字段作为连表条件。ref:使用普通索引作为查询条件,查询结果可能找到多个符合条件的行。index_merge:当查询条件使用了多个索引时,表示开启了Index Merge优化,此时执行计划中的key列列出了使用到的索引。range:对索引列进行范围查询,执行计划中的key列表示哪个索引被使用了。index:查询遍历了整棵索引树,与ALL类似,只不过扫描的是索引,而索引一般在内存中,速度更快。ALL:全表扫描。
key(重要)key 列表示 MySQL 实际使用到的索引。如果为 NULL,则表示未用到索引。
Extra(重要)
这列包含了 MySQL 解析查询的额外信息,通过这些信息,可以更准确的理解 MySQL 到底是如何执行查询的。常见的值如下:
Using filesort:在排序时使用了外部的索引排序,没有用到表内索引进行排序。Using temporary:MySQL 需要创建临时表来存储查询的结果,常见于ORDER BY和GROUP BY。Using index:表明查询使用了覆盖索引,不用回表,查询效率非常高。Using index condition:表示查询优化器选择使用了索引条件下推这个特性。Using where:表明查询使用了WHERE子句进行条件过滤。一般在没有使用到索引的时候会出现。Using join buffer (Block Nested Loop):连表查询的方式,表示当被驱动表的没有使用索引的时候,MySQL 会先将驱动表读出来放到join buffer中,再遍历被驱动表与驱动表进行查询。
当 Extra 列包含 Using filesort 或 Using temporary 时,MySQL 的性能可能会存在问题,需要尽可能避免。
MySQL读写分离✅
读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。这样的话,就能够小幅提升写性能,大幅提升读性能。
一般情况下,会选择一主多从,一台主数据库负责写,其他的从数据库负责读。主库和从库之间会进行数据同步,以保证从库中数据的准确性。这样的架构实现起来比较简单,并且也符合系统的写少读多的特点。
如何实现读写分离
- 部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。
- 保证主数据库和从数据库之间的数据是实时同步的,即主从复制。
- 系统将写请求交给主数据库处理,读请求交给从数据库处理。
基于项目层面有两种方案:
- 代理方式
可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。提供类似功能的中间件有 MySQL Router(官方, MySQL Proxy 的替代方案)、Atlas(基于 MySQL Proxy)、MaxScale、MyCat。 - 组件方式
可以通过引入第三方组件来实现读写分离,例如使用sharding-jdbc,直接引入 jar 包即可使用,非常方便。同时,也节省了很多运维的成本。
MySQL主从复制原理✅
MySQL 主从复制是依赖于 binlog 。另外,常见的一些同步 MySQL 数据到其他数据源的工具(比如 canal)的底层一般也是依赖 binlog。具体如下:
- 主库将数据库中数据的变化写入到
binlog - 从库连接主库
- 从库创建一个 I/O 线程向主库请求更新的
binlog - 主库创建一个
binlog dump线程来发送binlog,从库中的 I/O 线程负责接收 - 从库的 I/O 线程将接收的
binlog写入到中继日志relay log中。 - 从库的 SQL 线程读取中继日志
relay log同步数据到本地(也就是再执行一遍 SQL )。
binlog主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)relay log是中继日志,是主从复制过程中关键的一环,负责在从库上中转和存储从主库获取的 binlog 数据,确保从库数据与主库数据一致。
如何避免主从延迟
主从同步延迟:写完主库之后,主库的数据同步到从库需要时间,这个时间差就导致了主库和从库的数据不一致性问题。
- 强制将读请求路由到主库处理,这样可以保证数据的一致性,但是会增加主库的压力。
- 延迟读取,即从库的数据延迟一段时间后再读取,这样可以保证数据的一致性,但是会牺牲数据的实时性。比如支付,支付成功后,跳转到一个支付成功的页面,当点击返回之后才返回自己的账户。
什么情况下会出现主从延迟
主从同步延时是指从库的数据落后于主库的数据,这种情况可能由以下两个原因造成:
- 从库 I/O 线程接收
binlog的速度跟不上主库写入binlog的速度,导致从库relay log的数据滞后于主库binlog的数据; - 从库 SQL 线程执行
relay log的速度跟不上从库 I/O 线程接收binlog的速度,导致从库的数据滞后于从库relay log的数据。
下面是一些可能导致主从延迟的原因及解决方案:
- 从库机器性能比主库差:从库接收
binlog写入relay log以及执行 SQL 语句的速度会比较慢,进而导致延迟。解决方法是选择与主库一样规格或更高规格的机器作为从库,或者对从库进行性能优化,比如调整参数、增加缓存、使用 SSD 等。 - 从库处理的读请求过多:从库需要执行主库的所有写操作,同时还要响应读请求,如果读请求过多,会占用从库的 CPU、内存、网络等资源,影响从库的复制效率。解决方法是引入缓存(推荐)、使用一主多从的架构,将读请求分散到不同的从库,或者使用其他系统来提供查询的能力,比如将
binlog接入到Hadoop、Elasticsearch等系统中。 - 大事务:运行时间比较长,长时间未提交的事务就可以称为大事务。由于大事务执行时间长,并且从库上的大事务会比主库上的大事务花费更多的时间和资源,因此非常容易造成主从延迟。解决办法是避免大批量修改数据,尽量分批进行。类似的情况还有执行时间较长的慢 SQL ,实际项目遇到慢 SQL 应该进行优化。
- 从库太多:主库需要将
binlog同步到所有的从库,如果从库数量太多,会增加同步的时间和开销。解决方案是减少从库的数量,或者将从库分为不同的层级,让上层的从库再同步给下层的从库,减少主库的压力。 - 网络延迟:如果主从之间的网络传输速度慢,或者出现丢包、抖动等问题,那么就会影响
binlog的传输效率,导致从库延迟。解决方法是优化网络环境,比如提升带宽、降低延迟、增加稳定性等。 - 单线程复制:MySQL5.5 及之前,只支持单线程复制。为了优化复制性能,MySQL 5.6 引入了多线程复制,MySQL 5.7 还进一步完善了多线程复制。
- 复制模式:MySQL 默认的复制是异步的,必然会存在延迟问题。全同步复制不存在延迟问题,但性能太差了。半同步复制是一种折中方案,相对于异步复制,半同步复制提高了数据的安全性,减少了主从延迟(还是有一定程度的延迟)。MySQL 5.5 开始,MySQL 以插件的形式支持
semi-sync半同步复制。并且,MySQL 5.7 引入了 增强半同步复制。
MySQL分库分表✅
分库分表是指将一个数据库中的数据分散到多个数据库或多个表中,以减轻单个数据库或表的压力,提高数据库的性能。
分库
分库就是将数据库中的数据分散到不同的数据库上,可以垂直分库,也可以水平分库。
- 垂直分库:把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库。
- 如:将数据库中的用户表、订单表和商品表分别单独拆分为用户数据库、订单数据库和商品数据库。
- 水平分库:把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。
- 订单表数据量太大,对订单表进行了水平切分,第0-1000万订单数据存储在订单库1中,1000万-2000万订单数据存储在订单库2中,然后将切分后的 2 张订单表分别放在两个不同的数据库。
分表
分表就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。
- 垂直分表:对数据表列的拆分,把一张列比较多的表拆分为多张表。
- 水平分表:对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题。
什么情况下需要分库分表
- 单表的数据达到千万级别以上,数据库读写速度比较缓慢。
- 数据库中的数据占用的空间越来越大,备份时间越来越长。
- 应用的并发量太大(应该优先考虑其他性能优化方法,而非分库分表)。
分片算法
分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。
常见的分片算法有:
- 范围分片:按照特定的范围区间(比如时间区间、ID 区间)来分配数据,比如 将 id 为 1
299999 的记录分到第一个表, 300000599999 的分到第二个表。范围分片适合需要经常进行范围查找且数据分布均匀的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。 - 哈希分片:求指定分片键的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。哈希分片可以使每个表的数据分布相对均匀,但对动态伸缩(例如新增一个表或者库)不友好。
- 地理位置分片:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。
- 一致性哈希分片:将哈希空间组织成一个环形结构,将分片键和节点(数据库或表)都映射到这个环上,然后根据顺时针的规则确定数据或请求应该分配到哪个节点上,解决了传统哈希对动态伸缩不友好的问题。
分片键
分片键是指用来决定数据分片的字段,可以是单个字段,也可以是多个字段的组合。分片键的选择非常重要,不同的分片键会导致数据分布不均匀,进而导致热点数据的问题。
分库分表会带来什么问题
- 事务问题:同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足要求。这时要引入分布式事务。
- 跨库聚合查询问题:分库分表会导致常规聚合查询操作,如
GROUP BY,ORDER BY等变得异常复杂。这是因为这些操作需要在多个分片上进行数据汇总和排序,而不是在单个数据库上进行。 - 数据迁移问题:当数据量过大时,需要对数据进行迁移,这个过程可能会导致数据不一致。
- 。。。
分库分表中数据如何迁移
- 停机迁移:停机后,写一个脚本将老库的数据都同步到新库中。
- 双写方案:在写入数据的时候,同时写入到新库中,然后再定时将老库的数据同步到新库中。
MySQL数据冷热分离✅
数据冷热分离是指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据存储在高性能存储介质中。
优缺点
- 优点:热数据的查询性能得到优化(用户的绝大部分操作体验会更好)、节约成本(可以冷热数据的不同存储需求,选择对应的数据库类型和硬件配置,比如将热数据放在 SSD 上,将冷数据放在 HDD 上)
- 缺点:系统复杂性和风险增加(需要分离冷热数据,数据错误的风险增加)、统计效率低(统计的时候可能需要用到冷库的数据)。
冷数据和热数据
- 热数据是指经常被访问和修改且需要快速访问的数据。
- 冷数据是指不经常访问,对当前项目价值较低,但需要长期保存的数据。
冷热数据如何区分
- 时间维度区分:按照数据的创建时间、更新时间、过期时间等,将一定时间段内的数据视为热数据,超过该时间段的数据视为冷数据。
- 例如,订单系统可以将 1 年前的订单数据作为冷数据,1 年内的订单数据作为热数据。这种方法适用于数据的访问频率和时间有较强的相关性的场景。
- 访问频率区分:将高频访问的数据视为热数据,低频访问的数据视为冷数据。
- 例如,内容系统可以将浏览量非常低的文章作为冷数据,浏览量较高的文章作为热数据。这种方法需要记录数据的访问频率,成本较高,适合访问频率和数据本身有较强的相关性的场景。
冷数据如何迁移
- 任务调度:可以利用
xxl-job或者其他分布式任务调度平台定时去扫描数据库,找出满足冷数据条件的数据,然后批量地将其复制到冷库中,并从热库中删除。这种方法修改的代码非常少,非常适合按照时间区分冷热数据的场景。 - 监听数据库的变更日志
binlog:将满足冷数据条件的数据从binlog中提取出来,然后复制到冷库中,并从热库中删除。这种方法可以不用修改代码,但不适合按照时间维度区分冷热数据的场景。
冷数据如何存储
冷数据的存储要求主要是容量大,成本低,可靠性高,访问速度可以适当牺牲。冷数据存储方案:
- 中小厂:直接使用 MySQL/PostgreSQL 即可(不改变数据库选型和项目当前使用的数据库保持一致),比如新增一张表来存储某个业务的冷数据或者使用单独的冷库来存放冷数据(涉及跨库查询,增加了系统复杂性和维护难度)
- 大厂:Hbase(常用)、RocksDB、Doris、Cassandra
MySQL容灾
MySQL 容灾(灾难恢复)是指在 MySQL 数据库发生故障或灾难(如硬件故障、软件问题、自然灾害等)时,确保数据安全、系统尽快恢复以及服务的可用性。常见的 MySQL 容灾策略包括以下几种:
- 主从复制(Replication)
- 主从复制是一种常用的 MySQL 容灾方案,其中一个主数据库(Master)将数据复制到一个或多个从数据库(Slave)。当主数据库发生故障时,可以手动或自动切换到从数据库来提供服务。
- 优点:配置简单、低成本。
- 缺点:主从延迟可能导致数据丢失(在切换时主从未完全同步)。
- 双主复制(Master-Master Replication)
- 双主复制是主从复制的扩展,两个数据库互为主从。两个数据库都可以接受写入,当其中一个发生故障时,另一个数据库可以继续运行。
- 优点:提高了系统的容灾能力和读写性能。
- 缺点:数据冲突管理复杂,配置难度高。
- 多主复制(Group Replication)
- 多主复制是一种更先进的容灾方案,支持多个节点之间的同步复制,所有节点都可以同时读写。它基于分布式一致性协议(如 Paxos)来保证数据一致性。
- 优点:数据一致性好,自动故障切换。
- 缺点:对网络条件要求高,配置较复杂。
- MySQL Cluster(NDB Cluster)
- MySQL Cluster 使用 NDB 存储引擎,通过多个数据节点组成集群,来确保数据的冗余和高可用性。即使某些节点出现故障,集群可以继续正常工作。
- 优点:高可用性、无单点故障。
- 缺点:复杂性高,适合需要高扩展性的场景。
- 备份与恢复
- 定期对 MySQL 数据库进行备份也是容灾的重要策略之一。常用的备份方法包括逻辑备份(如使用
mysqldump工具)和物理备份(如使用XtraBackup工具)。 - 优点:确保数据在灾难发生后能够恢复。
- 缺点:备份和恢复可能需要较长时间,且存在一定的延迟。
- 定期对 MySQL 数据库进行备份也是容灾的重要策略之一。常用的备份方法包括逻辑备份(如使用
- 热备份与冷备份
- 热备份 是指在数据库正常运行时进行备份,不影响数据库的可用性。
- 冷备份 是指在数据库停止服务时进行备份,数据一致性有保障,但会中断服务。
- 容灾切换(Failover)
- 当主数据库发生故障时,容灾切换机制可以自动将流量切换到备份数据库(从数据库或另一个主数据库)。常用的自动切换工具包括 MHA(Master High Availability Manager) 和 Orchestrator。
- 优点:自动化减少了故障响应时间。
- 缺点:配置复杂,切换过程中仍可能有短暂的服务中断。
- 云端容灾
- 许多云服务提供商(如 AWS RDS、阿里云等)提供了内置的容灾机制,用户可以利用这些服务轻松实现多区域、多节点的数据冗余和故障恢复。
选择 MySQL 容灾策略时,应根据实际的业务需求、可用性要求以及运维成本来进行权衡和选择。
MySQL字符集
MySQL 支持很多种字符集的方式,比如 GB2312、GBK、BIG5、多种 Unicode 字符集(UTF-8 编码、UTF-16 编码、UCS-2 编码、UTF-32 编码等等)。 在 MySQL5.7 中,默认字符集是 latin1 ;在 MySQL8.0 中,默认字符集是 utf8mb4
MySQL字符集层次级别
- MySQL 中的字符集分为四个层次:
- server(MySQL 实例级别)
- database(库级别)
- table(表级别)
- column(字段级别)
其优先级可以简单的认为是从上往下依次增大,也即 column 的优先级会大于 table 等其余层次的。如指定 MySQL 实例级别字符集是utf8mb4,指定某个表字符集是latin1,那么这个表的所有字段如果不指定的话,编码就是latin1。
MySQL 字符编码集中有两套 UTF-8 编码实现:
utf8:utf8编码只支持1-3个字节 。 在 utf8 编码中,中文是占 3 个字节,其他数字、英文、符号占一个字节。但 emoji 符号占 4 个字节,一些较复杂的文字、繁体字也是 4 个字节。utf8mb4:UTF-8 的完整实现,正版!最多支持使用 4 个字节表示字符,因此,可以用来存储 emoji 符号。
Redis
Redisopen in new window (REmote DIctionary Server, REDIS)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。
Redis为什么这么快
- Redis 基于内存,内存的访问速度比磁盘快很多;
- Redis 基于
Reactor模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用 - Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。
- Redis 通信协议实现简单且解析高效。
Redis/Memcached
共同点:
- 都是基于内存的数据库,一般都用来当做缓存使用。
- 都有过期策略。
- 两者的性能都非常高。
区别:
- Redis 支持更丰富的数据结构,如
string、list、set、zset、hash等,而 Memcached 只支持key-value的数据结构。 - Redis 支持持久化,而 Memcached 不支持持久化。也就是说 Redis 有崩溃恢复的能力,Memcached没有。
- Redis 支持集群模式,而 Memcached 不支持集群模式。
- Redis 支持事务,而 Memcached 不支持事务。
- Redis 支持发布订阅,而 Memcached 不支持发布订阅。
- Redis 是单线程,而 Memcached 是多线程。
为什么用Redis
- 高性能:Redis 是基于内存的,读写速度非常快。
- 高并发:可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。大大提高了并发量。
- 功能齐全:除了可以用作缓存之外,Redis还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大!
三种常用缓存读写策略
旁路缓存模式(Cache Aside Pattern)
适合读请求比较多的场景,该模式下服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
读:
- 从 cache 中读取数据,读取到就直接返回
- cache 中读取不到的话,就从 db 中读取数据返回
- 把 db 中读取到的数据写入 cache 中。
写:
- 先更新 db
- 再删除 cache 中的数据
先更新数据库后再删除缓存,可以确保在数据库更新成功的前提下,缓存中的旧数据才会被清除,避免出现旧数据重新进入缓存的情况。
为什么不先删缓存再更新数据库呢?
如果你先删除缓存,然后再更新数据库,可能在这两个操作之间出现一个短暂的时间窗口。如果在这个窗口中有其他请求进来,它们可能会读到旧的数据库数据,并将旧数据重新写入缓存,导致数据不一致。
这种策略有一些缺陷:
- 首次请求数据一定不在
cache的问题- 解决办法:可以将热点数据可以提前放入
cache中。
- 解决办法:可以将热点数据可以提前放入
- 写操作比较频繁的话导致
cache中的数据会被频繁被删除,这样会影响缓存命中率 。- 解决办法:可以使用
Write Through或者Write Behind策略。
- 解决办法:可以使用
读写穿透(Read/Write Through Pattern)
服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。
读:
- 从
cache中读取数据,读取到就直接返回。 - 读取不到的话,先从 db 读取数据然后写入到
cache中,再返回。
写:
- 先查
cache,cache中不存在,直接更新 db。 cache中存在,则先更新cache,然后cache服务自己更新 db(同步更新cache和 db)
该模式也有首次请求数据不一定在
cache的问题,对于热点数据可以提前放入缓存中。
异步缓存写入(Write Behind Pattern)
Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。这种方式db的写性能非常高,但对数据一致性带来了更大挑战,比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉了。
应用场景:消息队列中消息的异步写入磁盘、MySQL 的
Innodb Buffer Pool机制都用到了这种策略。
Redis除了做缓存还有哪些应用场景
- 分布式锁:基于
Redisson来实现分布式锁。 - 限流:通过 Redis + Lua 脚本的方式来实现限流,也可以直接利用
Redisson中的RRateLimiter来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。 - 消息队列:Redis 自带的
List数据结构可以作为一个简单的队列使用。 - 延时队列:
Redisson内置了延时队列(基于Sorted Set实现的)。 - 分布式
Session:利用String或者Hash数据类型保存Session数据,所有的服务器都可以访问。 - 热点数据存储:将热点数据放入 Redis 中,减少数据库的压力。
Redis可以做消息队列吗
可以。Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。Redis 2.0 之后,Redis 提供了 PUBLISH 和 SUBSCRIBE 命令,可以实现发布订阅模式,也就是说 Redis 也可以做消息队列。
和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此通常建议不要使用 Redis 来做消息队列,完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。
Redis可以做搜索引擎吗
Redis 是可以实现全文搜索引擎功能的,需要借助 RediSearch,这是一个基于 Redis 的搜索引擎模块。
RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检查、标签查询、向量相似度查询、多关键词搜索、分页搜索等功能,算是一个功能比较完善的全文搜索引擎了。相比较于 Elasticsearch 来说,RediSearch 有如下优势:
- 性能更优秀:依赖 Redis 自身的高性能,基于内存操作(
Elasticsearch基于磁盘)。 - 较低内存占用实现快速索引:
RediSearch内部使用压缩的倒排索引,所以可以用较低的内存占用来实现索引的快速构建。
RediSearch 适用于小型项目的简单搜索场景,对于大型项目的搜索场景,还是推荐使用 Elasticsearch。
- 数据量限制:
RedisSearch是基于 Redis 实现的,其能存储的数据量受限于 Redis 的内存容量,不适合存储大量数据。 - 分布式能力较差:
RedisSearch支持分布式部署,但在实际应用中可能会面临一些挑战,如数据分片、节点间通信、数据一致性等问题。Elasticsearch可以支持 PB 级别的数据量,可以轻松扩展到多个节点,利用分片机制提高可用性和性能。 RediSearch聚合功能相较于Elasticsearch功能较弱。- 生态较差:
Elasticsearch可以轻松和常见的一些系统/软件集成比如Hadoop、Spark、Kibana,而RedisSearch则不具备该优势。
基于Redis实现延时任务✅
类似问题:订单在 10 分钟后未支付就失效,如何用 Redis 实现? 红包 24 小时未被查收自动退还,如何用 Redis 实现?
两种方案:
- Redis 过期事件监听
Redisson内置的延时队列
Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。Redisson 内置的延时队列具备下面这些优势:
- 减少了丢消息的可能:
DelayedQueue中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大,也可以使用扫描数据库的方法作为补偿机制。 - 消息不存在重复消费问题:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。
Redis过期事件监听实现延时任务的原理
Redis 2.0 引入了发布订阅 (pub/sub) 功能(其中有一个叫做 channel的概念),涉及发布者(publisher)和订阅者(subscriber,也叫消费者)两个角色:
- 发布者通过
PUBLISH投递消息给指定channel。 - 订阅者通过
SUBSCRIBE订阅它关心的channel。并且,订阅者可以订阅一个或者多个channel。
在 pub/sub 模式下,生产者需要指定消息发送到哪个 channel 中,而消费者则订阅对应的 channel 以获取消息。
Redis 中有很多默认的 channel,Redis 本身向它们发送一些消息,比如 __keyevent@0__:expired,这个 channel 用来监听 Redis 中的 key 过期事件。
实现延时任务的原理就是:在 Redis 中设置一个 key,然后设置过期时间,当 key 过期时,Redis 会向 __keyevent@0__:expired 这个channel发送一条消息,这时候就可以在这个 channel 上监听到这个消息,从而实现延时任务。
Redis过期事件监听实现延时任务有什么缺陷
- 时效性差:过期事件消息是在 Redis 服务器删除
key时发布的,而不是一个key过期之后就会就会直接发布。Redis 采用定期删除+惰性/懒汉式删除。 因此会存在设置了key的过期时间,但到了指定时间key还未被删除,进而没有发布过期事件的情况。 - 丢消息:Redis 的
pub/sub模式中的消息并不支持持久化,如果 Redis 服务器宕机了,过期事件就会丢失。 - 多服务实例下消息重复消费:pub/sub 模式目前只有广播模式,当生产者向特定频道发布一条消息时,所有订阅相关频道的消费者都能够收到该消息。如果有多个服务实例,每个订阅相关频道的实例都会监听到过期事件,这样就会导致消息重复消费的问题。
Redisson内置的延时队列实现延时任务的原理及优势
可以借助 Redisson 内置的延时队列 RDelayedQueue 来实现延时任务功能。RDelayedQueue 基于 Redis 的 SortedSet 来实现,SortedSet 是一个有序集合,可以为每个元素设置一个分数作为权重。利用这一特性,将需要延迟执行的任务设置相应的过期时间作为分数,然后插入到 SortedSet 中,使用zrangebyscore 命令扫描 SortedSet 中过期的元素,将过期元素从中移除并添加到到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被监听到。这样避免了对 SortedSet 的轮询,提高了执行效率。
Redisson 内置的延时队列具备下面这些优势:
- 减少了丢消息的可能:
DelayedQueue中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大,也可以使用扫描数据库的方法作为补偿机制。 - 消息不存在重复消费问题:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。
Redis数据类型✅
- 5种基本数据类型:
数据类型 底层实现 String(字符串)SDS(简单动态字符串)List(列表)LinkedList(双向链表)、ZipList(压缩列表)、QuickList(快速列表)Hash(哈希)Dict(哈希表/字典)、ZipList(压缩列表)Set(集合)Dict(哈希表/字典)、Intset(整数集合)Zset(有序集合)ZipList(压缩列表)、SkipList(跳表) - 3种特殊数据类型:
Bitmap:位图HyperLogLog:基数统计Geo:地理位置
String
String 的实现并没有使用 C语言的字符串类型,而是自己构建了一种 简单动态字符串(Simple Dynamic String,SDS)。SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
常用命令
| 命令 | 介绍 |
|---|---|
| SET key value | 设置指定 key 的值 |
| SETNX key value | 只有在 key 不存在时设置 key 的值 |
| GET key | 获取指定 key 的值 |
| MSET key1 value1 key2 value2 …… | 设置一个或多个指定 key 的值 |
| MGET key1 key2 … | 获取一个或多个指定 key 的值 |
| STRLEN key | 返回 key 所储存的字符串值的长度 |
| INCR key | 将 key 中储存的数字值增一 |
| DECR key | 将 key 中储存的数字值减一 |
| EXISTS key | 判断指定 key 是否存在 |
| DEL key(通用) | 删除指定的 key |
| EXPIRE key seconds(通用) | 给指定 key 设置过期时间 |
应用场景String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
List
Redis 的 List 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
常用命令
| 命令 | 介绍 |
|---|---|
| RPUSH key value1 value2 … | 在指定列表的尾部(右边)添加一个或多个元素 |
| LPUSH key value1 value2 … | 在指定列表的头部(左边)添加一个或多个元素 |
| LSET key index value | 将指定列表索引 index 位置的值设置为 value |
| LPOP key | 移除并获取指定列表的第一个元素(最左边) |
| RPOP key | 移除并获取指定列表的最后一个元素(最右边) |
| LLEN key | 获取列表元素数量 |
| LRANGE key start end | 获取列表 start 和 end 之间 的元素 |
应用场景
- 信息流展示,如最新文章、最新动态。
- 消息队列,只是与专业的消息队列相比,
List功能较弱。
Hash
Hash 是一个 String 类型的 field-value(键值对) 的映射表,适合用于存储对象,后续操作可以直接修改这个对象中的某些字段的值。
Hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表),不过做了更多优化。
常用命令
| 命令 | 介绍 |
|---|---|
| HSET key field value | 设置指定哈希表中指定字段的值 |
| HSETNX key field value | 只有指定字段不存在时设置指定字段的值 |
| HMSET key field1 value1 field2 value2 … | 同时将一个或多个 field-value (域-值)对设置到指定哈希表中 |
| HGET key field | 获取指定哈希表中指定字段的值 |
| HMGET key field1 field2 … | 获取指定哈希表中一个或者多个指定字段的值 |
| HGETALL key | 获取指定哈希表中所有的键值对 |
| HEXISTS key field | 查看指定哈希表中指定的字段是否存在 |
| HDEL key field1 field2 … | 删除一个或多个哈希表字段 |
| HLEN key | 获取指定哈希表中字段的数量 |
| HINCRBY key field increment | 对指定哈希中的指定字段做运算操作(正数为加,负数为减) |
应用场景
- 存储对象,如用户信息、商品信息、文章信息、购物车信息.
Set
Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,类似于 Java 中的 HashSet 。当需要存储无重复列表数据时,Set 是一个很好的选择,且 Set 提供了判断某个元素是否在其内的重要接口,这个也是 List 所不能提供的。
常用命令
| 命令 | 介绍 |
|---|---|
| SADD key member1 member2 … | 向指定集合添加一个或多个元素 |
| SMEMBERS key | 获取指定集合中的所有元素 |
| SCARD key | 获取指定集合的元素数量 |
| SISMEMBER key member | 判断指定元素是否在指定集合中 |
| SINTER key1 key2 … | 获取给定所有集合的交集 |
| SINTERSTORE destination key1 key2 … | 将给定所有集合的交集存储在 destination 中 |
| SUNION key1 key2 … | 获取给定所有集合的并集 |
| SUNIONSTORE destination key1 key2 … | 将给定所有集合的并集存储在 destination 中 |
| SDIFF key1 key2 … | 获取给定所有集合的差集 |
| SDIFFSTORE destination key1 key2 … | 将给定所有集合的差集存储在 destination 中 |
| SPOP key count | 随机移除并获取指定集合中一个或多个元素 |
| SRANDMEMBER key count | 随机获取指定集合中指定数量的元素 |
应用场景
- 存放无重复数据:如文章点赞、用户收藏、共同好友等。
- 需要获取多个数据源交集、并集和差集的场景:如共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等场景。
- 随机获取数据源:如抽奖、随机推荐等。
Zset
Zset相较于Set增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。
常用命令
| 命令 | 介绍 |
|---|---|
| ZADD key score1 member1 score2 member2 … | 向指定有序集合添加一个或多个元素 |
| ZCARD KEY | 获取指定有序集合的元素数量 |
| ZSCORE key member | 获取指定有序集合中指定元素的 score 值 |
| ZINTERSTORE destination numkeys key1 key2 … | 将给定所有有序集合的交集存储在 destination 中,对相同元素对应的 score 值进行 SUM 聚合操作,numkeys 为集合数量 |
| ZUNIONSTORE destination numkeys key1 key2 … | 求并集,其它和 ZINTERSTORE 类似 |
| ZDIFFSTORE destination numkeys key1 key2 … | 求差集,其它和 ZINTERSTORE 类似 |
| ZRANGE key start end | 获取指定有序集合 start 和 end 之间的元素(score 从低到高) |
| ZREVRANGE key start end | 获取指定有序集合 start 和 end 之间的元素(score 从高到底) |
| ZREVRANK key member | 获取指定有序集合中指定元素的排名(score 从大到小排序) |
应用场景
- 需要随机获取数据源中的元素根据某个权重进行排序的场景:各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
- 需要存储的数据有优先级或者重要程度的场景:如任务优先级、消息优先级、商品价格、商品销量等。
Bitmap
Bitmap存储的是连续的二进制数据(1/0),只需要一个 bit 位来表示某个元素对应的值或者状态,非常节省内存。可以将Bitmap 看作是一个存储二进制数字(0和1)的数组,数组中每个元素的下标叫做 offset(偏移量)。
常用命令
| 命令 | 介绍 |
|---|---|
| SETBIT key offset value | 设置指定 offset 位置的值 |
| GETBIT key offset | 获取指定 offset 位置的值 |
| BITCOUNT key start end | 获取 start 和 end 之前值为 1 的元素个数 |
| BITOP operation destkey key1 key2 … | 对一个或多个 Bitmap 进行运算,可用运算符有 AND,OR,XOR 以及 NOT |
应用场景
- 用户签到:每天签到的用户用一个
bit位来表示,offset为用户的id,value为签到状态(1/0)。 - 用户在线状态:用一个
bit位来表示用户的在线状态,offset为用户的id,value为在线状态(1/0)。
HyperLogLog
HyperLogLog 是一种基数统计算法,用于估计一个集合中不重复元素的个数。HyperLogLog 的优势在于占用的内存空间固定,并且不随着统计的元素数量增加而增加。
常用命令
| 命令 | 介绍 |
|---|---|
| PFADD key element1 element2 … | 添加一个或多个元素到 HyperLogLog 中 |
| PFCOUNT key1 key2 | 获取一个或者多个 HyperLogLog 的唯一计数。 |
| PFMERGE destkey sourcekey1 sourcekey2 … | 将多个 HyperLogLog 合并到 destkey 中,destkey 会结合多个源,算出对应的唯一计数。 |
应用场景
- 数量量巨大(百万、千万级别以上)的计数场景:如热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、热门商品 uv 统计等。
Geospatial
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。 通过 GEO 可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。
常用命令
| 命令 | 介绍 |
|---|---|
| GEOADD key longitude1 latitude1 member1 … | 添加一个或多个元素对应的经纬度信息到 GEO 中 |
| GEOPOS key member1 member2 … | 返回给定元素的经纬度信息 |
| GEODIST key member1 member2 M/KM/FT/MI | 返回两个给定元素之间的距离 |
| GEORADIUS key longitude latitude radius distance | 获取指定位置附近 distance 范围内的其他元素,支持 ASC(由近到远)、DESC(由远到近)、Count(数量) 等参数 |
| GEORADIUSBYMEMBER key member radius distance | 类似于 GEORADIUS 命令,只是参照的中心点是 GEO 中的元素 |
应用场景
- 附近的人:根据用户的经纬度信息,获取附近的人。
- 附近的商家:根据用户的经纬度信息,获取附近的商家。
- 附近的车辆:根据用户的经纬度信息,获取附近的车辆。
Set和Zset区别
- 数据存储:Set是一种无序且不重复的数据结构。Zset是一种有序且不重复的数据结构,每个元素关联一个浮点数分数(score),元素根据分数进行排序。
- 功能:Set支持添加、删除和查找元素等基本操作。Zset在Set基础上,还支持根据分数范围进行区间检索。
- 内部实现:Set内部使用Hash表来实现。Zset根据存储情况使用ZipList或者SkipList实现。
- 应用场景:Set适用于存储不需要排序和索引的数据集,如用户的标签、兴趣爱好等。Zset适用于需要根据分数进行排序或检索的场景,如排行榜、商品价格排序等。
Zset底层实现
https://www.cnblogs.com/hld123/p/18074778
Zset根据实际情况动态使用ZipList或者SkipList存储元素,以在内存和性能中找到平衡。
ZipList或者SkipList的切换依赖于两个配置参数:zset-max-ziplist-entries(默认值为128 单位:个)和 zset-max-ziplist-value(默认值为64,单位:字节)。
当 zset 存储的元素数量小于 128,且所有元素的最大长度小于 64kb时,Redis会选择使用压缩列表作为底层实现。压缩列表占用的内存较少,但是在需要修改数据时,可能需要对整个压缩列表进行重写,性能较低。
当 zset 存储的元素数量超过 128,或者任何元素的长度超过 64kb时,Redis 会将底层结构从压缩列表转换为跳跃表。跳跃表的查找和修改数据的性能较高,但是占用的内存也较多。
压缩表ziplist:一种为节省内存而设计的特殊编码结构,它将所有的元素和分数紧凑地存储在一起。这种方式的优点是占用内存少,但是在需要修改数据时,可能需要对整个压缩列表进行重写,性能较低。当 Zset 存储的元素数量较少,且元素的字符串长度较短时,Redis 会选择使用压缩列表作为底层实现。
跳跃表skiplist:一种可以进行快速查找的有序数据结构,它通过维护多级索引来实现快速查找。这种方式的优点是查找和修改数据的性能较高,但是占用的内存也较多。当 zset 存储的元素数量较多,或者元素的字符串长度较长时,Redis 会选择使用跳跃表作为底层实现。
一个zset结构同时包含一个字典和一个跳跃表。跳跃表按score从小到大保存所有集合元素。而字典则保存着从member到score的映射,这样就可以用O(1)的复杂度来查找member对应的score值。虽然同时使用两种结构,但它们会通过指针来共享相同元素的member和score,因此不会浪费额外的内存。
跳表SkipList
https://mp.weixin.qq.com/s?__biz=MzU0ODMyNDk0Mw==&mid=2247495510&idx=1&sn=7a9f174b2a5facd92ee0efccf712eecc&chksm=fb427c76cc35f560d0ce02d6b7ff2f3e28c0349434734a428b20dfa2c3366d6266b15eacb588&scene=27
https://javaguide.cn/database/redis/redis-skiplist.html#%E5%85%83%E7%B4%A0%E6%9F%A5%E8%AF%A2
跳表是一种可以进行二分查找的有序链表,其在有序链表的的基础上添加了多级索引,将增删改查的时间复杂度变为O(logn)。跳表的节点跟一般的链表不同,有val、next、down三个属性,多级索引其实就是多级链表,最下层的链表包含所有元素,上一层元素是下一层的一半,第k层的元素是2^k个。假设跳表元素最大为65536个,则其最大层数为16。
特性:
- 一个跳表有若干层链表组成;
- 每一层链表都是有序的;
- 跳表最下面一层的链表包含所有数据;
- 如果一个元素出现在某一次层,那么该层下面的所有层都必须包含该元素;
- 上一层的元素指向下层的元素必须是相同的;
- 头指针 head 指向最上面一层的第一个元素;
CRUD:
- 插入:每次插入新元素或者增添新索引的时候,进行一次随机运算,随机数值范围为 0-1 之间,如果随机值 > 0.5,则为当前元素添加更高一级的索引,这样可以保证生成一级索引的概率为50%,二级索引的概率为25%…
- 删除:删除时必须定位到当前跳表各层元素小于 要删除元素 的最大值,然后右移(next),看元素是否为要删除的元素,如果是的话删除,然后下沉(down),执行同样操作。
- 查询:查询从最顶层索引开始,每层定位到当前跳表各层元素小于 要查询的元素的最大值,然后下沉,右移,继续比较。
Zset为什么要用跳表
- 跳表 vs 平衡树
- 跳表和平衡书的插入、查询、删除操作的时间复杂度都是 O(logN),但平衡树每一次插入、删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表使用概率平衡而不是严格强制的平衡,插入、删除操作只需要修改相邻节点的指针,不需进行旋转操作,因此跳表的插入、删除操作效率更高。
- 跳表 vs 红黑树
- 跳表不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
- 跳表 vs B+ 树
- B+树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。Redis数据存储在内存中,必不可能需要存储大量数据,因此 B+ 树的优势并不明显。
使用String存储对象还是Hash
String存储的是序列化后的对象数据,存放的是整个对象。Hash是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或常需单独查询对象中个别字段信息,使用Hash。String存储相对来说更加节省内存,缓存相同数量的对象数据,String消耗的内存约是Hash的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,用String。
购物车信息用String还是Hash存储
购物车中的商品频繁修改和变动,建议使用 Hash储存。
- 用户 id 为
key - 商品 id 为
field,商品数量为value
具体维护:
- 用户添加商品就是往 Hash 里面增加新的 field 与 value;
- 查询购物车信息就是遍历对应的 Hash;
- 更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可);
- 删除商品就是删除 Hash 中对应的 field;
- 清空购物车直接删除对应的 key 即可。
用Set实现抽奖系统
SADD key member1 member2 ...:向指定集合添加一个或多个元素。SPOP key count:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。SRANDMEMBER key count: 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
Bitmap统计活跃用户
用日期(精确到天)作为 key,用户 ID 为 offset,如果当日活跃过就设置为 1,否则为 0。通过 BITCOUNT 命令统计某个时间段内的活跃用户数。
统计网站UV、PV
PV(Page Views)页面浏览量
- 定义:PV指的是页面浏览量,即网页被访问的总次数。每次用户加载一个网页,PV就增加一次。即使是同一个用户多次刷新页面,每刷新一次,PV就会增加一次。
- 用途:PV用于衡量网页内容的受欢迎程度和用户对网页的互动频率。PV越高,说明该页面被访问的次数越多。
UV(Unique Visitors)独立访客
- 定义:UV指的是独立访客量,即访问某个网站的不同用户数量。UV通常以天为单位计算,即一天内访问网站的不同用户数量。同一个用户在一天内多次访问,只会被计为1个UV。
- 用途:UV用于衡量网站的访问人数。UV越高,说明网站吸引了更多的不同用户。
使用Jedis:
1 | // PV统计直接使用String,每次访问页面对PV计数器进行累加。 |
PFADD PAGE_1:UV USER1 USER2 ...... USERn:将访问指定页面的每个用户 ID 添加到HyperLogLog中。PFCOUNT PAGE_1:UV:统计指定页面的 UV。
SETNX实现分布式锁及改进
SETNX(Set if Not Exists)命令:
SETNX是 Redis 实现分布式锁的基础命令,它的意思是“如果键不存在,则设置这个键”。因此,它可以用来确保只有一个客户端能成功设置锁:- 当客户端尝试获取锁时,它会使用
SETNX命令去设置一个键(锁),如果键不存在,Redis 会成功设置,并返回1,表示成功获取锁。 - 如果键已经存在,Redis 将返回
0,表示锁已被其他客户端持有,当前客户端无法获取锁。1
SETNX lock_key 1
- 当客户端尝试获取锁时,它会使用
设置锁的过期时间:如果使用
SETNX命令设置了锁,但由于某些原因客户端未能及时释放锁,可能会导致死锁。为了解决这个问题,通常会给锁设置一个过期时间:使用
EXPIRE命令为键设置一个过期时间,以防止锁被永久持有。1
EXPIRE lock_key 10 # 锁在10秒后自动过期
Redis 还提供了
SET命令的扩展,可以在一个命令中同时实现SETNX和设置过期时间:1
SET lock_key "value" NX EX 10
上面的命令表示:如果
lock_key不存在,则设置其值,并同时设置 10 秒的过期时间。
释放锁:锁是客户端占用的,应该由占用锁的客户端释放。释放锁时,客户端只需删除对应的键即可:
1
DEL lock_key
防止误删锁:锁的误删问题可能发生在以下场景中:某个客户端获取了锁,但过期时间到了,锁自动释放,而另一个客户端获取了新的锁。如果第一个客户端仍然认为自己持有锁,就可能错误地删除了其他客户端设置的锁。
- 为了解决这个问题,可以给每个锁加上唯一标识,只有持有该标识的客户端才能删除锁:
- 当获取锁时,使用随机生成的唯一标识作为锁的值。
- 在释放锁时,先检查锁的值是否与自己持有的标识一致,只有一致时才删除锁。
1
2
3
4
5if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end - 这是一个 Lua 脚本,确保锁的释放是原子性的,防止误删其他客户端的锁。
RedLock 分布式锁算法:在分布式系统中,单个 Redis 节点的锁机制可能不够稳定,因此 Redis 官方提出了一种基于多个 Redis 实例的分布式锁算法,称为 RedLock。原理如下:
- 客户端依次向多个(通常是 5 个) Redis 实例请求锁。
- 如果在大多数实例上成功获取锁,则认为锁获取成功。
- 锁的过期时间必须一致,确保锁最终会被自动释放。
List实现滑动窗口展示数据
使用Redis的GEO获取用户附近的商户,假设现在5公里范围内有非常多的商户,采用滑动窗口的方式展示商户数据,用Redis的哪种数据类型来实现滑动窗口?
- 存储商户ID列表:假设通过
GEO获取到用户5公里范围内的所有商户的 ID,可以将这些商户 ID 按照一定顺序(例如距离从近到远)存储在一个 Redis 列表中。比如可以用LPUSH或RPUSH命令依次将商户 ID 放入列表。 - 滑动窗口展示:当需要分页展示商户时,可以利用
LRANGE命令获取列表中的一部分数据,模拟滑动窗口。例如:LRANGE merchants_list 0 9可以获取第1页的10个商户数据。LRANGE merchants_list 10 19可以获取第2页的数据,依次类推。
- 滑动窗口更新:如果需要滑动更新商户列表,也可以使用
LPOP或RPOP等操作逐步移除已经展示的商户数据,保持列表的滑动更新。 - 分页机制:滑动窗口可以通过维护当前页的索引来实现,比如记录每次滑动后的开始和结束索引,然后通过
LRANGE提取对应区间的数据。
总之,Redis 的列表数据结构非常适合这种顺序性、分页性要求的场景。
Redis实现乐观锁
Redis 实现乐观锁的方式主要通过使用 WATCH、MULTI、EXEC 命令来实现。具体步骤如下:
- WATCH:使用
WATCH命令来监控一个或多个键。在调用WATCH后,如果这些键中的任何一个在事务执行前被修改(即在EXEC前被修改),事务将不会被执行,而是返回失败。Redis 在事务开始前会检查被监控的键是否发生了变化,从而实现类似乐观锁的机制。 - MULTI:接下来使用
MULTI开始事务。事务中的所有命令都会被放入队列,直到执行EXEC为止。 - EXEC:使用
EXEC提交事务。如果在WATCH之后到EXEC之前,被监控的键发生了变化,EXEC将返回nil,表示事务未执行成功。此时,客户端可以选择重试事务。 - UNWATCH:如果客户端决定放弃监控键,可以使用
UNWATCH取消WATCH的监控。
代码示例:
1 | import redis |
在这个例子中,如果在 WATCH 和 EXEC 之间其他客户端修改了 balance,pipe.execute() 将抛出 WatchError,事务失败,客户端可以选择重试。
Redis持久化机制✅
使用缓存的时候,经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。
为什么要持久化
- 重用数据(比如重启机器、机器故障之后恢复数据)
- 数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。
三种持久化方式
- 快照(
snapshotting, RDB) - 只追加文件(
append-only file, AOF) - RDB 和 AOF 的混合持久化(Redis 4.0 新增)
RDB持久化
Redis通过创建快照获取存储在内存里面的数据在 某个时间点 上的副本。快照创建后,可以对其进行备份,也可以将其复制到其他服务器,从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器时使用。
RDB创建时会阻塞主线程吗
有两个命令生成 RDB 快照文件
save:同步保存操作,会阻塞 Redis 主线程;bgsave:fork出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
AOF持久化
与快照持久化相比,AOF 持久化的实时性更好。开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式(fsync策略)的配置决定何时将系统内核缓存区的数据同步到硬盘中的。
只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险(系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了)。
AOF工作流程
- 命令追加(
append):所有的写命令会追加到 AOF 缓冲区中。 - 文件写入(
write):将 AOF 缓冲区的数据写入到系统内核缓冲区中。这一步系统调用write函数,将数据写入到了系统内核缓冲区之后直接返回(延迟写)。 - 文件同步(
fsync):系统内核缓冲区根据对应的持久化方式(fsync策略)向硬盘做同步操作。这一步系统调用fsync函数,fsync用于强制刷新系统内核缓冲区(同步到到磁盘),它将阻塞直到写入磁盘完成后返回,确保写磁盘操作结束才会返回。 - 文件重写(
rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。 - 重启加载(
load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。

AOF持久化的三种同步策略
即 fsync 策略,用于控制系统内核缓冲区的数据何时同步到硬盘中。
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为什么是执行完命令后记录日志
不同于 MySQL 先记录日志再执行命令(方便故障恢复),Redis 先执行命令再记录日志(追求性能)。
- 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
- 在命令执行完之后再记录,不会阻塞当前的命令执行。
但也存在风险:
- 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
- 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。
AOF重写
当 AOF 变得太大时,Redis 在后台创建一个子线程,自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
重写期间,AOF维护一个AOF重写缓冲区,记录创建新AOF文件期间执行的所有写命令,当创建新AOF文件完成后,将AOF重写缓冲区内容追加到新AOF文件末尾。最后用新AOF文件替换旧AOF文件。
可以配置如下参数:auto-aof-rewrite-min-size:如果 AOF 文件大小小于该值,则不会触发 AOF 重写。默认值为 64 MB;auto-aof-rewrite-percentage:执行 AOF 重写时,当前 AOF 大小(aof_current_size)和上一次重写时 AOF 大小(aof_base_size)的比值。如果当前 AOF 文件大小增加了这个百分比值,将触发 AOF 重写。将此值设置为 0 将禁用自动 AOF 重写。默认值为 100。
AOF校验机制
Redis 在启动时对 AOF 文件进行检查,以判断文件是否完整,是否有损坏或者丢失的数据。具体原理就是使用校验和(checksum) 的数字来验证 AOF 文件。这个校验和是通过对整个 AOF 文件内容进行 CRC64 算法计算得出的数字。如果文件内容发生了变化,那么校验和也会随之改变。
Redis 在启动时会比较计算出的校验和与文件末尾保存的校验和(计算的时候会把最后一行保存校验和的内容给忽略点),从而判断 AOF 文件是否完整。
Redis4.0对持久化的优化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
如何选择RDB和AOF
RDB优势:
- RDB文件存储的内容是经过压缩的二进制数据,非常适合用于备份和恢复数据。而 AOF 文件存储的内容是每次写命令,会比RDB文件大。
- RDB文件恢复数据快,直接解析还原数据即可,而AOF文件需要逐行执行命令来恢复数据。
AOF优势:
- RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。AOF 支持秒级数据丢失,仅仅是追加命令到 AOF 文件,操作轻量。
- RDB文件以二进制格式保存数据,且Redis 版本迭代中存在多版本的 RDB,可能存在兼容性问题。
- AOF 以一种易于理解和解析的格式包含所有操作的日志。可以方便地导出 AOF 文件进行分析。
综上:
- Redis 保存的数据丢失一些也没什么影响的话,可以选择使用 RDB。
- 如果保存的数据要求安全性比较高的话,建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化。
- 不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误。
Redis单线程模型✅
Redis 从设计上是单线程的,也就是说,Redis 使用一个线程来处理所有客户端的请求。这一设计在 Redis 的早期版本中非常明显,所有的操作都在单个线程上完成,避免了线程上下文切换的开销,提高了性能。
为什么单线程模型性能高?
- 内存操作为主:Redis 主要将数据存储在内存中,内存操作的速度非常快,因此没有必要使用多线程来加速计算。
- 避免并发问题:单线程模型避免了多线程编程中的锁、竞态条件等并发问题,大大简化了开发和维护的复杂性。
- IO多路复用:虽然是单线程,但通过 IO 多路复用机制,可以同时处理大量客户端连接。
IO多路复用✅
通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
Redis 支持多种 IO 多路复用库,包括:
selectepoll(Linux)kqueue(Mac OS)evport(Solaris)
I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。
文件事件处理器主要是包含 4 个部分:
- 多个 socket(客户端连接)
- IO多路复用程序(支持多个客户端连接的关键)
- 文件事件分派器(将
socket关联到相应的事件处理器) - 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
工作原理:
IO多路复用机制的基本工作原理是:
- Redis 将多个 socket 添加到 IO 事件监控列表中,等待事件发生(如读就绪、写就绪等)。
- 当有一个或多个 socket 上有事件时,Redis 的事件循环会被唤醒,并开始依次处理这些就绪的 socket。
- 每次处理一个客户端请求并返回结果,处理完后继续监听下一个 IO 事件。
优点
- 高效处理大量连接:虽然是单线程,但通过 IO 多路复用机制,Redis 可以处理数千甚至数万个客户端的连接请求。
- 低开销:与为每个连接创建一个线程相比,IO 多路复用不需要额外的线程管理开销,减少了系统资源的消耗。

事件驱动模型
Redis 的事件驱动模型依赖于两个核心事件:
- **文件事件(File Event)**:处理客户端的连接、请求和响应,主要依赖于 IO 多路复用。
- **时间事件(Time Event)**:处理定时任务,如定期的任务执行、定期检查是否有超时连接等。
Redis4.0多线程
在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作。这里引入的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主线程之外的其他线程来“异步处理”,从而减少对主线程的影响。
异步命令:
UNLINK:可以看作是DEL命令的异步版本。FLUSHALL ASYNC:用于清空所有数据库的所有键,不限于当前SELECT的数据库。FLUSHDB ASYNC:用于清空当前SELECT数据库中的所有键。
总得来说,Redis 6.0 之前,Redis 的主要操作仍然是单线程处理的。
Redis6.0之前为什么不用多线程
- 单线程编程容易并且更容易维护;
- Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
Redis6.0之后为何引入多线程
Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。这里的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此不需要担心线程安全问题。
Redis后台线程
Redis 是单线程模型(主要逻辑是单线程完成的),但实际还有一些后台线程用于执行比较耗时的操作:
- 通过
bio_close_file后台线程来释放 AOF/RDB 等过程中产生的临时文件资源。 - 通过
bio_aof_fsync后台线程调用fsync函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。 - 通过
bio_lazy_free后台线程释放大对象(已删除)占用的内存空间.
lua脚本如何确保原子操作
- 单线程:Redis是单线程模型,其在同一时间只能执行一个命令。Lua脚本是在Redis的主线程上执行的,且Redis在执行Lua脚本时会阻塞其他命令的执行。这种机制确保了在执行Lua脚本的过程中,不会有其他Redis命令插入或打断脚本的执行。因此,整个Lua脚本中的所有操作(包括读取和写入)是连续执行的,不会被其他命令中断,从而实现了原子性。
- 事务:Lua脚本中,所有的Redis命令都是作为一个整体执行的。即使脚本中包含多个Redis命令,这些命令在逻辑上也是作为一个不可分割的单元来执行的。如果脚本执行过程中发生错误,整个脚本会回滚,没有部分操作会生效。这进一步确保了脚本执行的原子性。
Redis内存管理✅
Redis给缓存数据设置过期时间有啥用
- 因为Redis是内存数据库,内存有限,如果数据一直不过期,会导致内存被耗尽,进而影响系统的正常运行。
- 某些业务场景就是需要某个数据只在某一时间段内存在,过期后就没用了,比如验证码、短信验证码等。若传统的数据库,一般都是自己判断过期,这样更麻烦并且性能要差很多。
Redis如何判断数据过期
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
在查询一个 key 的时候,Redis 首先检查该 key 是否存在于过期字典中(时间复杂度为 O(1)),如果不在就直接返回,在的话需要判断一下这个 key 是否过期,过期直接删除 key 然后返回 null。
Redis过期key删除策略
- 惰性删除:只会在取出/查询
key的时候才对数据进行过期检查。这样对 CPU 最友好,但可能会造成太多过期key没有删除。 - 定期删除:周期性地随机从设置了过期时间的
key中抽查一批,然后逐个检查这些key是否过期,过期就删除key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。 - 延迟队列:把设置过期时间的
key放到一个延迟队列里,到期之后就删除key。这样可保证每个过期key都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。 - 定时删除:每个设置了过期时间的
key都会在设置的时间到达时立即被删除。这样可确保内存中不会有过期的键,但它对 CPU 压力最大,因为它需要为每个键都设一个定时器。
Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略,这也是大部分缓存框架的选择。两者结合使用既能 CPU 友好,又能内存友好。
定期删除策略详细细节
- Redis 默认每 100ms 检查 20 个随机的过期
key,如果有过期的key就删除。如果执行时间超过阈值 25ms,就中断这一次定期删除循环,以避免使用过多的 CPU 时间。如果在这一批中过期key超过10%,就会重复执行此删除流程,以更积极地清理过期 key。如果过期的 key 比例低于这个比例,就会中断这一次定期删除循环,避免做过多的工作而获得很少的内存回收。
Redis内存淘汰策略✅
当 Redis 内存不足时,会根据配置的内存淘汰策略来删除一些数据,以释放内存空间。Redis有 6 种内存淘汰策略:
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):从数据集(server.db[i].dict)中移除最近最少使用的数据淘汰。allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。no-eviction(默认内存淘汰策略):禁止驱逐数据,当内存不足以容纳新写入数据时,新写入操作会报错。
4.0 版本后增加以下两种:
volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。allkeys-lfu(least frequently used):从数据集(server.db[i].dict)中移除最不经常使用的数据淘汰。
allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期时间的键值中淘汰数据。
Redis事务
Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。Redis 事务实际开发中使用的非常少,功能比较鸡肋,不满足原子性(Redis不支持回滚)和持久性(always持久化策略性能太差,其他策略都会有数据丢失),事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。
如何解决Redis事务的缺陷
可以使用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。不过严格来说,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。
Redis性能优化✅
批量操作减少网络传输
Redis 是基于内存的数据库,网络传输是 Redis 性能的瓶颈之一。可以通过批量操作来减少网络传输次数,提高 Redis 性能。
批量操作的三种方式
- 原生批量操作:Redis 提供了一些原生的批量操作命令,如
MSET、MGET、DEL等。 - Pipeline:通过
pipeline可以将多个命令打包成一个请求一次性发送给 Redis 服务器,减少网络传输次数。 - Lua脚本:
大量key集中过期如何处理
如果大量 key 集中过期的问题,可能会使 Redis 的请求延迟变高。解决:
- 尽量避免
key集中过期,在设置键的过期时间时尽量随机一点。 - 对过期的
key开启lazyfree机制,这样会在后台异步删除过期的 key,不会阻塞主线程的运行。
大量key集中过期
Redis采用定期删除+惰性/懒汉式删除清理过期key。如果大量 key 集中过期的问题,客户端请求必须等待定期清理过期 key 任务线程执行完成(定期任务线程是在主线程中执行的),这可能会使 Redis 的请求延迟变高。解决:
- 给
key设置随机过期时间。 - 开启
lazy-free(惰性删除/延迟释放),让 Redis 采用异步方式延迟释放key使用的内存,将删除key的操作该操作交给单独的子线程处理,避免阻塞主线程。
bigkey(大key)
如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。bigkey的标准:
String类型的value超过 1MB- 复合类型(
List、Hash、Set、Sorted Set等)的value包含的元素超过 5000 个(不过,对于复合类型的value来说,不一定包含的元素越多,占用的内存就越多)。
产生原因
- 程序设计不当,比如直接使用
String类型存储较大的文件对应的二进制数据。 - 对于业务的数据规模考虑不周到,比如使用集合类型的时候没有考虑到数据量的快速增长。
- 未及时清理垃圾数据,比如哈希中冗余了大量的无用键值对。
危害
- 单线程的Redis 在操作
bigkey时候比较耗时,造成客户端超时阻塞。 - 网络拥塞:获取
bigkey需要较多的流量。 - 工作线程阻塞:如果使用
del删除大key时,会阻塞工作线程,这样就没办法处理后续的命令。 bigkey会进一步影响主从同步。
bigkey发现
--bigkeys:执行redis-cli -p 6379 --bigkeys扫描所有key,只能找到top 1 bigkey(占用内存最大的String数据类型,包含元素最多的复合数据类型)。SCAN+STRLEN/HLEN/LLEN:Redis 自带的SCAN命令按指定模式扫描匹配的key,然后结合STRLEN/HLEN/LLEN返回key的长度或者成员数量。- 集合类型可以使用
MEMORY USAGE - 借助开源工具分析 RDB 文件。如
redis-rdb-tools、rdb_bigkeys - 使用公有云的 Redis 分析服务,如阿里云、腾讯云等。
处理bigkey
- 分割
bigkey:将bigkey分割为多个小key。例如,将一个含有上万字段数量的Hash按照一定策略(比如二次哈希)拆分为多个Hash。 - 手动清理:Redis 4.0+ 可以使用
UNLINK命令来异步删除一个或多个指定的key。Redis 4.0 以下可以考虑使用SCAN命令结合DEL命令来分批次删除。 - 采用合适的数据结构:例如,文件二进制数据不使用
String保存、使用HyperLogLog统计页面UV、Bitmap保存状态信息(0/1)。 - 开启
lazy-free(惰性删除/延迟释放):lazy-free特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放key使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
也可以配合使用上述方法
hotkey(热key)
如果一个 key 的访问次数比较多且明显多于其他 key,就可以看作是 hotkey。
产生原因
某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。
危害
hotkey会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。- 如果突然访问
hotkey的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。
hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性。
发现hotkey
--hotkeys:执行redis-cli -p 6379 --hotkeys命令MONITOR: Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。该命令对性能影响很大,禁止长时间开启。- 可在合适的时机短暂执行
MONITOR命令并将输出重定向至文件,关闭命令后通过对文件中请求进行归类分析即可找出这段时间中的hotkey。
- 可在合适的时机短暂执行
- 根据业务情况提前预估。比如参与秒杀活动的商品数据等。无法预估所有
hotkey,比如突发的热点新闻事件等。 - 使用公有云的 Redis 分析服务,如阿里云、腾讯云等。
处理hotkey
- 读写分离:主节点处理写请求,从节点处理读请求。
- 使用
Redis Cluster:将热点数据分散存储在多个 Redis 节点上。 - 二级缓存:
hotkey采用二级缓存的方式进行处理,将hotkey存放一份到 JVM 本地内存中(可以用 Caffeine)。 - 使用公有云的 Redis 服务中的解决方案。
也可以配合使用上述方法。
hotkey为什么是性能的瓶颈,QPS1000访问1个key与QPS1000访问1000个key有什么区别?
热键(hotkey)在高并发场景中会成为性能瓶颈,主要原因是它导致对单个资源的竞争加剧。具体来讲,QPS(Queries Per Second)10000 访问一个 key 和 QPS 10000 访问 10000 个 key 的区别如下:
- 热键问题:
- 访问单个 key 的场景:当所有请求都集中访问一个 key 时,系统的资源(如缓存、数据库、网络、CPU 等)都集中在对该 key 的操作上。这种情况下,访问一个 key 会造成对单一资源(如内存、CPU 或 I/O)的高竞争,从而导致锁争用、队列排队、缓存击穿等问题。尤其在分布式系统中,访问单个 key 可能导致热点 shard(分片)超载,影响整个系统性能。
- 访问多个 key 的场景:如果请求分布在多个不同的 key 上,系统的负载能够更均匀地分摊在多个资源上,不会出现单点过载的情况。这样可以充分利用系统的并发处理能力,减少锁争用和排队的情况。
- 资源争用:
- 访问一个 key:所有操作(如读写)都针对同一个数据项,尤其是在并发写的情况下,会导致锁争用和串行化处理。例如,在某些缓存系统(如 Redis)中,如果对一个 key 有大量并发请求,可能会导致 key 的 eviction(淘汰)、缓存击穿等问题,进一步加重数据库的压力。
- 访问多个 key:多个 key 的访问能更好地分散资源的使用,降低单点的竞争。请求的处理可以并行进行,不会因为单个 key 的高并发导致性能瓶颈。
- 延迟和吞吐量:
- 访问一个 key:高并发访问一个 key 时,延迟通常会增加,因为请求可能需要排队等待处理,尤其是写操作。而且读操作可能会受到缓存击穿或失效影响,导致频繁访问后端存储,增加系统负担。
- 访问多个 key:由于没有集中访问同一个 key,资源使用更均匀,延迟较低,吞吐量更高。系统可以同时处理多个请求而不会出现瓶颈。
总结:
- QPS 10000 访问 1 个 key:容易导致资源集中竞争,产生锁争用、缓存击穿、负载不均等问题,成为性能瓶颈。
- QPS 10000 访问 10000 个 key:负载更均匀,系统能够更好地利用并发能力,减少资源争用问题,从而提高整体性能。
慢查询命令
Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) 时间复杂度的命令,如KEYS *、HGETALL等。这些命令可能会导致 Redis 阻塞,影响 Redis 的性能。
Redis内存碎片✅
Redis内存碎片就是内存中的一些不连续的小块内存,这些小块内存无法被利用,但又无法释放,造成内存浪费。内存碎片不会影响性能,但会增加内存消耗。
内存碎片产生原因
- Redis 存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
- Redis 采用多种内存分配器分配内存,默认采用
jemalloc,其按照 2 的幂次方分配内存,可能会导致内存浪费。
- Redis 采用多种内存分配器分配内存,默认采用
- 频繁修改 Redis 中的数据也会产生内存碎片。
查看内存碎片率
使用 info memory 命令查看 Redis 内存碎片率。
mem_fragmentation_ratio (内存碎片率)= used_memory_rss (操作系统实际分配给 Redis 的物理内存空间大小)/ used_memory(Redis 内存分配器为了存储数据实际申请使用的内存空间大小)
内粗碎片率越大,说明 Redis 中的内存碎片越多。used_memory_rss 减去 used_memory 的值不仅包括内粗碎片,还包括其他进程开销,以及共享库、堆栈等的开销。
通常内存碎片率mem_fragmentation_ratio > 1.5(使用 Redis 存储实际大小 2G 的数据需要使用大于 3G 的内存)就需要清理内存碎片了
Redis内存碎片清理
- 重启节点可以做到内存碎片重新整理。如果采用的是高可用架构的 Redis 集群的话,可以将碎片率过高的主节点转换为从节点,以便进行安全重启。
- Redis4.0-RC3可以通过
config set activedefrag yes开启主动碎片整理功能,Redis 会在后台自动整理内存碎片。1
2
3
4
5
6
7config set activedefrag yes # 开启主动碎片整理功能
config set active-defrag-ignore-bytes 500mb # 内存碎片占用空间达到 500mb 的时候开始清理
config set active-defrag-threshold-lower 50 # 内存碎片率大于 1.5 的时候开始清理
config set active-defrag-cycle-min 20 # 内存碎片清理所占用 CPU 时间的比例不低于 20%
config set active-defrag-cycle-max 50 # 内存碎片清理所占用 CPU 时间的比例不高于 50%
Redis生产问题✅
缓存穿透
缓存穿透是指查询一个不存在的数据,导致每次请求都落到数据库上。
缓存穿透是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
解决方案
- 做参数校验。一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。可以直接在前端做参数校验。
- 缓存无效
key。短暂地缓存无效key,并设置一个较短的过期时间,这样就可以防止缓存穿透。 - 布隆过滤器。布隆过滤器是一种数据结构,对所有可能查询的参数以
hash形式存储在布隆过滤器中,当一个请求过来时,首先通过布隆过滤器判断这个请求的参数是否存在,如果不存在,直接返回,如果存在再去查询数据库。 - 接口限流。根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单。
缓存击穿
缓存击穿是指某个热点数据在缓存失效的瞬间有大量请求访问。
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
举例
秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。
解决方案
- 永不过期(不推荐):设置热点数据永不过期或者过期时间比较长。
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
- 加锁(看情况):在缓存失效后,设置互斥锁确保只有一个请求去查询数据库并更新缓存。 互斥锁:使用SETNX命令 + 设置过期时间作为互斥锁来解决缓存击穿问题。确保了数据的一致性,但是性能会收到一定影响。
- 逻辑过期时间:在原有数据基础上添加一个过期时间字段。通过对比当前时间和逻辑过期时间,判断缓存是否过期。当缓存过期时,先使用过期的旧数据,然后再异步更新缓存。性能很好,但是数据一致性不能保证。
缓存雪崩
缓存雪崩是指在某一时刻大量缓存失效,导致数据库压力骤增。
缓存雪崩是缓存在同一时间大面积的失效或者是Redis宕机,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。
当有网络波动的时候,请求进行重试也可能会导致缓存雪崩。
举例
数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。
解决方案
针对大量缓存同时失效的情况:
- 设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
- 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
- 多级缓存(推荐):设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
- 使用Redis集群(推荐):使用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
针对 Redis 服务不可用的情况:
- Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案。
- 多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
缓存穿透/缓存击穿区别
- 缓存穿透中,请求的
key既不存在于缓存中,也不存在于数据库中。 - 缓存击穿中,请求的
key对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。
缓存预热如何实现
- 使用定时任务,比如
xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。 - 使用消息队列,比如
Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。
缓存击穿/缓存雪崩区别
缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。
Redis常见阻塞原因
- O(n)命令:如
KEYS *(返回所有符合规则的key)、HGETALL(会返回一个Hash中所有的键值对)等,这些命令可能会导致 Redis 阻塞,影响 Redis 的性能。 Save创建RDB快照:save命令会使用 Redis 服务器主进程,直到 RDB 快照创建完成,从而阻塞了主进程的其他操作。- AOF记录AOF文件阻塞:AOF 在执行完命令后,记录 AOF 文件,这个记录过程是在 Redis 主线程中进行的。
- AOF刷盘阻塞:开启 AOF 持久化后,每执行一条命令,都会
append到AOF缓冲区,然后write到系统内核缓冲区,最后fsync刷到磁盘。当磁盘压力太大的时候,会导致fsync操作发生阻塞,主线程调用write函数时也会被阻塞。 - AOF重写阻塞:AOF文件重写后,会把存储在AOF重写缓冲区中的创建新AOF文件期间的写命令追加到新的 AOF 文件末尾,这个追加操作是在主线程中进行的,可能会阻塞主线程。
- 查找和删除
bigkey:查找bigkey时,Redis 会遍历所有的key,这个操作是阻塞的。删除bigkey时,会先释放bigkey内存,然后将这块内存插入到内存空闲链表中,如果短时间释放大量的bigkey,空闲内存块插入链表的操作就会阻塞主线程。 - 清空数据库:
flushall和flushdb命令会删除和释放所有键值对,然后将内存插入空闲内存链表中,这个操作是阻塞的。 - CPU竞争:Redis 是典型的 CPU 密集型应用,不建议和其他多核 CPU 密集型服务部署在一起。当其他进程过度消耗 CPU 时,将严重影响 Redis 的吞吐量。
- 网络问题:连接拒绝、网络延迟,网卡软中断等网络问题也可能会导致 Redis 阻塞。
Redis如何容灾
Redis容灾是指在Redis服务出现故障时,确保数据的高可用性和一致性。常见的Redis容灾策略包括:
- 集群模式(Cluster):Redis Cluster是Redis内置的分布式实现,可以在多个节点间自动分片存储数据,并且支持自动故障转移。当某个节点失效时,集群中的其他节点会接管其数据和请求,确保服务的持续运行。
- 优点:数据分片,支持大规模数据存储和高可用性。
- 缺点:配置复杂,数据一致性要求较高。
- 主从复制(Master-Slave Replication):Redis支持主从复制,即在一台主服务器(Master)上写入的数据会自动复制到一台或多台从服务器(Slave)。当主服务器出现故障时,可以手动或自动将从服务器提升为主服务器,继续提供服务。
- 优点:配置简单,可以实现读写分离。
- 缺点:在主服务器故障后需要手动切换,数据可能会有短暂的丢失(未同步到从服务器的数据)。
- 哨兵模式(Sentinel):哨兵模式是在主从复制的基础上增加了监控、通知和自动故障转移功能。Redis Sentinel会监控主从节点的状态,如果发现主节点故障,会自动将某个从节点提升为主节点,并通知其他从节点和应用程序。
- 优点:自动故障转移,提高高可用性。
- 缺点:需要额外的哨兵实例,占用资源。
- 持久化机制(Persistence):Redis提供了RDB和AOF两种持久化机制,可以将内存中的数据定期保存到磁盘上,防止数据丢失。在灾难发生后,可以通过持久化文件恢复数据。
- RDB:定期将内存中的数据快照保存到磁盘上,适合备份。
- AOF:将每次写操作记录到日志文件中,可以实现更高的持久性。
- 优点:数据恢复能力强。
- 缺点:需要权衡持久化频率与性能的关系。
- 异地多活和冷备方案:为应对更大范围的灾难(如机房故障),可以采用异地多活或冷备方案,将Redis集群部署在多个地理位置,并进行实时同步或定期备份。在某个机房发生故障时,可以快速切换到异地的数据中心。
- 异地多活:多个数据中心同时提供服务,数据同步较为复杂。
- 冷备:异地数据中心仅作为备份,在主数据中心故障时启动。
- 数据备份与恢复:定期对Redis数据进行备份(如导出RDB文件),并存储到安全的位置(如异地存储、云存储)。在出现不可恢复的灾难时,可以通过备份文件进行数据恢复。
- 优点:备份灵活,支持灾难恢复。
- 缺点:备份频率和恢复速度影响可用性。
Redis高可用
Redis 高可用主要涉及通过不同的技术和部署架构来保证 Redis 在发生故障时仍然能够继续提供服务,避免单点故障。以下是 Redis 高可用的几种实现方式:
主从复制(Master-Slave Replication)
- 架构:Redis 支持一主多从的架构,即一个主节点 (Master) 负责写入数据,多个从节点 (Slave) 负责同步主节点的数据,以提供读取服务。
- 实现:
- 从节点会主动向主节点发送同步请求,主节点会把当前的数据集发送给从节点,从节点收到后会加载数据并保存。
- 主节点会将写操作日志发送给从节点,从节点再执行这些操作,从而实现数据同步。
- 优点:读写分离,可以通过增加从节点来提高读的性能。
- 缺点:如果主节点故障,需要手动或者通过其他机制进行故障转移。
哨兵模式(Sentinel)
- 架构:Redis Sentinel 是一种监控和管理主从复制集群的机制。它能在主节点故障时自动进行故障转移。
- 实现:
- 哨兵节点会定期检查主从节点的状态,一旦发现主节点不可用,会通过选举机制选出一个新的主节点,并让其他从节点与新的主节点进行同步。
- 哨兵也负责通知客户端新的主节点地址,使客户端能够无缝切换。
- 优点:实现了高可用性和自动故障恢复,减少了人工干预。
- 缺点:哨兵本身需要高可用性部署,多个哨兵节点协同工作才能避免单点故障。
Redis Cluster
- 架构:Redis Cluster 是 Redis 官方提供的分布式解决方案,支持数据分片和自动故障转移。
- 实现:
- Redis Cluster 将数据分为多个槽 (slot),每个节点负责一部分槽的数据。集群节点之间会通过 Gossip 协议交换状态信息。
- 当一个节点不可用时,集群中的其他节点会自动将其替换为从节点,并重新分配数据槽。
- 数据在多个节点上自动分片存储,从而实现横向扩展和高可用。
- 优点:原生支持分布式,自动分片和故障转移,适合大规模部署。
- 缺点:集群管理复杂,要求客户端支持 Redis Cluster 协议,且不支持多键事务。
Keepalived+Redis
- 架构:使用 Keepalived 和 Redis 进行高可用部署,通常是在主从复制的基础上,通过虚拟 IP(VIP)的漂移来实现高可用。
- 实现:
- Keepalived 监控 Redis 主节点的状态,当主节点不可用时,会将 VIP 切换到从节点,确保客户端可以继续通过同一 IP 地址访问 Redis。
- 优点:配置简单,客户端无需感知主从切换。
- 缺点:故障转移时间较长,容易出现短时间内的不可用状态。
其他高可用技术的结合
- Proxy 层:如 Twemproxy 或 Codis,可以作为 Redis 前端的代理,管理多个 Redis 实例,实现请求路由和故障转移。
- 高可用框架:可以将 Redis 作为后端缓存服务,与 Kubernetes 等容器编排系统结合,实现自动扩展、自动重启和健康检查等功能。
Redis集群如何横向扩展数据存储量
在Redis集群中,横向扩展数据存储量主要通过增加节点(即分片)的方式实现。以下是具体步骤和方法:
- 添加新节点:
- 向现有Redis集群中添加新的Redis节点。每个节点都是一个独立的Redis实例,负责处理集群中一部分数据的存储和请求。
- 新增节点后,Redis集群将重新平衡数据,将部分数据从现有节点迁移到新节点,以分散存储压力。
- 重新分配数据槽:
- Redis集群使用哈希槽(hash slots)机制将数据分片分布在不同的节点上。Redis集群中的每个键都根据CRC16算法映射到一个哈希槽(共16384个槽位),每个节点管理一部分槽位。
- 当添加新的节点时,集群会重新分配槽位,将一部分槽位从现有节点转移到新节点,数据也会相应地迁移。这一过程可以通过
reshard命令手动完成,也可以使用Redis的自动分片功能。
- 数据迁移:
- Redis提供了工具(如
redis-trib或redis-cli中的CLUSTER命令)来帮助将数据从旧节点迁移到新节点。这一过程是在后台进行的,确保集群在迁移过程中仍然可用。
- Redis提供了工具(如
- 水平扩展的注意事项:
- 数据均衡性:随着节点的增加,需确保数据均匀分布在所有节点上,以避免某些节点成为瓶颈。
- 网络带宽:数据迁移过程会消耗网络带宽,因此在扩展期间需要注意网络的负载情况。
- 应用兼容性:确保应用程序能够处理数据分布在多个节点上的情况。大部分客户端库(如Jedis、Lettuce等)已经支持Redis集群模式。
通过以上方法,Redis集群能够在不影响服务可用性的情况下,实现数据存储量的横向扩展。
mongodb
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写。MongoDB 提供面向文档的存储方式,支持“无模式”的数据建模,可存储较复杂的数据类型,是一款非常流行的 文档类型数据库。
在高负荷下,MongoDB 天然支持水平扩展和高可用,可以很方便地添加更多的节点/实例,以保证服务性能和可用性。
mongodb的存储结构是什么
主要由三个单元组成:
- 数据库(Database):一个 MongoDB 可以有多个数据库,每个数据库都有自己的集合和文档。
- 集合(Collection):一个数据库可以有多个集合,每个集合可以有多个文档。类似于关系型数据库中的表(
Table)。 - 文档(Document):MongoDB 中最基本的单元,由 BSON(Binary JSON) 键值对(
key-value)组成,类似于关系型数据库中的行(Row)。
SQL 与 MongoDB 常见术语对比:
| SQL | MongoDB |
|---|---|
| 表(Table) | 集合(Collection) |
| 行(Row) | 文档(Document) |
| 列(Col) | 字段(Field) |
| 主键(Primary Key) | 对象 ID(Objectid) |
| 索引(Index) | 索引(Index) |
| 嵌套表(Embedded Table) | 嵌入式文档(Embedded Document) |
| 数组(Array) | 数组(Array) |
集合
集合存储于数据库中,没有固定的结构,也就是 无模式 的,这意味着可以往集合插入不同格式和类型的数据。
集合不需要事先创建,当第一个文档插入或者第一个索引创建时,如果该集合不存在,则会创建一个新的集合。
文档
BSON 文档由键值对组成的数据结构,BJSON 的遍历速度优于 JSON,但 BJSON 需要更多的存储空间。
mongodb特点
- 数据被存储为文档:记录被存在 BSON 文档里面,文档是mongodb的基本数据单元。
- 数据模型自由:不需要设计表结构,可以存储不同结构的文档。
- 支持多种查询方式:支持读写操作 (CRUD)以及数据聚合、文本搜索和地理空间查询。
- 支持ACID事务:MongoDB单文档具备原子性,也具备事务特性, 4.0 版本开始支持多文档事务,4.2 版本开始支持分布式事务。
- 高效的二进制存储:文档以键值对的形式存储在集合中,键(
ObjectId类型)唯一标识文件,值是以 BSON 形式存在。 - 自带数据压缩功能:MongoDB 3.0 版本开始支持
WiredTiger存储引擎,支持数据压缩,减少磁盘空间占用。 - 支持多种类型索引:支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等。
- 支持自动故障恢复:提供自动故障恢复的功能,主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。
- 支持分片集群:支持集群自动切分数据,在数据插入和更新时,能够自动路由和存储。
- 支持存储大文件:单文档要求不大于 16MB,对于大于 16MB 的文件,使用
GridFS存储。
mongodb适合什么场景
MongoDB 的优势在于其数据模型和存储引擎的灵活性、架构的可扩展性以及对强大的索引支持。适用于大数据量的存储、多类型索引、高性能读写、高可用性和可扩展性的场景。
mongodb存储引擎
MongoDB 采用的是 插件式的存储引擎架构,现在主要使用两种存储引擎:
MMAPv1存储引擎:使用了内存映射文件技术,支持复制集和分片集群,4.x 版本已经不再支持。WiredTiger存储引擎:自 MongoDB 3.2 以后,默认的存储引擎为WiredTiger存储引擎,它提供文档级并发模型、检查点和数据压缩(后文会介绍到)等功能。In-Memory存储引擎:In-Memory 存储引擎在 MongoDB Enterprise 中可用。它不是将文档存储在磁盘上,而是保留在内存中以获得更可预测的数据延迟。
WiredTiger存储引擎
WiredTiger 存储引擎自 3.2 版本后成为 MongoDB 的默认存储引擎,其默认使用B+树作为索引结构,但也支持 LSM(Log Structured Merge) 树。
使用 B+ 树时,WiredTiger 以 page 为基本单位往磁盘读写数据。B+ 树的每个节点为一个 page,共有三种类型的 page:
root page(根节点):B+ 树的根节点。internal page(内部节点):不实际存储数据的中间索引节点。leaf page(叶子节点):真正存储数据的叶子节点,包含一个页头(page header)、块头(block header)和真正的数据(key/value),其中页头定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息;块头定义了此页的checksum、块在磁盘上的寻址位置等信息。
mongodb聚合
将多个文档甚至是多个集合汇总到一起计算分析(比如求和、取最大值)并返回计算后的结果,这个过程被称为聚合操作。通过聚合可以:
- 将来自多个文档的值组合在一起。
- 对集合中的数据进行的一系列运算。
- 分析数据随时间的变化。
两种聚合方法:
- 聚合管道(Aggregation Pipeline):执行聚合操作的首选方法。
- 单一目的聚合方法(Single purpose aggregation methods):也就是单一作用的聚合函数比如
count()、distinct()、estimatedDocumentCount()。
聚合管道由多个阶段组成,每个阶段在文档通过管道时转换文档。每个阶段接收前一个阶段的输出,进一步处理数据,并将其作为输入数据发送到下一个阶段。管道工作流程:
- 接受一系列原始数据文档
- 对这些文档进行一系列运算
- 结果文档输出给下一个阶段
mongodb事务
支持ACID事务:MongoDB单文档具备原子性,也具备事务特性, 4.0 版本开始支持多文档事务,4.2 版本开始支持分布式事务。
mongodb数据压缩
借助 WiredTiger 存储引擎,MongoDB 支持数据压缩,减少磁盘空间占用。WiredTiger 存储引擎支持以下压缩算法:
snappy:快速压缩算法,压缩比(3~5倍)较低,但速度快。zlib:压缩比(5~7倍)较高,但速度较慢。Zstandard:针对实时压缩场景,具有更好的压缩比,提供更高的压缩率和更低的 CPU 使用率,MongoDB 4.2 开始可用。
WiredTiger 日志也会被压缩(大于128kb),默认使用的也是 Snappy 压缩算法。
mongodb索引
索引的目的主要是用来提高查询效率,如果没有索引的话,MongoDB 必须扫描集合中的每个文档,以选择与查询语句匹配的文档。如果查询存在合适的索引,MongoDB 可以使用该索引来限制它必须检查的文档数量,并且可以使用索引中的排序返回排序后的结果。
索引可以显著缩短查询时间,但是使用索引、维护索引是有代价的。
索引类型
- 单字段索引:建立在单个字段上的索引。
- 复合索引:建立在多个字段上的索引,也可以称之为组合索引、联合索引。
- 多键索引:MongoDB的一个字段可能是数组,在对这种字段创建索引时,就是多键索引。MongoDB 会为数组的每个值创建索引。就是说你可以按照数组里面的值做条件来查询,这个时候依然会走索引。
- 哈希索引:按数据的哈希值索引,用在哈希分片集群上。
- 文本索引:支持对字符串内容的文本搜索查询。文本索引可以包含任何值为字符串或字符串元素数组的字段。一个集合只能有一个文本搜索索引,但该索引可以覆盖多个字段。MongoDB 虽然支持全文索引,但是性能低下,暂时不建议使用。
- 地理位置索引:基于经纬度的索引,适合 2D 和 3D 的位置查询。
- 唯一索引:确保索引字段不会存储重复值。如果集合已经存在了违反索引的唯一约束的文档,则后台创建唯一索引会失败。
- TTL 索引:TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间,当一个文档达到预设的过期时间之后就会被删除.
复合索引中字段顺序
复合索引中字段的顺序非常重要,MongoDB 会按照复合索引中字段的顺序来建立索引。如果查询条件中的字段顺序和复合索引中的字段顺序一致,那么 MongoDB 可以使用该索引。
复合索引遵循最左前缀原则,拥有多个键的索引,可以同时得到所有这些键的前缀组成的索引,但不包括除左前缀之外的其他子集。
有一个类似
{a: 1, b: 1, c: 1, ..., z: 1}这样的索引,那么实际上也等于有了{a: 1}、{a: 1, b: 1}、{a: 1, b: 1, c: 1}等一系列索引,但是不会有{b: 1}这样的非左前缀的索引。
TTL索引
TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间 expireAfterSeconds,当一个文档达到预设的过期时间之后就会被删除。TTL 索引除了有 expireAfterSeconds 属性外,和普通索引一样。
数据过期对于某些类型的信息很有用,比如机器生成的事件数据、日志和会话信息,这些信息只需要在数据库中保存有限的时间。
原理
- MongoDB 会开启一个后台线程读取该 TTL 索引的值判断文档是否过期,但不保证已过期的数据立马被删除,因后台线程每 60 秒触发一次删除任务,且如果删除的数据量较大,会存在上一次的删除未完成,而下一次的任务已经开启的情况,导致过期的数据也会出现超过了数据保留时间 60 秒以上的现象。
- 对于副本集而言,TTL 索引的后台进程只会在 Primary 节点开启,在从节点会始终处于空闲状态,从节点的数据删除由主库删除后产生的
oplog做同步。
TTL索引限制
- TTL 索引是单字段索引。复合索引不支持 TTL
- 如果某个字段已经存在非 TTL 索引,那么在该字段上无法再创建 TTL 索引。
mongodb索引/MySQL索引
- 数据模型:
- MongoDB:是一个文档型数据库,使用BSON格式存储数据。索引可以基于文档的字段,支持嵌套字段索引。
- MySQL:是关系型数据库,使用表结构。索引通常是在表的列上创建的。
- 索引类型:
- MongoDB:支持多种索引类型,包括单字段索引、复合索引、地理空间索引、全文索引等。此外,MongoDB 还支持哈希索引和稀疏索引。
- MySQL:主要支持B树索引和哈希索引,同时也支持全文索引和空间索引等。B树索引是最常用的,适用于大多数查询。
- 索引创建与维护:
- MongoDB:索引创建较为简单,可以在插入文档时自动创建索引。同时,MongoDB在写入时会自动维护索引。
- MySQL:索引创建和维护可能更加复杂,尤其是在涉及多表联接和复杂查询时。索引的维护会影响写入性能。
- 查询优化:
- MongoDB:查询优化器使用基于成本的优化算法,能够自动选择最佳索引。MongoDB还提供了查询分析工具,可以帮助优化查询。
- MySQL:也使用基于成本的优化器,查询优化器会根据统计信息选择最佳索引。MySQL的
EXPLAIN语句可以帮助分析查询执行计划。
- 性能影响:
- MongoDB:索引加速读取操作,但会增加写入和存储开销。过多的索引可能导致性能下降。
- MySQL:同样,索引可以加速查询,但会增加写入的延迟和存储成本。
- 适用场景:
- MongoDB:更适合于需要灵活数据模型和高并发读写的场景,比如内容管理系统、社交网络等。
- MySQL:更适合于结构化数据,且事务性要求较高的应用场景,比如银行系统和订单管理系统等。
总的来说,MongoDB和MySQL的索引设计反映了它们不同的数据库架构和应用场景,选择使用哪种索引类型应根据具体的需求和数据特性来决定。









