目录
[DBMS 类型](#DBMS 类型)
[主键 vs 外键](#主键 vs 外键)
[DROP vs DELETE vs TRUNCATE](#DROP vs DELETE vs TRUNCATE)
[ORDER BY 排序(默认升序)](#ORDER BY 排序(默认升序))
[GROUP BY 分组](#GROUP BY 分组)
[IN 和 BETWEEN](#IN 和 BETWEEN)
[JOIN 连接](#JOIN 连接)
[UNION 组合](#UNION 组合)
[MySQL 为什么能在数据库领域长期占据主流地位](#MySQL 为什么能在数据库领域长期占据主流地位)
[MySQL 字段类型](#MySQL 字段类型)
[整数类型的 UNSIGNED 属性](#整数类型的 UNSIGNED 属性)
[CHAR vs VARCHAR](#CHAR vs VARCHAR)
[VARCHAR(100) vs VARCHAR(10)](#VARCHAR(100) vs VARCHAR(10))
[DECIMAL vs FLOAT/DOUBLE](#DECIMAL vs FLOAT/DOUBLE)
[DATETIME vs TIMESTAMP](#DATETIME vs TIMESTAMP)
[NULL vs ''](#NULL vs '')
[Boolean 类型如何表示?](#Boolean 类型如何表示?)
[手机号存储用 INT 还是 VARCHAR](#手机号存储用 INT 还是 VARCHAR)
[MySQL 基础架构](#MySQL 基础架构)
[MySQL 存储引擎](#MySQL 存储引擎)
[MyISAM vs InnoDB](#MyISAM vs InnoDB)
[MySQL 索引](#MySQL 索引)
[MySQL 日志](#MySQL 日志)
[MySQL 事务](#MySQL 事务)
[MySQL 性能优化](#MySQL 性能优化)
[Redis 为什么这么快?](#Redis 为什么这么快?)
[为什么要用 Redis?](#为什么要用 Redis?)
[为什么用 Redis 而不用本地缓存呢?](#为什么用 Redis 而不用本地缓存呢?)
[Redis 应用](#Redis 应用)
[Redis 数据类型](#Redis 数据类型)
[String vs Hush](#String vs Hush)
[购物车信息用 String 还是 Hash 存储更好呢?](#购物车信息用 String 还是 Hash 存储更好呢?)
[使用 Redis 实现一个排行榜怎么做?](#使用 Redis 实现一个排行榜怎么做?)
[Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+ 树?](#Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+ 树?)
[Set 的应用场景是什么?](#Set 的应用场景是什么?)
[使用 Bitmap 统计活跃用户怎么做?](#使用 Bitmap 统计活跃用户怎么做?)
[HyperLogLog 适合什么场景?](#HyperLogLog 适合什么场景?)
如果我想判断一个元素是否不在海量元素集合中,用什么数据类型?
[Redis 持久化机制](#Redis 持久化机制)
[RDB 持久化](#RDB 持久化)
[AOF 持久化](#AOF 持久化)
[RDB 和 AOF 的混合持久化(Redis 4.0 )](#RDB 和 AOF 的混合持久化(Redis 4.0 ))
[Redis 线程模型](#Redis 线程模型)
[Redis 内存管理](#Redis 内存管理)
[过期 key 删除策略](#过期 key 删除策略)
[大量 key 集中过期怎么办?](#大量 key 集中过期怎么办?)
[Redis 内存淘汰机制](#Redis 内存淘汰机制)
[Redis 事务](#Redis 事务)
[Redis 性能优化](#Redis 性能优化)
[Lua 脚本](#Lua 脚本)
[Redis 生产问题](#Redis 生产问题)
😺数据库基础
数据库 (Database - DB):指按照一定数据模型组织、存储的结构化数据集合,是实际存储数据的载体。
数据库管理系统 (Database Management System - DBMS):是一种管理数据库的软件,如 MySQL、Oracle。主要负责数据的存储、查询、事务管理、并发控制和权限控制,并提供 SQL 接口操作数据。
数据库系统 (Database System - DBS):是一个更完整的系统,包括数据库(DB)、数据库管理系统(DBMS)、硬件设备、应用程序以及用户。
数据库管理员 (Database Administrator - DBA ):负责数据库系统的设计、维护和管理,包括性能优化、备份恢复、安全控制和高可用架构。
DB 是数据,DBMS 是管理软件,DBS 是整体系统,DBA 是管理人员。
数据库管理系统(DBMS)
主要提供以下四大核心功能:
- 数据定义功能(DDL)
通过数据定义语言对数据库对象进行定义和管理,包括表、索引、视图、存储过程等。 - 数据操作功能(DML)
提供对数据的增删改查操作,是开发中最常使用的功能。 - 数据控制功能
包括事务管理、并发控制、完整性约束和权限控制,用于保证数据的正确性、安全性和一致性。 - 数据库维护功能
包括数据备份与恢复、性能监控、日志管理以及数据导入导出,用于保证数据库系统的稳定运行。
Q:DDL 和 DML 有什么区别?
A:DDL 操作数据库结构(表、索引等),DML 操作数据内容(增删改查)
本质区别:结构 vs 数据
DBMS 类型
DBMS(数据库管理系统)可以按照数据模型和架构设计分为三大类:
1. 关系型数据库(RDBMS):如 MySQL、Oracle,基于表结构和 SQL,支持事务(ACID),适用于结构化数据和对一致性要求高的场景。B+树索引、事务(redo/undo log)、锁。
2. NoSQL(为互联网而生):互联网带来了海量数据、高并发、数据结构多变,NoSQL核心思想就是用一致性换性能,用结构换灵活性。去事务或弱事务、分布式存储、最终一致性(BASE)。适用于缓存、大数据、日志等场景。
(1)键值型(Key-Value)Redis
(2)文档型(Document)MongoDB
(3)列式数据库(Column)HBase、Cassandra
(4)图数据库(Graph)Neo4j
3. NewSQL(解决NoSQL的缺陷):分布式关系型数据库,结合了 NoSQL 的扩展能力和关系型数据库的事务特性,支持 SQL 和 ACID,例如 TiDB、OceanBase、Spanner。
实际开发中怎么用? 通常是混合使用。MySQL → 核心业务数据,Redis → 缓存
Q:为什么会出现 NoSQL?
A:因为关系型数据库在海量数据和高并发场景下扩展性不足,NoSQL通过牺牲部分事务特性换取高性能和水平扩展能力。
Q:NewSQL 解决了什么问题?
A:NewSQL 解决了 NoSQL 不支持强事务的问题,在保证 ACID 的同时支持分布式扩展。
ACID 是数据库事务的四大特性:
A:Atomicity 原子性:事务是最小单位,不可分割。
C:Consistency 一致性:执行前后,数据库的完整性约束不变。
I:Isolation 隔离性:多个事务同时跑时,互相不干扰。
D:Durability 持久性:事务一旦提交成功 ,数据就永久保存,断电、重启也不会丢。
关系型数据库属性
(1)元组(Tuple):表中的一行数据。
(2)码(Key):是能够唯一标识元组的一个或多个属性的集合。
- 候选码(Candidate Key):是最小的能够唯一标识元组的属性集合,一个关系中可以有多个候选码。例如,在学生表中,如果"学号"能唯一标识学生,同时"身份证号"也能唯一标识学生,那么{学号}和{身份证号}都是候选码。
- 码 / 主键(Primary Key):从候选码中选出的一个,用于唯一标识元组,一个关系只能有一个主码。
- 外码 / 外键(Foreign Key):一个表中的属性,引用另一个表的主码,用于建立表之间的关系。
(3)属性分类
- 主属性(Prime Attribute):包含在任一候选码中的属性。
- 非主属性(Non-prime Attribute):不属于任何候选码的属性。
Q:候选码和主键有什么区别?
A:候选码是所有可能唯一标识元组的最小属性集合,而主键是从候选码中选出的一个,作为唯一标识。
ER图
Entity Relationship Diagram(实体联系图),提供了表示实体类型、属性和联系的方法。
- 实体(Entity):表示现实世界中的对象,在数据库中通常对应一张表。
- 属性(Attribute):表示实体的特征,在数据库中对应表的字段。
- 联系(Relationship):表示实体之间的关系,在数据库中通过外键或中间表实现。

实体之间常见的关系包括:
- 一对一(1:1):用户 ↔ 身份证,外键 or 合并表
- 一对多(1:N):班级 → 学生,在"多"的一方加外键(学生表:class_id)
- 多对多(M:N):学生 ↔ 课程,引入中间表(选课表:student_id、course_id)
Q:多对多关系如何实现?
A:通过引入中间表,将多对多关系拆分为两个一对多关系。
数据库范式
👉 一套"设计规范",用于减少数据冗余、避免数据异常,范式 = 让表结构更合理的规则。
函数依赖:如果 X 能唯一确定 Y,即 X → Y,则 Y 依赖 X。
部分函数依赖:若主键是联合主键(X = X1 + X2),存在非主属性Y,满足:Y 依赖于 X 的某一个部分(X1 或 X2),而不是依赖于整个联合主键 X,那么就称 Y 对 X 存在 部分函数依赖。
1NF(第一范式): 要求字段具有原子性,即每个字段不可再分。1NF 是所有关系型数据库的最基本要求 ,也就是说关系型数据库中创建的表一定满足第一范式。
反例:
| uid | name | phone |
|---|---|---|
| 1 | 张三 | 13800138000、13900139000 |
| 2 | 李四 | 13712345678 |
必须:
| uid | name | phone1 | phone2 |
|---|---|---|---|
| 1 | 张三 | 13800138000 | 13900139000 |
| 2 | 李四 | 13712345678 | NULL |
2NF(第二范式): 在满足1NF的基础上,要求非主属性完全依赖于主键,消除部分函数依赖。
反例:
| 学号 | 课程号 | 姓名 | 课程名 | 成绩 |
|---|---|---|---|---|
| 001 | C01 | 张三 | 数学 | 90 |
| 001 | C02 | 张三 | 英语 | 85 |
| 002 | C01 | 李四 | 数学 | 88 |
主键是 (学号,课程号) 一起才能唯一确定一行。姓名只依赖学号,跟课程号无关,课程名只依赖课程号,跟学号无关。
|--------|--------|
| 学号 | 姓名 |
| 001 | 张三 |
| 002 | 李四 |
|---------|---------|
| 课程号 | 课程名 |
| C01 | 数学 |
| C02 | 英语 |
|--------|---------|--------|
| 学号 | 课程号 | 成绩 |
| 001 | C01 | 90 |
| 001 | C02 | 85 |
| 002 | C01 | 88 |
**3NF(第三范式):**在满足2NF的基础上,要求非主属性不依赖于其他非主属性,消除传递函数依赖。
反例:
| 学号 | 系名 | 系主任 |
|---|
学号 → 系名,系名 → 系主任,传递依赖。拆表:
| 学号 | 系名 |
|---|
| 系名 | 系主任 |
|---|
1NF 管字段,2NF 管部分依赖,3NF 管传递依赖。
主键 vs 外键
- 主键(Primary Key)
用于唯一标识表中的每一行数据,具有唯一性和非空性,一张表只能有一个主键,用于保证实体完整性。 - 外键(Foreign Key)
用于建立表与表之间的关系,一个表中的外键引用另一张表的主键或唯一键,外键可以重复,也可以为空,用于保证引用完整性。
主键用于标识数据唯一性,外键用于维护表之间的关联关系。
Q:主键可以用 UUID 吗?
A:可以,但不推荐作为聚簇索引,因为 UUID 无序,会导致索引分裂,影响性能,通常建议使用自增 ID 或雪花算法。
Q:外键一定引用主键吗?
A:不一定,也可以引用唯一键(Unique Key),但最常见的是引用主键。
Q:为什么很多公司不用外键?
A:因为外键会增加数据库操作的性能开销(每次做 INSERT、DELETE 或者 UPDATE 都必须考虑外键约束,需要额外检查,增加数据库负担),并且在分库分表场景下难以维护(分库分表下外键无法生效),且外键使表之间高度耦合,不利于后续系统扩展和维护。因此通常在应用层保证数据一致性。
但是但是但是,外键也是有很多好处的:
①保证了数据库数据的一致性和完整性。②级联操作方便,减轻了程序代码量。
不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分库分表,并发量不是很高的情况还是可以考虑使用外键的。
存储过程
存储过程是数据库中预编译的SQL语句集合,可以将多条SQL语句和控制逻辑封装在一起,形成一个可重复调用的数据库对象。
它的优点包括:
- 减少网络交互,提高执行效率
- 预编译执行,性能较好
- 封装复杂业务逻辑
但在现代互联网系统中通常不推荐使用,原因包括:
- 调试困难,缺乏良好工具支持
- 维护成本高,不利于版本管理
- 扩展性差,不符合分布式架构
- 增加数据库负担
因此,通常将业务逻辑放在应用层实现,数据库只负责数据存储和查询。
DROP vs DELETE vs TRUNCATE
| 对应命令 | 操作 |
|---|---|
| DROP | 直接删除整个表(连结构一起删),属于DDL操作。 |
| TRUNCATE | 删除所有数据,但保留表结构,属于DDL操作。 |
| DELETE | 逐行删除数据,可以带条件WHERE、可以回滚(事务中),属于DML操作。 |
DML(Data Manipulation Language 数据操作语言)管数据内容:用于对数据库表中的数据进行操作,包括插入、更新、删除和查询,作用对象是表中的数据。
DDL(Data Definition Language 数据定义语言)管数据结构:用于对数据库对象进行定义和管理,包括创建、修改和删除表、索引等结构,作用对象是数据库结构。
Q:TRUNCATE 为什么不能回滚?
A:因为 TRUNCATE 属于DDL操作,会直接释放数据页,不记录逐行undo日志,因此无法回滚。
Q:DELETE 为什么比 TRUNCATE 慢?
A:DELETE 是逐行删除,并且需要记录日志和维护索引,而 TRUNCATE 是直接释放数据页。
😺数据库设计
需求 → ER图 → 表设计 → 性能设计 → 实施 → 运维
(1)需求分析阶段:分析业务需求,明确数据、功能、性能和安全要求。
(2)概念结构设计(ER图阶段):将需求抽象为概念模型,识别实体、属性和关系,并绘制ER图。
(3)逻辑结构设计:ER → 表、属性 → 字段、关系 → 外键 / 中间表、数据库三范式优化。
(4)物理结构设计:设计数据库的存储方式,包括索引、分区和存储引擎等。
(5)数据库实施阶段:真正建库 + 写SQL。
(6)运行和维护阶段:进行性能优化、监控、备份恢复和系统维护。
例子 - 电商平台:
(1)需求分析阶段:用户可以注册登录、用户可以浏览商品、用户可以下单购买、系统需要处理高并发下单(秒杀)、订单需要保证数据安全和一致性。
(2)概念结构设计(ER图阶段):
- 实体:User(用户)、Product(商品)、Order(订单)、OrderItem(订单详情)
- 关系:User 1-N Order、Order 1-N OrderItem、Product 1-N OrderItem
(3)逻辑结构设计:
- user(用户表):user_id(主键)、name、phone
- product(商品表):product_id(主键)、name、price
- order(订单表):order_id(主键)、user_id(外键)、create_time
- order_item(订单详情表):id(主键)、order_id(外键)、product_id(外键)、quantity
多对多 → 拆表(order_item);1对多 → 外键关联;满足第三范式减少冗余。
(4)物理结构设计:大表 order 按时间分区(提升查询性能)、存储引擎选择 InnoDB(支持事务)、热数据(商品)可接入 Redis 缓存。
(5)数据库实施阶段。
(6)运行和维护阶段。
电商系统的数据库设计流程是:
先分析用户、商品、订单等业务需求,然后通过ER图进行概念建模,再转换为用户表、商品表、订单表等关系模型,接着进行索引与分区等性能优化,之后完成建库与SQL实现,最后通过监控、缓存与分库分表进行持续优化与维护。
😺NoSQL
NoSQL(Not Only SQL)泛指非关系型的数据库,主要针对的是键值、文档以及图形类型数据存储。NoSQL 数据库天生支持分布式,数据冗余和数据分片等特性,旨在提供可扩展的高可用高性能数据存储解决方案。
NoSQL 数据库代表:MongoDB、Redis。
与关系型数据库相比,NoSQL 具有以下特点:
- 数据模型灵活(如键值、文档、列式、图)
- 支持横向扩展(分布式)
- 性能高,适合大规模数据
- 通常弱化事务支持
NoSQL 类型:
- 键值:键值数据库是一种较简单的数据库,其中每个项目都包含键和值。这是极为灵活的 NoSQL 数据库类型,因为应用可以完全控制 value 字段中存储的内容,没有任何限制。Redis 和 DynanoDB 是两款非常流行的键值数据库。
- 文档:文档数据库中的数据被存储在类似于 JSON(JavaScript 对象表示法)对象的文档中,非常清晰直观。每个文档包含成对的字段和值。这些值通常可以是各种类型,包括字符串、数字、布尔值、数组或对象等,并且它们的结构通常与开发者在代码中使用的对象保持一致。MongoDB 就是一款非常流行的文档数据库。
- 图形:图形数据库旨在轻松构建和运行与高度连接的数据集一起使用的应用程序。图形数据库的典型使用案例包括社交网络、推荐引擎、欺诈检测和知识图形。Neo4j 和 Giraph 是两款非常流行的图形数据库。
- 宽列:宽列存储数据库非常适合需要存储大量的数据。Cassandra 和 HBase 是两款非常流行的宽列存储数据库。
SQL 和 NoSQL 的核心区别:
SQL 强调结构化数据和事务一致性,适合业务系统;
NoSQL 强调高性能和扩展性,适合大数据和高并发场景。
|---------|----------------------------------------------|------------------------------------------------------------------------------------------|
| | SQL 数据库 | NoSQL 数据库 |
| 数据存储模型 | 结构化存储,具有固定行和列的表格 | 非结构化存储。文档:JSON 文档,键值:键值对,宽列:包含行和动态列的表,图:节点和边 |
| 例子 | Oracle、MySQL、Microsoft SQL Server、PostgreSQL | 文档:MongoDB、CouchDB,键值:Redis、DynamoDB,宽列:Cassandra、 HBase,图表:Neo4j、 Amazon Neptune、Giraph |
| ACID 属性 | 提供原子性、一致性、隔离性和持久性 (ACID) 属性 | 通常不支持 ACID 事务,为了可扩展、高性能进行了权衡,少部分支持比如 MongoDB 。不过,MongoDB 对 ACID 事务 的支持和 MySQL 还是有所区别的。 |
| 性能 | 性能通常取决于磁盘子系统。要获得最佳性能,通常需要优化查询、索引和表结构。 | 性能通常由底层硬件集群大小、网络延迟以及调用应用程序来决定。 |
| 扩展 | 垂直(使用性能更强大的服务器进行扩展)、读写分离、分库分表 | 横向(增加服务器的方式横向扩展,通常是基于分片机制) |
| 用途 | 普通企业级的项目的数据存储 | 用途广泛比如图数据库支持分析和遍历连接数据之间的关系、键值数据库可以处理大量数据扩展和极高的状态变化 |
| 查询语法 | 结构化查询语言 (SQL) | 数据访问语法可能因数据库而异 |
Q:NoSQL 一定不支持事务吗?
A:不完全正确,部分 NoSQL(如 MongoDB)支持事务,但一般不如关系型数据库强。
Q:为什么 Redis 这么快?
A:因为 Redis 基于内存操作,避免磁盘IO,同时数据结构简单。
Q:NoSQL 可以替代 MySQL 吗?
A:不能完全替代,两者适用场景不同,通常在系统中组合使用。
Q:NoSQL 数据库不能很好地存储关系型数据吗?
A:NoSQL 数据库可以存储关系型数据,只是与关系型数据库的存储方式不同。
😺SQL语法基础知识总结
基本概念
数据库(database):是存储有组织数据的容器,通常由多个数据表组成。
数据表(table):某一类数据的结构化集合,由行和列构成。
模式(Schema):用于定义数据库或表的结构,包括字段名称、数据类型等。
列(Column):表中的字段,用于描述数据的属性。
行(Row):表中的一条记录,表示一个具体的数据实体。
主键(Primary Key):用于唯一标识每一行数据的字段或字段组合,具有唯一性和非空性。
SQL 按功能主要分为四类:
- DDL(数据定义语言)
用于定义数据库结构,如 CREATE、ALTER、DROP。 - DML(数据操作语言)
用于操作表中的数据,包括 INSERT、UPDATE、DELETE、SELECT,实现增删改查。 - TCL(事务控制语言)
用于管理事务,包括 COMMIT 和 ROLLBACK。 - DCL(数据控制语言)
用于控制用户权限,包括 GRANT 和 REVOKE。
👉 DDL 定义结构,DML 操作数据,TCL 控制事务,DCL 管理权限。
DML(CRUD)
Create(增) → INSERT Delete(删) → DELETE
Update(改) → UPDATE Read(查) → SELECT
INSERT
插入完整一行
sql
INSERT INTO user
VALUES (10, 'root', 'root', 'xxx@163.com');
插入多行
sql
INSERT INTO user
VALUES
(10, 'root', 'root', 'a@163.com'),
(11, 'user1', 'user1', 'b@163.com'),
(12, 'user2', 'user2', 'c@163.com');
插入指定列
sql
INSERT INTO user(username, password, email)
VALUES ('admin', 'admin', 'xxx@163.com');
插入查询结果(数据迁移 / 数据复制)
sql
INSERT INTO user(username)
SELECT name
FROM account;
DELETE
删除指定行
sql
DELETE FROM user
WHERE username = 'robot';
TRUNCATE TABLE 可以清空表,也就是删除所有行。说明:TRUNCATE 语句不属于 DML 语法而是 DDL 语法。
sqlTRUNCATE TABLE user;
UPDATE
sql
UPDATE user
SET username='robot', password='robot'
WHERE username = 'root';
SELECT
查询单列
sql
SELECT prod_name
FROM products;
查询多列
sql
SELECT prod_name
FROM products;
查询所有列
sql
SELECT *
FROM products;
DISTINCT(去重)
sql
SELECT DISTINCT vend_id
FROM products;
LIMIT(分页)
sql
-- 返回前 5 条
SELECT * FROM mytable LIMIT 5;
SELECT * FROM mytable LIMIT 0, 5;
-- 返回第 3 ~ 5 行
SELECT * FROM mytable LIMIT 2, 3;
ORDER BY 排序(默认升序)
sql
-- 默认升序
SELECT * FROM products
ORDER BY prod_price;
-- 降序
SELECT * FROM products
ORDER BY prod_price DESC;
-- 多列排序
SELECT * FROM products
ORDER BY prod_price DESC, prod_name ASC;
ORDER BY 的底层原理:
如果没有索引,使用排序算法(filesort)。
如果有索引,可能直接利用索引顺序(避免排序)。
ORDER BY 是否走索引 = 性能关键
Q:ORDER BY 一定会排序吗?
A:不一定,如果有合适的索引,数据库可以直接利用索引顺序。
GROUP BY 分组
把多行数据按某个字段分组,再对每组进行统计。
group by 通常涉及聚合函数:COUNT() 统计数量、SUM() 求和、AVG() 平均值、MAX() 最大值、MIN() 最小值。
sql
SELECT cust_name, COUNT(cust_address) AS addr_num
FROM Customers GROUP BY cust_name;
-- 分组后排序
SELECT cust_name, COUNT(cust_address) AS addr_num
FROM Customers GROUP BY cust_name
ORDER BY cust_name DESC;
having:
- having 用于对汇总的 group by 结果进行过滤。
- having 一般都是和 group by 连用。
- where 和 having 可以在相同的查询中。
sql
SELECT cust_name, COUNT(*) AS NumberOfOrders
FROM Customers
WHERE cust_email IS NOT NULL
GROUP BY cust_name
HAVING COUNT(*) > 1;
| 对比项 | WHERE | HAVING |
|---|---|---|
| 作用对象 | 原始数据 | 分组后的数据 |
| 是否支持聚合函数 | ❌ 不支持 | ✅ 支持 |
| 执行顺序 | GROUP BY 前 | GROUP BY 后 |
数据库内部的执行顺序
SQL 执行不是按你写的顺序执行,而是:
FROM(先找数据) → WHERE(过滤行) → GROUP BY(分组) → HAVING(过滤组) → SELECT(选字段 + 计算) → ORDER BY(排序) → LIMIT(截取)
Q:COUNT 是什么时候执行的?
A:在 GROUP BY 之后,对每个分组单独计算。
Q:WHERE为什么不能用聚合函数?(COUNT 为例)
A:WHERE 在 GROUP BY 前执行,COUNT 在 GROUP BY 后才产生。
👉 统计结果是在分组完成之后才计算出来的,COUNT 不是对"原始数据"算的,而是对"分组后的每一组"算的。
子查询
子查询是嵌套在主查询中的 SQL 查询,也称为内部查询。
子查询可以嵌入 SELECT、INSERT、UPDATE 和 DELETE 语句中,也可以和 =、<、>、IN、BETWEEN、EXISTS 等运算符一起使用。
子查询可以作为外层查询的条件或数据源使用,常见于 WHERE 子句和 FROM 子句中。在 WHERE 子句中,子查询用于提供过滤条件,可以返回单值或集合。
- 在 FROM 子句中,子查询作为临时表使用,需要使用别名。
- 子查询的执行顺序是先执行内层查询,再执行外层查询。
WHERE
- WHERE 子句用于过滤记录,即缩小访问数据的范围。
- WHERE 后跟一个返回 true 或 false 的条件。
- WHERE 可以与 SELECT,UPDATE 和 DELETE 一起使用。
- 可以在 WHERE 子句中使用的操作符:>、>=、=、<、<=、!=、BETWEEN、LIKE、IN。
IN 和 BETWEEN
- IN 操作符在 WHERE 子句中使用,作用是在指定的几个特定值中任选一个值。
- BETWEEN 操作符在 WHERE 子句中使用,作用是选取介于某个范围内的值。
sql
SELECT *
FROM products
WHERE vend_id IN ('DLL01', 'BRS01');
SELECT *
FROM products
WHERE prod_price BETWEEN 3 AND 5;
AND、OR、NOT
sql
-- AND
SELECT prod_id, prod_name, prod_price
FROM products
WHERE vend_id = 'DLL01' AND prod_price <= 4;
-- OR
SELECT prod_id, prod_name, prod_price
FROM products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01';
-- NOT
SELECT *
FROM products
WHERE prod_price NOT BETWEEN 3 AND 5;
LIKE
- 只有字段是文本值时才使用 LIKE。
- LIKE 支持两个通配符匹配选项:% 和 _:
- % 表示任何字符出现任意次数。
- _ 表示任何字符出现一次。
sql
-- % 匹配任意长度的字符(包括0个字符)
SELECT prod_id, prod_name, prod_price
FROM products
WHERE prod_name LIKE '%bean bag%';
-- 说明:%bean bag% 表示匹配"prod_name中包含bean bag字符串"的记录,
-- 比如"red bean bag chair""bean bag sofa""small bean bag"都能被匹配到。
-- - 匹配单个任意字符(必须有且只有1个)
SELECT prod_id, prod_name, prod_price
FROM products
WHERE prod_name LIKE '__ inch teddy bear';
-- 说明:__ 表示匹配2个任意字符,整条语句表示匹配"prod_name为'XX inch teddy bear'"的记录,
-- 比如"12 inch teddy bear""20 inch teddy bear"能被匹配到,
-- 而"5 inch teddy bear"(只有1个字符)、"100 inch teddy bear"(3个字符)无法匹配
JOIN 连接
连接表时需要在每个表中选择一个字段,并对这些字段的值进行比较,值相同的两条记录将合并为一条。连接表的本质就是将不同表的记录合并起来,形成一张新表。当然,这张新表只是临时的,它仅存在于本次查询期间。
sql
select table1.column1, table2.column2...
from table1
join table2
on table1.common_column1 = table2.common_column2;
ON 和 WHERE 的区别:
- 连接表时,SQL 会根据连接条件生成一张新的临时表。ON 就是连接条件,它决定临时表的生成。
- WHERE 是在临时表生成以后,再对临时表中的数据进行过滤,生成最终的结果集,这个时候已经没有 JOIN-ON 了。
| 连接类型 | 说明 |
|---|---|
| INNER JOIN 内连接 | (默认连接方式)只有当两个表都存在满足条件的记录时才会返回行。 |
| LEFT JOIN 左连接 | 返回左表中的所有行,即使右表中没有满足条件的行也是如此。 |
| RIGHT JOIN 右连接 | 返回右表中的所有行,即使左表中没有满足条件的行也是如此。 |
| FULL JOIN 全连接 | 只要其中有一个表存在满足条件的记录,就返回行。 |
| SELF JOIN | 将一个表连接到自身,就像该表是两个表一样。 为了区分两个表,在 SQL 语句中需要至少重命名一个表。 |
如果两张表的关联字段名相同,也可以使用 USING 子句来代替 ON:
sql
-- join....on
select c.cust_name, o.order_num
from Customers c
inner join Orders o
on c.cust_id = o.cust_id
order by c.cust_name;
-- 如果两张表的关联字段名相同,也可以使用USING子句:join....using()
select c.cust_name, o.order_num
from Customers c
inner join Orders o
using(cust_id)
order by c.cust_name;
UNION 组合
UNION 用于将多个查询结果进行合并,生成一个结果集。
- UNION 会对结果进行去重,而 UNION ALL 不去重。
- 所有参与 UNION 的查询必须具有相同的列数、列顺序以及兼容的数据类型。
- 最终结果集的列名以第一个 SELECT 为准。
sql
SELECT id, name FROM table1
UNION ALL
SELECT id, name FROM table2;
Q:UNION 和 UNION ALL 有什么区别?
A:UNION 会去重,UNION ALL 不去重,性能更高。
Q:UNION 和 JOIN 的区别?
A:UNION 是纵向合并结果(增加行),而 JOIN 是横向连接表(增加列)。JOIN 中连接表的列可能不同,但在 UNION 中,所有查询的列数和列顺序必须相同。
函数
(1)字符串函数
LEFT() 左截取 RIGHT() 右截取
LOWER() 小写 UPPER() 大写
LTRIM() 去左空格 RTRIM() 去右空格
LENGTH() 长度(字节)
sql
SELECT UPPER(name) FROM user;
(2)日期时间函数
NOW() 当前时间 CURDATE() 当前日期 DATEDIFF() 日期差
YEAR() 年 MONTH() 月 DAY() 日
sql
SELECT NOW();
SELECT YEAR(create_time) FROM orders;
(3)数值函数
ABS() 绝对值 SQRT() 开方
RAND() 随机数 MOD() 取余
sql
SELECT ABS(-10);
(4)聚合函数
SUM() 求和 AVG() 平均
MAX() 最大 MIN() 最小
COUNT() 计数
sql
SELECT COUNT(*) FROM user;
DDL(定义数据库对象)
数据库(DATABASE)
创建数据库
sql
CREATE DATABASE test;
删除数据库
sql
DROP DATABASE test;
P.S. DROP 删除整个数据库(结构+数据),DELETE 只删除表中的数据。
选择数据库
sql
USE test;
数据表(TABLE)
创建数据表
sql
-- 普通创建
CREATE TABLE user (
id int(10) unsigned NOT NULL COMMENT 'Id',
username varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户名',
password varchar(64) NOT NULL DEFAULT 'default' COMMENT '密码',
email varchar(64) NOT NULL DEFAULT 'default' COMMENT '邮箱'
) COMMENT='用户表';
-- 根据已有的表创建新表
CREATE TABLE vip_user AS
SELECT * FROM user;
删除数据表
sql
DROP TABLE user;
修改数据表
ALTER TABLE user ADD/DROP/MODIFY COLUMN;
sql
-- 添加列
ALTER TABLE user
ADD age int(3);
-- 删除列
ALTER TABLE user
DROP COLUMN age;
-- 修改列
ALTER TABLE `user`
MODIFY COLUMN age tinyint;
-- 添加主键
ALTER TABLE user
ADD PRIMARY KEY (id);
-- 删除主键
ALTER TABLE user
DROP PRIMARY KEY;
视图(VIEW)
视图 = 一条 SQL 的"结果映射"(虚拟表)
创建视图
sql
CREATE VIEW top_10_user_view AS
SELECT id, username
FROM user
WHERE id < 10;
可以理解为给这条SQL起了个名字,之后可以直接用:
sql
SELECT * FROM top_10_user_view;
删除视图
sql
DROP VIEW top_10_user_view;
索引(INDEX)
索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。
在 MySQL 中,索引通常基于 B+ 树实现,其查找时间复杂度为 O(log n)。
索引的优点:
- 提高查询效率,减少扫描数据量
- 可以保证数据唯一性(唯一索引)
索引的缺点:
- 增加存储空间
- 降低写操作性能(增删改需要维护索引)
因此,索引是一种典型的"空间换时间"的优化手段。
创建索引
sql
CREATE INDEX user_index
ON user (id);
-- 创建唯一索引
CREATE UNIQUE INDEX user_index
ON user (id);
添加索引
sql
ALTER table user
ADD INDEX user_index(id);
删除索引
sql
ALTER TABLE user
DROP INDEX user_index;
约束
SQL 约束用于规定表中的数据规则,保证数据的正确性和完整性。
约束可以在创建表时规定(通过 CREATE TABLE 语句),或者在表创建之后规定(通过 ALTER TABLE 语句)。
常见约束包括:
- NOT NULL:字段不能为空。
- UNIQUE:字段值必须唯一。
- PRIMARY KEY:NOT NULL 和 UNIQUE 的结合。确保某列(或两个列多个列的结合)有唯一标识,有助于更容易更快速地找到表中的一个特定的记录。
- FOREIGN KEY:用于建立表之间的关联关系。
- DEFAULT:设置字段默认值。
- CHECK:保证列中的值符合指定的条件。
sql
CREATE TABLE Users (
Id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id',
Username VARCHAR(64) NOT NULL UNIQUE DEFAULT 'default' COMMENT '用户名',
Password VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '密码',
Email VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '邮箱地址',
Enabled TINYINT(4) DEFAULT NULL COMMENT '是否有效',
PRIMARY KEY (Id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
Q:主键和唯一约束有什么区别?
A:主键不能为 NULL 且一张表只能有一个,而 UNIQUE 可以有多个且允许 NULL。
Q:约束和索引有什么关系?
A:主键和唯一约束通常通过索引实现。
TCL(管理事务)
MySQL 默认是隐式提交(autocommit=1),每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
SELECT 不涉及数据修改,无法回滚。DDL(如 CREATE、DROP)会自动提交,无法回滚。
- START TRANSACTION - 指令用于标记事务的起始点。
- SAVEPOINT - 指令用于创建保留点。
- ROLLBACK TO - 指令用于回滚到指定的保留点;如果没有设置保留点,则回退到 START TRANSACTION 语句处。
- COMMIT - 提交事务。
sql
-- 开始事务
START TRANSACTION;
-- 插入操作 A
INSERT INTO `user`
VALUES (1, 'root1', 'root1', 'xxxx@163.com');
-- 创建保留点 updateA
SAVEPOINT updateA;
-- 插入操作 B
INSERT INTO `user`
VALUES (2, 'root2', 'root2', 'xxxx@163.com');
-- 回滚到保留点 updateA
ROLLBACK TO updateA;
-- 提交事务,只有操作 A 生效
COMMIT;
通过 set autocommit=0 可以取消自动提交,直到 set autocommit=1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。
Q:为什么 DDL 不能回滚?
A:因为 DDL 会自动提交事务,涉及数据库结构修改。
Q:事务是如何实现回滚的?
A:通过 Undo Log 记录旧数据,实现数据恢复。
DCL(控制访问权限)
权限控制
数据库权限控制用于限制用户对数据的访问和操作,主要通过 GRANT 和 REVOKE 实现。
常见权限包括 SELECT、INSERT、UPDATE、DELETE、CREATE、DROP 等。
GRANT(授权): GRANT 权限1, 权限2 ON 数据范围 TO 用户;
sql
GRANT SELECT, INSERT ON test.user TO 'root'@'%';
REVOKE(撤权):
sql
REVOKE INSERT ON test.user FROM 'root'@'%';
sql
-- 创建账户
CREATE USER myuser IDENTIFIED BY 'mypassword';
-- 修改账户名
UPDATE user SET user='newuser' WHERE user='myuser';
FLUSH PRIVILEGES;
--删除账户
DROP USER myuser;
-- 查看权限
SHOW GRANTS FOR myuser;
-- 授予权限
GRANT SELECT, INSERT ON *.* TO myuser;
-- 删除权限
REVOKE SELECT, INSERT ON *.* FROM myuser;
-- 更改密码
SET PASSWORD FOR myuser = 'mypass';
😺MySQL
**关系型数据库(RDB,Relational Database)**就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。关系型数据库中,我们的数据都被存放在了各种表中(比如用户表),表中的每一行就存放着一条数据(比如一个用户的信息)。
SQL是一种结构化查询语言(Structured Query Language),专门用来与数据库打交道,目的是提供一种从数据库中读写数据的简单有效的方法。
MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息。
MySQL 为什么能在数据库领域长期占据主流地位
- 生态和成本优势
MySQL 是开源免费的数据库,降低了使用门槛,同时拥有完善的生态系统和庞大的社区支持,几乎所有主流开发语言和框架都对其提供良好支持。 - 核心技术能力强
MySQL(InnoDB 引擎)支持完整的事务(ACID),默认隔离级别为可重复读,并通过 MVCC 和 Next-Key Lock 机制在保证一致性的同时提升并发性能。同时支持主从复制、读写分离和分库分表,具备良好的扩展能力。 - 易用性和运维成本低
相比 Oracle 等商业数据库,MySQL 安装简单、使用方便、学习成本低,维护工具和社区资源丰富,因此整体运维成本较低。
MySQL 字段类型

整数类型的 UNSIGNED 属性
MySQL中的UNSIGNED属性用于表示无符号整数,即该字段不允许存储负数,只能存储非负整数。
使用UNSIGNED后,可以将整数的取值范围扩大一倍,因为不再需要使用一部分位数来表示符号位。例如:
- TINYINT:-128 ~ 127
- TINYINT UNSIGNED:0 ~ 255
- INT:-2,147,483,648 ~ 2,147,483,647
- INT UNSIGNED:0 ~ 4,294,967,295
在实际开发中,UNSIGNED通常用于ID字段、计数类字段等不可能为负数的场景,从而扩大数据存储范围。
CHAR vs VARCHAR
CHAR 和 VARCHAR 都是 MySQL 中的字符串类型,主要区别在于存储方式不同。
- CHAR 是定长字符串,不管实际存储的字符串长度是多少,都会占用固定长度的空间,不足部分会用空格填充。
- VARCHAR 是变长字符串,根据实际内容长度进行存储,同时会额外使用 1 到 2 个字节记录字符串长度。
因此,CHAR 适合存储长度固定或接近固定的字段,如手机号、身份证号等;VARCHAR 适合存储长度变化较大的字段,如用户名、文章标题等。
Q:VARCHAR 为什么更常用?
A:因为它可以根据实际长度存储数据,更节省空间,适合互联网大多数不确定长度的场景。
Q:CHAR 一定比 VARCHAR 快吗?
A:不一定。CHAR读取稍快,但现代数据库优化后差距不明显,通常优先考虑空间效率。
VARCHAR(100) vs VARCHAR(10)
VARCHAR(100) 和 VARCHAR(10) 的区别在于它们定义的最大存储长度不同,分别表示最多可存储 100 个字符和 10 个字符。
在磁盘存储上,如果存储相同的字符串,两者实际占用的空间是相同的,因为 VARCHAR 是变长类型,存储空间只取决于实际内容长度以及长度标记。
但在内存使用上,VARCHAR(100) 通常会比 VARCHAR(10) 占用更多内存,因为在排序、临时表等操作中,数据库会按定义的最大长度分配内存空间。
因此,VARCHAR(100) 更具扩展性,而 VARCHAR(10) 更严格,但可能需要修改表结构。
Q:VARCHAR(100) 一定比 VARCHAR(10)更占空间吗?
A:磁盘存储不会,但在内存操作(排序、临时表)中可能占用更多空间。
Q:VARCHAR 为什么不直接按最大长度存储?
A:因为VARCHAR是变长类型,如果按最大长度存储会浪费大量空间。
DECIMAL vs FLOAT/DOUBLE
DECIMAL 和 FLOAT/DOUBLE 的主要区别在于精度和存储方式。
DECIMAL 是定点数,用于精确存储小数,能够保证数值不会发生精度丢失,常用于金融和货币相关场景。
FLOAT 和 DOUBLE 是浮点数,采用二进制近似表示方式,可能存在精度误差,但计算速度较快,占用空间较小。
因此,DECIMAL 适用于对精度要求高的场景,如金额计算,而 FLOAT/DOUBLE 适用于科学计算等允许一定误差的场景。
在 Java 中,DECIMAL 对应 BigDecimal 类型。
Q:为什么 FLOAT 会有精度问题?
A:因为FLOAT采用二进制浮点表示,很多十进制小数无法精确转换为二进制,从而产生误差。
Q:为什么金融系统必须用 DECIMAL?
A:因为金融系统要求绝对精确,不能出现精度误差,否则会导致金额错误。
DATETIME vs TIMESTAMP
DATETIME 和 TIMESTAMP 都用于存储日期时间类型数据,但两者存在本质区别。
DATETIME 不受时区影响,存储的是什么就返回什么,占用 8 个字节,取值范围为 1000 年到 9999 年。
TIMESTAMP 与时区相关,存储的是 UTC 时间,会根据当前时区自动转换,占用 4 个字节,但时间范围较小,为 1970 年到 2038 年。
因此,如果应用需要处理多时区或自动时间转换,适合使用 TIMESTAMP;如果需要存储固定时间、需要表示 2038 年之后的时间或时间范围较大的场景,则更适合使用 DATETIME。
Q:什么是 2038 年问题?
A:TIMESTAMP使用32位时间戳存储,到2038年会溢出,导致时间错误。
Q:TIMESTAMP 为什么能自动转换时区?
A:因为它存储的是UTC时间,在读取时根据当前数据库时区进行转换。
Q:DATETIME 一定比 TIMESTAMP 更好吗?
A:不是,DATETIME不处理时区,TIMESTAMP更适合多时区系统。
NULL vs ''
NULL 表示数据缺失或未知,不等于任何值,包括自身,因此任何与 NULL 的比较结果都是 NULL,必须使用 IS NULL 或 IS NOT NULL 进行判断。
'' 表示一个已知的空字符串,是一个有效的字符串值,可以正常参与比较和运算。
在聚合函数中,大多数函数会忽略 NULL 值,而 '' 会被正常计算。
因此,NULL 表示"没有值",而 '' 表示"有值但为空"。
Q:为什么 MySQL 不推荐用 NULL?
A:因为NULL会增加查询复杂度、影响索引优化,并且逻辑判断需要特殊处理。
Boolean 类型如何表示?
MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应 false 或 true。
手机号存储用 INT 还是 VARCHAR
必须用 VARCHAR,而不能用 INT / BIGINT。
**主要原因是手机号本质是字符串标识符,而不是用于数学计算的数值。**使用 INT 会导致前导零丢失、无法存储国家代码(如 +86),并且无法表示格式化信息。
❌ 核心问题1:丢失格式信息,比如:前导 0 丢失,+86 无法存储,-、空格等格式无法表达。
❌ 核心问题2:不能表达真实业务需求,手机号不是用来计算的,而是唯一标识 + 字符串匹配。
❌ 核心问题3:国际化问题,国外手机号:+86 138-0013-8000、+1 202 555 0188。
此外,实际业务中手机号常用于加密存储或脱敏处理,加密后的数据为字符串形式,INT 类型无法存储。
因此,使用 VARCHAR 可以完整保留手机号格式,支持国际化,并且更符合业务语义,是更合理的设计方式。
MySQL 基础架构
一条 SQL 从客户端发出,到最终拿到结果,中间经历了哪些组件处理?
- 连接器,负责用户身份认证和权限验证;
- 分析器,对SQL语句进行词法和语法分析,判断语句是否合法;
- 优化器,根据成本选择最优执行方案,例如是否使用索引;
- 执行器,负责调用存储引擎执行SQL并检查权限;
- 存储引擎(如InnoDB)负责具体的数据存储和读取。
整体流程是:客户端发送SQL → 连接器 → 分析器 → 优化器 → 执行器 → 存储引擎 → 返回结果。
MySQL 存储引擎
MySQL 采用插件式存储引擎架构,支持多种存储引擎,可以通过 SHOW ENGINES 命令查看。
常见的存储引擎包括 InnoDB、MyISAM、Memory 等。其中 InnoDB 是 MySQL 5.5.5 之后的默认存储引擎。
InnoDB 支持事务、行级锁、外键约束以及崩溃恢复机制,适用于绝大多数业务场景。
MyISAM 的性能还行,各种特性也还不错(比如全文索引、压缩、空间函数等)。但是,MyISAM 不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。
因此,在实际开发中,通常默认选择 InnoDB 作为存储引擎。
MyISAM vs InnoDB
- 事务支持:MyISAM 不支持事务,而 InnoDB 支持事务,符合 ACID 特性。
- 锁机制:MyISAM 只支持表级锁,而 InnoDB 支持行级锁,提高并发性能。
- 崩溃恢复:MyISAM 不支持崩溃恢复,而 InnoDB 通过 Redo Log 支持数据恢复。
- MVCC:MyISAM 不支持,InnoDB 支持,用于提高并发性能。
- 外键:MyISAM 不支持,InnoDB 支持。
- 索引结构:两者都使用 B+树,但 InnoDB 是聚簇索引,数据和索引存储在一起,而 MyISAM 索引和数据分离。
- 缓存机制:InnoDB 使用 Buffer Pool 缓存数据和索引,而 MyISAM 只缓存索引。
MySQL 索引
索引是数据库中用于加速查询的一种数据结构,本质上是一种有序结构。在 MySQL 中,常用的索引结构是 B+树。
索引的主要作用是减少查询时需要扫描的数据量,从而提高查询性能,同时还可以用于保证数据唯一性以及加速排序和分组操作。但缺点是会增加存储空间占用,并且在数据进行增删改时需要维护索引,从而降低写操作性能。
此外,索引并不一定总是提高性能,当数据量较小、查询结果占比过大或者索引失效时,可能会导致性能下降。
Q:为什么用 B+树不用红黑树?
A:因为B+树高度更低,减少磁盘IO,并且支持范围查询。
索引为什么快?
**索引快的本质是通过 B+树结构减少磁盘 I/O 次数,并利用顺序访问提高效率。**在 MySQL 中,索引底层采用 B+树结构。B+树是一种多路平衡树,具有高度低的特点,对于千万级数据,其树高通常只有 3 到 4 层,因此查询时最多只需要 3 到 4 次磁盘 I/O 就可以定位到数据。且B+树的叶子节点之间通过链表连接,支持范围查询和顺序访问,有利于磁盘预读,提高查询效率。因此,索引通过减少 I/O 次数并优化访问方式,使查询性能大幅提升。
Q:B树和 B+树有什么区别?
A:B+树所有数据在叶子节点,并且叶子节点有链表,支持范围查询。
Q:为什么 B+树适合数据库?
A:因为它减少树高度、支持范围查询,并且适合磁盘顺序读取。
为什么 InnoDB 没有使用哈希作为索引的数据结构?
InnoDB 没有使用 Hash 作为索引结构,是因为 Hash 索引虽然在等值查询时性能很高,但不支持范围查询、排序以及联合索引的部分匹配,同时还存在哈希冲突问题,因此不适合作为通用数据库索引结构。
为什么 InnoDB 没有使用 B 树作为索引的数据结构?
相比 B树,B+树在数据库场景下更优。B+树的非叶子节点只存储索引键,使得单个节点可以存储更多键值,从而降低树的高度,减少磁盘 I/O 次数。同时,B+树的叶子节点通过链表连接,天然支持范围查询和顺序访问,查询性能更加稳定。
InnoDB 选择 B+树,是因为它在减少 I/O、支持范围查询和顺序访问方面表现最佳,最适合数据库场景。
Q:B+树为什么比 B树矮?
A:因为非叶子节点只存key,可以存更多索引项。
覆盖索引
覆盖索引是指查询所需要的所有字段都包含在索引中,因此可以直接通过索引获取数据,而无需回表查询。
在 InnoDB 中,二级索引的叶子节点只存储主键值,如果查询的字段不在索引中,就需要根据主键再查询一次主键索引,这个过程称为回表。
而覆盖索引由于索引中已经包含了所有需要查询的字段,因此可以避免回表,从而减少磁盘 I/O,提高查询性能。
联合索引
联合索引是指在多个字段上创建的索引,其底层仍然是 B+树结构,数据按照索引列从左到右依次排序。
最左前缀原则是指在使用联合索引时,查询条件必须从最左列开始匹配,MySQL 才能利用索引进行查询。索引会从左到右依次匹配,直到遇到范围查询或缺失中间列为止。
例如对于索引 (a, b, c),可以支持 (a)、(a,b)、(a,b,c) 这样的查询,但无法支持 (b)、(c) 或跳过中间列的查询。
再来看一个常见的面试题:如果有索引 联合索引(a,b,c),查询 a=1 AND c=1 会走索引么?c=1 呢?b=1 AND c=1 呢? b = 1 AND a = 1 AND c = 1 呢?
- 查询 a=1 AND c=1:根据最左前缀匹配原则,查询可以使用索引的前缀部分。因此,该查询仅在 a=1 上使用索引,然后对结果进行 c=1 的过滤。
- 查询 c=1:由于查询中不包含最左列 a,根据最左前缀匹配原则,整个索引都无法被使用。
- 查询 b=1 AND c=1:和第二种一样的情况,整个索引都不会使用。
- 查询 b=1 AND a=1 AND c=1:这个查询是可以用到索引的。查询优化器分析 SQL 语句时,对于联合索引,会对查询条件进行重排序,以便用到索引。会将 b=1 和 a=1 的条件进行重排序,变成 a=1 AND b=1 AND c=1。
哪些字段适合创建索引?
- 被频繁查询的字段,特别是出现在 WHERE 条件中的字段,可以有效减少扫描数据量。
- 用于表连接(JOIN)的字段,这类字段建立索引可以显著提升多表查询性能。
- 用于排序(ORDER BY)和分组(GROUP BY)的字段,索引可以利用其有序性避免额外排序。
- 区分度高的字段更适合建立索引,因为其过滤效果更好。
- 索引字段应尽量避免为 NULL,因为 NULL 会增加优化难度。
索引失效的原因有哪些?
索引失效:本来应该走索引,但 MySQL 优化器最终选择了全表扫描。
- 不满足最左前缀原则,例如联合索引跳过最左列;
- 对索引列进行函数、计算或表达式操作;
- LIKE 查询以 % 开头,无法利用前缀匹配;
- 使用 OR 条件且部分字段没有索引;
- IN 或 NOT IN 取值过大,优化器选择全表扫描;
- 发生隐式类型转换导致索引无法使用。
MySQL 日志
MySQL 中的日志是数据库非常重要的组成部分,用于记录数据库运行过程中的各种行为,例如数据变更、错误信息、事务操作以及主从复制等。通过日志可以实现数据恢复、主从同步以及问题排查,是保证数据库可靠性和可运维性的核心机制。
MySQL 常见日志主要包括 redo log、undo log、binlog、slow query log 和 error log 等。
redo log(重做日志)
redo log 是 InnoDB 存储引擎特有的日志,用于保证事务的持久性 (ACID 中的 D)。它记录的是数据页的物理修改操作,而不是 SQL 语句本身。当事务提交时,先写 redo log,再异步刷盘数据页,即使数据库宕机,也可以通过 redo log 恢复未落盘的数据。
redo log 采用循环写入的方式,由固定大小的日志文件组成,具有高性能写入特性。
undo log(回滚日志)
undo log 用于保证事务的原子性 (ACID 中的 A),主要记录数据修改前的旧值。当事务需要回滚时,可以通过 undo log 恢复数据到修改之前的状态 。同时 undo log 也用于 MVCC(多版本并发控制),用于实现一致性读。
undo log 是逻辑日志,记录的是"如何撤销操作"。
binlog(二进制日志)
binlog 是 MySQL Server 层的日志,与存储引擎无关,记录所有对数据库产生更改的 SQL 语句或行级变更 。binlog 主要用于主从复制和数据恢复 。
binlog 有三种格式:statement(记录SQL)、row(记录行变化)、mixed(混合模式)。在主从复制中,从库通过 binlog 来同步主库数据。
redo log vs binlog
redo log 是 InnoDB 存储引擎层日志,用于崩溃恢复,属于物理日志,循环写入。
binlog 是 MySQL 服务层日志,用于主从复制和数据恢复,属于逻辑日志,追加写入。
两阶段提交机制保证 redo log 和 binlog 的一致性,防止数据不一致问题。
slow query log(慢查询日志)
slow query log 用于记录执行时间超过阈值的 SQL 语句 ,用于定位性能问题。通过该日志可以分析慢 SQL,从而进行索引优化或 SQL 重写。
slow query log 主要用于性能调优,是数据库优化的重要工具。
error log(错误日志)
error log 记录 MySQL 启动、运行和关闭过程中出现的错误信息,也包括崩溃信息和严重警告。DBA 通过 error log 可以快速定位数据库异常问题。
general log(通用日志)
general log 记录 MySQL 所有执行过的 SQL 语句,包括连接、查询、修改等操作。由于性能开销较大,通常只在调试环境开启。
日志用于保证事务的持久性与一致性,实现崩溃恢复,支持主从复制,同时帮助定位性能问题和系统异常。redo log 和 undo log 主要用于事务机制,binlog 主要用于数据复制和恢复,slow log 和 error log 主要用于运维和问题排查。
| 日志 | 作用 |
|---|---|
| redo log | 崩溃恢复 |
| undo log | 回滚 + MVCC |
| binlog | 主从复制 |
| slow log | 性能优化 |
MySQL 事务
sql
-- 开启一个事务
START TRANSACTION;
-- 多条 SQL 语句
SQL1,SQL2...
-- 提交事务
COMMIT;
ACID 是数据库事务的四大特性:
A:Atomicity 原子性:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
C:Consistency 一致性:执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
I:Isolation 隔离性:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
D:Durability 持久性:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
并发事务问题
第一是脏读,即一个事务读取到了另一个事务未提交的数据。

第二是丢失修改,即多个事务同时修改同一数据,导致其中一个事务的修改结果被覆盖。

第三是不可重复读,即在一个事务内多次读取同一数据,结果不一致,通常是由于其他事务修改并提交导致。

第四是幻读,即在一个事务内多次查询满足条件的数据,结果集发生变化,通常是由于其他事务插入或删除数据导致。

不可重复读和幻读有什么区别?
不可重复读的重点是内容修改或者记录减少比如多次读取一条记录 发现其中某些记录的值被修改;幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
幻读其实可以看作是不可重复读的一种特殊情况,单独把幻读区分出来的原因主要是解决幻读和不可重复读的方案不一样。
举个例子:执行 delete 和 update 操作的时候,可以直接对记录加锁,保证事务安全。而执行 insert 操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行 insert 操作的时候需要依赖 Next-Key Lock(Record Lock+Gap Lock) 进行加锁来保证不出现幻读。
并发事务的控制方式
数据库如何在"多个事务同时执行"时,既保证性能,又保证数据正确?
MySQL 的答案只有两个:锁(Lock) + MVCC(多版本并发控制)
- 锁 → 阻塞别人(悲观)
- MVCC → 不阻塞,用版本解决(乐观)
-
共享锁(S锁 / 读锁):多个事务可以同时读、不能写。
-
排他锁(X锁 / 写锁):只能一个事务持有、其他事务不能读也不能写。
根据根据锁粒度的不同,又被分为:
- 表级锁:锁整张表、并发低、开销小。
- 行级锁(InnoDB默认):锁一行数据、并发高、开销大。
锁的局限性:会阻塞、会死锁、并发能力有限。====MVCC 来优化读操作
MVCC(多版本并发控制)
一份数据存多个版本,让不同事务看到不同版本,读不加锁,直接读历史版本。
MVCC = undo log(保存历史版本) + 隐藏字段(事务id 回滚指针) + Read View(读视图)
sql
-- EG
SELECT * FROM user; -- MVCC
UPDATE user SET age=20; -- 加锁
在实际中,MySQL 通常结合使用锁和 MVCC 来实现高并发和数据一致性的平衡。
MySQL InnoDB 存储引擎的默认隔离级别是 REPEATABLE READ。
REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的(除非数据是被本身事务自己所修改),可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。
MySQL 性能优化
答题框架:点(慢SQL) → 线(索引/SQL/表结构) → 面(架构)
第一步:定位问题
通过慢查询日志和 EXPLAIN 等工具定位性能瓶颈,找出执行效率低的 SQL。
EXPLAIN 本质:模拟 MySQL 怎么执行这条 SQL(执行计划)
sql
EXPLAIN SELECT * FROM user WHERE id = 1;
重点看 4 个字段:
- type访问类型:const主键查询>ref普通索引匹配多行>range>index>ALL全表扫描。
- key用哪个索引:有名字代表走索引了,NULL没走索引。
- rows扫描行数:rows 越小越好。
- extra额外信息:Using index覆盖索引(不回表)>Using where正常(过滤)>Using temporary用临时表,慢>Using filesort文件排序,巨慢
EXPLAIN 核心看:有没有走索引(key)、扫多少数据(rows)、查的方式(type)、有没有额外开销(extra)
第二步:SQL + 索引优化
从索引优化、SQL 优化和表结构设计入手,例如为高频查询字段建立索引、避免使用 SELECT *、合理使用联合索引等。
- 索引优化
(1)WHERE 建索引
sql
SELECT * FROM user WHERE age = 20;
-- 加索引
CREATE INDEX idx_age ON user(age);
(2)JOIN 字段建索引
sql
SELECT *
FROM orders o
JOIN user u ON o.user_id = u.id;
-- 加索引
-- user.id 有索引(主键)
-- orders.user_id 加索引
(3)高区分度字段优先
gender = '男/女',只有 2 种值 → 区分度低 ❌
user_id、email,唯一 → 区分度高 ✅
(4)联合索引(最左前缀)
sql
CREATE INDEX idx_name_age ON user(name, age);
-- 能走索引
WHERE name = '张三'
WHERE name = '张三' AND age = 20
-- 不能走索引
WHERE age = 20
(5)覆盖索引
sql
SELECT * FROM user WHERE age = 20;
-- 流程:查索引 → 查主键 → 回表查数据
流程:查索引 → 查主键 → 回表查数据
- 先在 idx_age 索引树 找到所有 age=20 的记录。
普通二级索引(比如 idx_age)只存两样东西:索引字段:age,主键:id
它不存 name、phone、create_time 这些其他字段! - 但你写的是 SELECT *,索引里没有 name、phone 等数据。
- 必须拿着 id 去聚簇索引(主键索引) 再查一遍(回表!!)。
sql
CREATE INDEX idx_age_name ON user(age, name);
SELECT age,name FROM user WHERE age = 20;
-- 索引覆盖
直接从索引拿数据 ✅,不回表 → 更快
- SQL优化
(1)不用 SELECT *:查多余字段,无法走覆盖索引。
(2)子查询执行多次,改为JOIN
sql
-- ❌ 子查询,子查询执行多次
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM user WHERE age > 20);
-- ✅ JOIN,一次执行
SELECT o.*
FROM orders o
JOIN user u ON o.user_id = u.id
WHERE u.age > 20;
(3)深分页问题
sql
-- ❌ 先查10万行 → 再丢掉 → 再取10条
SELECT * FROM user LIMIT 100000,10;
-- ✅ 优化:基于主键连续分页
SELECT * FROM user WHERE id > 100000 LIMIT 10;
- 表结构优化
(1)字段类型:age VARCHAR(10) ❌浪费空间,age TINYINT。
(2)避免 NULL:age INT NULL ❌索引复杂,age INT DEFAULT 0。
(3)适当反范式:比如订单表直接存用户名,不用每次 JOIN 用户表,空间换时间。
第三步:架构优化
当单机性能无法满足需求时,可以采用架构优化手段,如读写分离、分库分表以及使用缓存中间件(如 Redis)来减轻数据库压力。
首先,优先使用缓存中间件(如 Redis)缓存热点数据,减少数据库访问压力,这是性价比最高的优化手段。
其次,可以采用读写分离架构,将写操作放在主库,读操作分发到多个从库,从而提升系统的并发处理能力。
当数据量进一步增大时,可以进行分库分表,将数据拆分到多个数据库或数据表中,降低单表数据量,提高查询效率。但需要注意其带来的复杂性问题。
此外,还可以通过数据冷热分离和异步化处理进一步优化系统性能。
(1)缓存(最优先,性价比最高)
- 思想:能不查数据库,就不查数据库,
- 常用:Redis
- 问题:缓存一致性
- 解决:先更新 DB,再删缓存(常用)、延迟双删。
(2)读写分离(提升并发能力)
- 思想:从库读,主库写。
- 问题:主从延迟问题(写完 → 从库还没同步 → 查不到 )。
- 解决:强一致读(查主库)、延迟读。
(3)分库分表
思想:把一个大表 → 拆成多个小表
(4)数据冷热分离
热数据(常访问) → 高性能
冷数据(历史) → 低成本
eg:最近3个月订单 → 主库,历史订单 → 冷库。
(5)异步化
同步 → 改异步
eg:下单 → 发消息(MQ)→ 慢慢处理(避免瞬时压力打爆数据库)
😺缓存
缓存本质是通过空间换时间,把热点数据放到更快的存储中,从而减少慢操作,提高系统性能。
在实际系统中,缓存通常将数据存储在内存中,例如使用 Redis 等缓存中间件,从而避免频繁访问数据库,提高系统响应速度和吞吐量。
缓存不仅应用于业务系统中,在计算机体系结构中也广泛存在,例如 CPU Cache、操作系统中的 TLB、浏览器缓存和 CDN 等。
因此,缓存的本质是通过提高数据访问速度来提升系统整体性能。
Q:缓存和数据库有什么区别?
A:缓存通常存储在内存中,访问速度更快,但数据可能不持久;数据库存储在磁盘中,数据更安全但访问较慢。
Q:缓存会带来什么问题?
A:主要问题是数据一致性、缓存失效和内存占用。
Q:为什么 Redis 比 MySQL 快?
A:因为 Redis 基于内存操作,避免了磁盘 I/O。
本地缓存
本地缓存(Local Cache)是存储在应用进程内部的一种缓存机制。
核心特点:和应用在同一个进程、不走网络、直接内存访问。
常见的单体架构图如下,我们使用 Nginx 来做负载均衡,部署两个相同的应用到服务器,两个服务使用同一个数据库,并且使用的是本地缓存。

注意: 在集群模式下使用本地缓存,必须考虑负载均衡策略 。如果 Nginx 使用默认的轮询(Round-Robin),同一个用户的请求会随机落在不同机器,导致本地缓存命中率极低。
解决方案如下:
- 网关层:使用一致性哈希或 Sticky Session,保证同一用户的请求固定打到同一台机器。
- 应用层 :仅将本地缓存用于**"全局几乎不变"**的数据(如配置字典),而非用户维度数据。
本地缓存的方案
缓存必备三件套:① 过期时间(TTL)② 淘汰策略 ③ 统计能力
缓存不只是存数据,还要"管理生命周期"。
第一类是基于 JDK 的原生实现,例如 HashMap 和 ConcurrentHashMap。
只提供了缓存的功能,无过期、无淘汰、无统计,一个稍微完善一点的缓存框架至少要提供:过期时间 、淘汰机制 、命中率统计这三点。
第二类是传统本地缓存框架,例如 Ehcache、Guava Cache 和 Spring Cache。
Ehcache 功能较为全面,支持磁盘持久化和二级缓存;Guava Cache 提供了较为完善的缓存功能,使用方便;Spring Cache 是一个缓存抽象层,可以通过注解方式简化使用。
第三类是高性能缓存框架,例如 Caffeine。
Caffeine 在性能和淘汰策略上都优于 Guava,支持多种过期策略、容量控制和统计功能,是当前推荐的本地缓存方案。
本地缓存的缺陷
本地缓存虽然具有访问速度快、实现简单和成本低的优点,但也存在明显的缺陷。
首先,本地缓存无法在多个服务实例之间共享数据,在分布式架构中容易导致数据不一致的问题。其次,在负载均衡场景下,请求可能被分发到不同服务器,从而导致缓存命中率下降。
此外,本地缓存的容量受限于应用所在机器的内存资源,如果应用本身占用内存较大,则可用于缓存的空间有限。
因此,本地缓存更适用于单体应用或小规模系统,在分布式场景中通常需要结合分布式缓存来解决这些问题。
Q:为什么本地缓存会不一致?
A:因为每个服务实例都有独立缓存,更新无法同步。
Q:如何解决本地缓存问题?
A:使用分布式缓存(如 Redis)或多级缓存方案。
Q:本地缓存还能用吗?
A:可以,用于缓存全局不变数据或作为一级缓存提升性能。
Q:多级缓存架构中本地缓存(一级缓存)怎么设计?
A:在多级缓存架构中,本地缓存(一级缓存)通常设计为每台服务器各自维护一份相同的数据副本,而不是通过一致性哈希或 Sticky Session 进行用户绑定。这是因为一级缓存主要用于缓存热点数据或公共数据,这类数据对所有请求是共享的,因此每台机器维护一份副本可以提高命中率,同时避免复杂的路由策略。
对于用户维度的数据,一般不建议依赖本地缓存,而是直接使用 Redis 等分布式缓存来保证数据共享性和一致性。因此,主流设计是本地缓存用于提升访问速度,Redis 用于解决数据共享问题。
分布式缓存
分布式缓存(Distributed Cache)是独立部署的缓存服务,多个应用实例共享同一份缓存数据。
缓存从"应用内部"变成"独立系统"。
典型结构:用户 → Nginx(负载均衡)→ 应用集群 → Redis(分布式缓存)→ 数据库(持久化)

与本地缓存不同,分布式缓存不依赖于应用进程,而是作为独立的中间件服务存在,例如 Redis。
使用分布式缓存后,可以解决本地缓存无法共享的问题,同时支持更大的数据容量和更高的并发访问能力。
但与此同时,引入分布式缓存也会带来一系列新的挑战:
- 系统架构复杂度明显提升,需要额外考虑缓存与数据库的一致性、缓存穿透、缓存击穿、缓存雪崩等问题;
- 数据访问多了一层网络交互,会产生一定的网络开销与延迟;
- 系统开发与运维成本上升,引入分布式缓存意味着需要独立部署、维护一套缓存集群,而缓存依赖高性能内存资源,本身硬件成本较高;再加上开发、调试、监控、扩容等全生命周期的投入,整体成本会明显增加。
分布式缓存的方案
目前主流方案是 Redis,它不仅支持丰富的数据结构,还支持持久化、高可用和集群模式,生态完善,是分布式缓存的首选。
① 数据结构丰富:String、List、Set、ZSet、Hash,支持排行榜、消息队列、计数器。
② 支持持久化:RDB、AOF。
③ 支持高可用:主从复制、哨兵模式、Cluster集群。
④ 生态极强:Java / Spring 完美支持。
多级缓存
在系统中引入多层缓存,一般是:本地缓存(L1) + 分布式缓存(L2)
本地缓存和分布式缓存虽然都属于缓存,但本地缓存的访问速度要远快于分布式缓存,这是因为访问本地缓存不存在额外的网络开销。
L1解决极致速度,L2解决数据共享,数据库解决持久化。

多级缓存一致性如何保证?
多级缓存不追求强一致,只保证最终一致。
在多级缓存系统中,保证强一致性成本太高,业界的几个提供多级缓存功能的缓存框架基本都是最终一致性保证。可以使用 Redis 的发布/订阅机制、Redis Stream 或者消息队列来确保当一个实例的本地缓存发生变化时,其他实例能够及时更新其本地缓存,以保持缓存一致性。
- DB 修改数据:首先在数据库中进行数据修改。
- 通过监听 Canal 消息,触发缓存的更新:使用 Canal 监听数据库的变更操作,当检测到数据变化时,触发缓存更新。
- 同步 Redis 缓存:对于 Redis 缓存,因为集群中只共享一份数据,所以直接同步缓存即可。
- 同步本地缓存:由于本地缓存分布在不同的 JVM 实例中,需要借助广播消息队列(MQ)机制,将更新通知广播到各个业务实例,从而同步本地缓存。
😺Redis
Redis 为什么这么快?
纯内存操作 (Memory-Based Storage) :这是最主要的原因。Redis 数据读写操作都发生在内存中,访问速度是纳秒级别,而传统数据库频繁读写磁盘的速度是毫秒级别,两者相差数个数量级。
高效的 I/O 模型 (I/O Multiplexing & Single-Threaded Event Loop) :Redis 使用单线程事件循环配合 I/O 多路复用技术,让单个线程可以同时处理多个网络连接上的 I/O 事件(如读写),避免了多线程模型中的上下文切换和锁竞争问题。虽然是单线程,但结合内存操作的高效性和 I/O 多路复用,使得 Redis 能轻松处理大量并发请求(Redis 线程模型会在后文中详细介绍到)。
优化的内部数据结构 (Optimized Data Structures) :Redis 提供多种数据类型(如 String, List, Hash, Set, Sorted Set 等),其内部实现采用高度优化的编码方式(如 ziplist, quicklist, skiplist, hashtable 等)。Redis 会根据数据大小和类型动态选择最合适的内部编码,以在性能和空间效率之间取得最佳平衡。
简洁高效的通信协议 (Simple Protocol - RESP) :Redis 使用的是自己设计的 RESP (REdis Serialization Protocol) 协议。这个协议实现简单、解析性能好,并且是二进制安全的。客户端和服务端之间通信的序列化/反序列化开销很小,有助于提升整体的交互速度。
Q:那既然都这么快了,为什么不直接用 Redis 当主数据库呢?
A:主要是因为内存成本太高,并且 Redis 提供的数据持久化仍然有数据丢失的风险。
为什么要用 Redis?
1、访问速度更快
传统数据库数据保存在磁盘,而 Redis 基于内存,内存的访问速度比磁盘快很多。引入 Redis 之后,我们可以把一些高频访问的数据放到 Redis 中,这样下次就可以直接从内存中读取,速度可以提升几十倍甚至上百倍。
2、高并发
一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g),但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。
3、功能全面
Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大!
为什么用 Redis 而不用本地缓存呢?
|--------|--------------------|------------------|
| | 本地缓存 | Redis |
| 数据一致性 | 多服务器部署时存在数据不一致问题 | 数据一致 |
| 内存限制 | 受限于单台服务器内存 | 独立部署,内存空间更大 |
| 数据丢失风险 | 服务器宕机数据丢失 | 可持久化,数据不易丢失 |
| 管理维护 | 分散,管理不便 | 集中管理,提供丰富的管理工具 |
| 功能丰富性 | 功能有限,通常只提供简单的键值对存储 | 功能丰富,支持多种数据结构和功能 |
Redis 应用
- 分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。
- 限流 :一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的
RRateLimiter来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。 - 消息队列:Redis 5.0 中增加的 Stream 类型的数据结构适合用来做消息队列。
- 延时队列:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
- 分布式 Session:利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
- 复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景,比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜、通过 HyperLogLog 统计网站 UV 和 PV。
Redis 数据类型
5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。
String vs Hush
- 对象存储方式:String 存储的是序列化后的对象数据,存放的是整个对象,操作简单直接。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
- 内存消耗:Hash 通常比 String 更节省内存,特别是在字段较多且字段长度较短时。Redis 对小型 Hash 进行优化(如使用 ziplist 存储),进一步降低内存占用。
- 复杂对象存储:String 在处理多层嵌套或复杂结构的对象时更方便,因为无需处理每个字段的独立存储和操作。
- 性能:String 的操作通常具有 O(1) 的时间复杂度,因为它存储的是整个对象,操作简单直接,整体读写的性能较好。Hash 由于需要处理多个字段的增删改查操作,在字段较多且经常变动的情况下,可能会带来额外的性能开销。
总结:
- 在绝大多数情况下,String 更适合存储对象数据,尤其是当对象结构简单且整体读写是主要操作时。
- 如果你需要频繁操作对象的部分字段或节省内存,Hash 可能是更好的选择。
购物车信息用 String 还是 Hash 存储更好呢?
由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储:
用户 id 为 key
商品 id 为 field,商品数量为 value

使用 Redis 实现一个排行榜怎么做?
Redis 中有一个叫做 Sorted Set(有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
相关的一些 Redis 命令:ZRANGE(从小到大排序)、ZREVRANGE(从大到小排序)、ZREVRANK(指定元素排名)。
Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+ 树?
ZSet(有序集合)底层:skiplist(跳表)+ hashtable。
用跳表 保证有序 + 范围查找。用哈希表保证 O (1) 查 member 分值。
跳表(Skip List)本质:多层索引的有序链表。普通链表(慢) + 多级索引(加速)。
平衡树 vs 跳表:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 O(log n)。对于范围查询来说,平衡树也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表诞生的初衷就是为了克服平衡树的一些缺点。跳表使用概率平衡(随机决定节点升几层)而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多。跳表的平衡是指:多层节点分布均匀,查找效率不会退化。
红黑树 vs 跳表:相比较于红黑树来说,跳表的实现也更简单一些,不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
B+ 树 vs 跳表:B+ 树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+ 树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+ 树那样插入时发现失衡时还需要对节点分裂与合并。
Set 的应用场景是什么?
- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 HyperLogLog 更适合一些)、文章点赞、动态点赞等等。
- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)等等。
- 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。
使用 Set 实现抽奖系统怎么做?
- SADD key member1 member2 ...:向指定集合添加一个或多个元素。
- SPOP key count:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。
- SRANDMEMBER key count:随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
使用 Bitmap 统计活跃用户怎么做?
Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap,只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。如果想要使用 Bitmap 统计活跃用户的话,可以使用日期(精确到天)作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1。
bash
// 初始化数据
SETBIT 20210308 1 1
(integer) 0
SETBIT 20210308 2 1
(integer) 0
SETBIT 20210309 1 1
(integer) 0
// 统计 20210308~20210309 总活跃 用户数
BITOP and desk1 20210308 20210309
(integer) 1
BITCOUNT desk1
(integer) 1
// 统计 20210308~20210309 在线活跃 总用户数
BITOP or desk2 20210308 20210309
(integer) 1
BITCOUNT desk2
(integer) 2
20210308:3 月 8 号活跃用户的位图,20210309:3 月 9 号活跃用户的位图。
BITOP AND:把两个位图按位与,结果存到 desk1。
BITOP OR:把两个位图按位或,结果存到 desk2。
BITCOUNT:计数。
HyperLogLog 适合什么场景?
HyperLogLog (HLL) 是一种非常巧妙的概率性数据结构,它专门解决一类非常棘手的大数据问题:在海量数据中,用极小的内存,估算一个集合中不重复元素的数量,也就是我们常说的基数(Cardinality)。
HLL 做的最核心的权衡,就是用一点点精确度的损失,来换取巨大的内存空间节省。它给出的不是一个 100%精确的数字,而是一个带有很小标准误差(Redis 中默认是 0.81%)的近似值。
适合以下特征的场景:
- 数据量巨大,内存敏感: 这是 HLL 的主战场。比如,要统计一个亿级日活 App 的每日独立访客数。如果用传统的 Set 来存储用户 ID,一个 ID 占几十个字节,上亿个 ID 可能需要几个 GB 甚至几十 GB 的内存,这在很多场景下是不可接受的。而 HLL,在 Redis 中只需要固定的 12KB 内存,就能处理天文数字级别的基数,这是一个颠覆性的优势。
- 对结果的精确度要求不是 100%: 这是使用 HLL 的前提。比如,产品经理想知道一个热门帖子的 UV(独立访客数)是大约 1000 万还是 1010 万,这个细微的差别通常不影响商业决策。但如果场景是统计一个交易系统的准确交易笔数,那 HLL 就完全不适用,因为金融场景要求 100%的精确。
HyperLogLog 具体的应用场景:
- 网站/App 的 UV(Unique Visitor)统计: 比如统计首页每天有多少个不同的 IP 或用户 ID 访问过。
- 搜索引擎关键词统计: 统计每天有多少个不同的用户搜索了某个关键词。
- 社交网络互动统计: 比如统计一条微博被多少个不同的用户转发过。
使用 HyperLogLog 统计页面 UV 怎么做?
- PFADD key element1 element2 ...:添加一个或多个元素到 HyperLogLog 中。
- PFCOUNT key1 key2:获取一个或者多个 HyperLogLog 的唯一计数。
1、将访问指定页面的每个用户 ID 添加到 HyperLogLog 中。
bash
PFADD PAGE_1:UV USER1 USER2 ...... USERn
2、统计指定页面的 UV。
bash
PFCOUNT PAGE_1:UV
如果我想判断一个元素是否不在海量元素集合中,用什么数据类型?
这是布隆过滤器的经典应用场景。布隆过滤器可以告诉你一个元素一定不存在或者可能存在,它也有极高的空间效率和一定的误判率,但绝不会漏报。也就是说,布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后将对应的位数组的下标设置为 1(当位数组初始化时,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。
如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
Redis 持久化机制
Redis 支持持久化,而且支持 3 种持久化方式:
- 快照(snapshotting,RDB)
- 只追加文件(append-only file, AOF)
- RDB 和 AOF 的混合持久化(Redis 4.0 新增)
RDB 持久化
Redis DataBase 快照持久化(全量备份): 定时拍个快照,文件小、恢复快,但可能丢数据。
Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
RDB 创建快照时会阻塞主线程吗?
Redis 提供了两个命令来生成 RDB 快照文件:
- save : 同步保存操作,会阻塞 Redis 主线程。
- bgsave : fork 出一个子进程,子进程执行。
AOF 持久化
Append Only File 日志持久化(增量写命令): 每条写命令追加记日志,数据更安全,文件大、恢复慢。
与快照持久化相比,AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:
bash
appendonly yes
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。
只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。
AOF 工作基本流程
- 命令追加(append) :所有的写命令会追加到AOF 缓冲区中。
- 文件写入(write) :将 AOF 缓冲区的数据写入到 AOF 文件中 。这一步需要调用write函数 (系统调用),write将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。
- 文件同步(fsync) :这一步才是持久化的核心!根据你在 redis.conf 文件里 appendfsync 配置的策略,Redis 会在不同的时机,调用fsync 函数 (系统调用)。fsync 针对单个文件操作,对其进行强制硬盘同步(文件在内核缓冲区里的数据写到硬盘),fsync 将阻塞直到写入磁盘完成后返回,保证了数据持久化。
- 文件重写(rewrite) :随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
- 重启加载(load) :当 Redis 重启时,可以加载 AOF 文件进行数据恢复。

RDB 和 AOF 的混合持久化(Redis 4.0 )
结合 RDB 恢复速度快和 AOF 数据安全性高的优势,弥补两种纯持久化方式的不足。
- 开启混合持久化后,Redis 在执行 AOF 重写时,子进程会先将内存中的全量数据以 RDB 二进制快照格式写入新的 AOF 文件头部,之后父进程处理的增量写命令则以普通 AOF 文本格式追加到文件末尾。
- 正常运行时,Redis 依旧按照 AOF 机制持续追加写命令,只有在满足 AOF 重写条件时才会生成 RDB 格式内容,并非定时单独生成 RDB 快照。
- 恢复数据时,Redis 会先加载头部的 RDB 快照快速恢复基础数据,再逐行执行后续的 AOF 增量命令补齐最新数据,既保证了恢复效率,又能最大程度减少数据丢失。
混合持久化的优势明显,恢复速度接近纯 RDB,数据安全性与 AOF 一致,缺点是 AOF 文件中的 RDB 部分为二进制格式,可读性较差,同时压缩与解析会消耗一定 CPU 资源。
怎么选?
若优先考虑备份、灾难恢复、主从复制效率和低性能开销,且能接受一定的数据丢失(如分钟级),可选择 RDB;
若优先考虑数据安全性(秒级持久化)、版本兼容性和可操作性,能接受较大的文件体积和较慢的恢复速度,可选择 AOF;
若想兼顾两者优势,Redis 4.0 及以上版本可开启混合持久化,结合 RDB 恢复快和 AOF 安全高的特点,是生产环境中更推荐的方案。
Redis 线程模型
Redis 的核心命令执行是单线程模型,一个线程通过 I/O 多路复用(如 epoll)监听多个客户端连接,并在事件就绪时进行处理,从而避免了为每个连接创建线程的开销。
Redis 之所以采用单线程,是因为其性能瓶颈主要在内存和网络,而不是 CPU。也就是说,不是 CPU 算不过来,而是因为数据读写(内存)和数据传输(网络)更慢。因此,即使引入多线程,也无法显著提升性能,反而会带来线程切换、锁竞争和复杂性问题。同时单线程可以避免锁竞争、死锁以及线程上下文切换等问题,保证系统简单高效。
Redis 内存管理
为缓存数据设置过期时间
首先,可以控制内存使用,防止缓存数据长期占用内存导致 OOM 问题。
其次,满足业务的时效性需求,例如短信验证码、登录 Token 等数据只在一定时间内有效,通过过期时间可以自动失效,避免手动判断。
此外,过期机制可以保证缓存数据的"新鲜度",避免旧数据长期存在。
同时,设置过期时间还能简化系统设计,将数据过期逻辑交由 Redis 处理,提高开发效率。
最后,过期时间还可以配合缓存策略(如随机过期)来避免缓存雪崩等问题。
如何判断数据是否过期
Redis 通过一个叫做 expires 的过期字典来管理 key 的过期时间。该字典的键是数据库中的 key,值是一个毫秒级的 UNIX 时间戳,表示该 key 的过期时间。
当客户端访问某个 key 时,Redis 会先检查该 key 是否存在于过期字典中,如果不存在则说明该 key 没有设置过期时间;如果存在,则将当前时间与过期时间进行比较,如果已经过期,则删除该 key 并返回空结果,否则返回对应的 value。
这种设计将数据和过期信息分离,提高了查询效率并节省了内存空间。
过期 key 删除策略
常用的过期数据的删除策略就下面这几种:
- 惰性删除:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。
- 延迟队列:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
- 定时删除:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。
Redis 采用的是哪种删除策略呢?
Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略,这也是大部分缓存框架的选择。
惰性删除是在访问 key 时才检查其是否过期,如果过期则删除,这种方式对 CPU 友好,但可能导致过期 key 长时间占用内存。
定期删除(随机抽样 + 限时执行 + 自适应调节)则是 Redis 后台周期性随机抽取一部分设置了过期时间的 key 进行检查,并删除已过期的 key。该过程具有时间限制(如 25ms)和自适应机制:如果过期 key 比例较高,会加大清理力度,否则会提前结束,以减少 CPU 消耗。
此外,定期删除的执行频率由 hz 参数控制,默认每秒执行 10 次。
这种组合策略在 CPU 开销和内存占用之间取得了良好的平衡。
大量 key 集中过期怎么办?
当 Redis 中大量 key 在同一时间集中过期时,可能会导致 CPU 使用率升高、请求延迟增加以及内存压力增大等问题。这是因为 Redis 在处理过期 key 时需要执行删除操作,而 Redis 的核心命令执行是单线程的,大量删除操作会影响正常请求处理。
为了解决该问题,可以采用以下方案:
- 在设置过期时间时引入随机值,避免 key 同时过期;
- 开启lazy free 机制(lazyfree-lazy-expire),将删除操作放到后台线程执行,减少对主线程的阻塞;
- 在高并发场景下,可以通过限流、降级或多级缓存等手段缓解压力。
Redis 内存淘汰机制
Redis 的内存淘汰策略是在内存使用达到 maxmemory 限制时触发的,用于决定删除哪些数据以腾出空间。Redis 提供了多种淘汰策略,包括 volatile 和 allkeys 两大类,其中 volatile 表示只淘汰设置了过期时间的 key,allkeys 表示所有 key 都可以被淘汰。
常见策略包括 allkeys-lru(最近最少使用)、allkeys-lfu(最少使用频率)、volatile-lru 等。其中,allkeys-lfu 能够更好地保留热点数据,是较推荐的策略。
对于"2000 万数据,Redis 只存 20 万热点数据"的问题,可以通过以下方式保证热点数据:
首先,使用 allkeys-lru 或 allkeys-lfu 淘汰策略,使冷数据自动被淘汰;其次,采用缓存旁路模式(Cache Aside),只有被访问的数据才会加载到 Redis;最后,设置合理的过期时间,避免冷数据长期占用内存。
Redis 事务
Redis 事务是将多个命令进行打包,然后按顺序执行的一种机制。通过 MULTI 开启事务,之后的命令会被放入队列中,直到执行 EXEC 时才会统一执行。
Redis 事务并不具备传统关系型数据库事务的完整特性,它不支持回滚,也不保证严格的原子性和持久性。因此 Redis 事务更像是一个命令队列机制。
此外,Redis 提供 WATCH 命令实现乐观锁机制,可以监控某个 key 是否被其他客户端修改,如果在事务执行前 key 被修改,则事务会失败。
由于 Redis 事务功能较弱且性能开销较高,实际开发中一般不建议使用,而是使用 Lua 脚本或分布式锁来实现复杂一致性控制。
Redis 性能优化
Redis 性能优化的核心在于减少网络开销和 RTT(往返时间)。
一个 Redis 命令的执行可以简化为以下 4 步:
- 发送命令;
- 命令排队;
- 命令执行;
- 返回结果。
其中,第 1 步和第 4 步耗费时间之和称为 Round Trip Time(RTT,往返时间),也就是数据在网络上传输的时间。
为了提升性能,可以采用批量操作来减少网络请求次数,例如使用 MGET、MSET 等原生批量命令。同时,也可以使用 Pipeline 技术将多个命令一次性发送到 Redis 服务器,从而减少 RTT 和 socket I/O 开销。
在 Redis 官方提供的分片集群解决方案 Redis Cluster 环境下,由于采用哈希槽机制,不同 key 可能分布在不同节点,因此批量命令可能被拆分为多个请求,无法保证原子性。
总体来说,Redis 性能优化的核心思想是减少网络交互次数,提高一次请求的数据处理能力。
Pipeline
为减少 RTT,可以使用 Pipeline 技术,将多条 Redis 命令一次性打包发送给服务端执行,从而将多次网络请求合并为一次请求,显著降低网络开销和 socket I/O 次数。
Lua 脚本
Redis Lua 脚本是一种在服务端执行的脚本机制,一段 Lua 脚本在 Redis 中可以视为一条命令执行,执行过程具有原子性,即在执行期间不会被其他命令或脚本打断。
Lua 脚本相比 Pipeline 的优势在于不仅可以批量执行 Redis 命令,还支持在脚本中进行逻辑判断和数据处理,从而实现复杂业务逻辑。
但是 Lua 脚本也存在局限性,一方面,如果脚本执行过程中出现错误,已经执行的命令不会回滚,因此不具备数据库级别的事务原子性;另一方面,在 Redis Cluster 环境下,由于 key 分布在不同 hash slot 上,Lua 脚本的原子性也无法跨节点保证。
Pipeline 是客户端批量发送多个命令,用于减少网络 RTT,从而提升性能,但不具备原子性。
Lua 脚本是在 Redis 服务端执行的一段脚本,可以批量执行命令并支持逻辑控制,在执行过程中不会被其他命令打断,因此具备逻辑上的原子性,不支持回滚。
Redis 生产问题
缓存穿透
缓存穿透是指大量请求查询一个既不在缓存中,也不在数据库中的数据,导致请求每次都直接打到数据库上,从而对数据库造成较大压力。
产生原因主要包括恶意构造非法请求、参数校验不严以及查询不存在的数据未进行缓存处理。
常见解决方案包括:
- 参数校验,在请求进入系统前直接拦截非法请求,避免无效查询进入数据库。
- 缓存空值,当数据库查询结果为空时,也将空值写入 Redis 并设置较短过期时间,避免重复查询数据库。
- 布隆过滤器,在请求进入缓存前先通过布隆过滤器判断 key 是否存在,不存在则直接返回,从源头拦截无效请求。
- 接口限流,通过限制访问频率或 IP 黑名单机制,防止恶意请求过多访问系统。
缓存击穿
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
常见解决方案包括:
- 永不过期(不推荐):设置热点数据永不过期或者过期时间比较长。
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
- 加锁(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存。
缓存雪崩
缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。
常见解决方案包括:
针对 Redis 服务不可用的情况:
- Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
- 多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
针对大量缓存同时失效的情况:
- 设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。
- 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在于缓存中(通常是因为缓存中的那份数据已经过期)。