Skip to content

腾讯后端暑期实习面经 ​

来源:https://www.nowcoder.com/feed/main/detail/456f7bc6b2264dd5944e69639f76c281

1、虚拟机和容器的区别是什么? ​

基本定义 ​

虚拟机 ​

通过虚拟化技术(如 VMware、VirtualBox、Hyper-V 或 KVM)在物理硬件上模拟出一台完整的计算机。它包括一个完整的操作系统(Guest OS)、应用程序以及所需的库和依赖项。虚拟机运行在虚拟化层(Hypervisor)之上,Hypervisor 负责管理物理硬件资源并分配给多个虚拟机。

容器 ​

一种轻量级的虚拟化技术,它将应用程序及其依赖项(如库、配置文件等)打包在一起,但不包含完整的操作系统。容器共享宿主操作系统的内核,通过容器引擎(如 Docker、Podman)进行管理和隔离。

架构差异 ​

虚拟机 ​
  • 架构:物理硬件 → Hypervisor(如 Type 1 或 Type 2) → 虚拟机(每个虚拟机包含 Guest OS + 应用 + 依赖项)。
  • 每个虚拟机都有独立的操作系统内核,导致资源占用较高。
  • Hypervisor 模拟硬件(如虚拟 CPU、内存、磁盘),Guest OS 运行在这些虚拟硬件上。
容器 ​
  • 架构:物理硬件 → 宿主操作系统 → 容器引擎(如 Docker) → 容器(仅包含应用 + 依赖项)。
  • 容器直接使用宿主操作系统的内核,不需要额外的 Guest OS,因此更轻量。
  • 通过命名空间(Namespace)和控制组(cgroups)实现隔离。

工作原理 ​

虚拟机 ​

Hypervisor(虚拟机管理程序)将物理硬件抽象为多个虚拟硬件实例,每个虚拟机认为自己独占一套硬件。Guest OS 运行在虚拟硬件上,与宿主 OS 完全独立。例如,你可以在 Linux 宿主机上运行多个 Windows VM。

容器 ​

容器通过操作系统的特性(如 Linux 的 Namespace 和 cgroups)实现隔离:

  • Namespace:隔离进程、文件系统、网络等,使每个容器有独立的运行环境。
  • cgroups:限制资源使用(如 CPU、内存),防止容器之间相互干扰。 容器直接调用宿主内核的系统调用,无需模拟硬件,因此效率更高。

主要区别 ​

特性虚拟机 (VM)容器 (Container)
操作系统每个 VM 有独立的 Guest OS共享宿主 OS 的内核,无独立 OS
资源占用较高(需要为每个 OS 分配资源)较低(仅打包应用和依赖项)
启动速度较慢(需要启动整个 OS)很快(几秒甚至毫秒级别)
隔离性强(完全隔离,独立的内核和硬件模拟)较弱(共享内核,依赖宿主 OS 隔离机制)
可移植性依赖虚拟化平台,迁移稍复杂高(容器镜像可在支持的平台上运行)
安全性高(内核隔离,漏洞影响范围小)较低(共享内核,可能受宿主影响)
典型工具VMware, VirtualBox, KVM, Hyper-VDocker, Podman, Kubernetes

2、mysql聚集索引和二级索引? ​

基本定义 ​

聚集索引 ​

聚集索引是指数据行的物理存储顺序与索引的顺序一致。换句话说,表的数据本身就是按照聚集索引的键值排序存储的。一个表只能有一个聚集索引,因为数据的物理存储顺序只能有一种。

二级索引 ​

二级索引是非聚集索引,索引的键值和数据行的物理存储顺序无关。二级索引存储的是索引键值以及指向实际数据行的引用(通常是主键值)。一个表可以有多个二级索引。

存储结构 ​

聚集索引 ​
  • 在 InnoDB 引擎中(MySQL 默认引擎),聚集索引的叶子节点直接存储整行数据。
  • 主键(Primary Key)默认是聚集索引。如果表没有定义主键,InnoDB 会选择一个唯一非空索引作为聚集索引;如果没有这样的索引,InnoDB 会生成一个隐藏的 6 字节 ROWID 作为聚集索引。
  • 数据和索引是“绑定”在一起的,查询聚集索引时不需要额外的查找,直接就能获取整行数据。
二级索引 ​
  • 二级索引的叶子节点存储的是索引键值和指向数据的指针(通常是主键值)。
  • 数据本身存储在聚集索引中,因此查询二级索引时,通常需要通过主键值再去聚集索引中查找完整数据(称为“回表”)。

工作原理 ​

假设有一个表 users,结构如下:

id (主键)  | name | age
1         | Alice | 25
2         | Bob   | 30
3         | Carol | 35
聚集索引 ​

假设 id 是主键(即聚集索引),表的数据按 id 的顺序物理存储:

id=1 | Alice | 25
id=2 | Bob   | 30
id=3 | Carol | 35

查询 SELECT * FROM users WHERE id = 2 时,InnoDB 直接定位到 id=2 的记录,获取整行数据,无需额外查找。

二级索引 ​

假设在 name 上创建了二级索引,索引结构可能是:

name=Alice | id=1
name=Bob   | id=2
name=Carol | id=3

查询 SELECT * FROM users WHERE name = 'Bob' 时:

  1. 先在二级索引中找到 name='Bob',得到 id=2。
  2. 再到聚集索引中通过 id=2 查找完整数据(回表)。

如果查询只需 name 和 id(如 SELECT name, id FROM users WHERE name = 'Bob'),可以直接从二级索引获取数据,无需回表(称为“覆盖索引”)。

区别 ​

特性聚集索引 (Clustered Index)二级索引 (Secondary Index)
存储内容叶子节点存储整行数据叶子节点存储索引键值 + 主键指针
数量限制一个表只能有一个一个表可以有多个
数据顺序数据按索引键物理排序数据顺序与索引无关
查询效率直接获取数据,效率高可能需要回表,效率稍低
插入/更新成本较高(需要维护数据物理顺序)较低(仅更新索引结构)

3、mysql的隔离模式? ​

事务隔离的目标是解决并发事务执行时可能出现的问题,主要包括以下三种异常:

  • 脏读: 一个事务读取到另一个事务未提交的数据。如果后一个事务回滚,前一个事务读取的数据就变成了“脏数据”。
  • 不可重复读:一个事务在执行期间多次读取同一行数据,但由于另一个事务修改并提交了该数据,导致前后读取结果不一致。
  • 幻读:一个事务在执行期间多次查询某个范围的数据,但由于另一个事务插入或删除了数据,导致前后查询结果的行数不一致(像是“幻觉”)。

四种隔离级别 ​

MySQL InnoDB 支持以下四种隔离级别,从低到高分别是:

读未提交 ​

事务可以读取其他事务未提交的数据。

可能的问题:

  • 脏读:读取到未提交的“脏数据”。
  • 不可重复读和幻读也可能发生。

例子:

sql
事务 A: UPDATE users SET age = 30 WHERE id = 1;  (未提交)
事务 B: SELECT age FROM users WHERE id = 1;     (读取到 age = 30)
事务 A: ROLLBACK;                              (回滚,age 恢复为原值)

事务 B 读取到了未提交的“脏数据”。

读已提交 ​

事务只能读取其他事务已提交的数据,解决了脏读问题。

可能的问题:

  • 不可重复读:同一事务内多次读取同一行,数据可能被其他事务修改。
  • 幻读也可能发生。

例子:

sql
事务 A: SELECT age FROM users WHERE id = 1;     (读取 age = 25)
事务 B: UPDATE users SET age = 30 WHERE id = 1; (提交)
事务 A: SELECT age FROM users WHERE id = 1;     (读取 age = 30)

事务 A 两次读取结果不同,出现不可重复读。

可重复读 ​

保证事务在执行期间多次读取同一行数据时,结果一致,解决了不可重复读问题。这是 MySQL InnoDB 的默认隔离级别。

可能的问题:

  • 幻读:在范围查询时,其他事务插入或删除数据可能导致“幻行”。

实现方式:

  • 使用 MVCC:事务开始时生成一个一致性快照(Read View),后续读取都基于此快照。
  • 结合间隙锁(Gap Lock)防止幻读(InnoDB 在此级别增强了幻读防护)。

例子:

sql
事务 A: SELECT age FROM users WHERE id = 1;     (读取 age = 25)
事务 B: UPDATE users SET age = 30 WHERE id = 1; (提交)
事务 A: SELECT age FROM users WHERE id = 1;     (仍读取 age = 25)

事务 A 的读取结果保持一致,但如果涉及范围查询,可能遇到幻读:

sql
事务 A: SELECT * FROM users WHERE age > 20;     (返回 2 行)
事务 B: INSERT INTO users (id, age) VALUES (4, 25); (提交)
事务 A: SELECT * FROM users WHERE age > 20;     (返回 3 行,幻读)
串行化 ​

最高隔离级别,所有事务串行执行,完全隔离,解决了脏读、不可重复读和幻读。

实现方式:通过加锁(表锁或行锁)强制事务顺序执行,禁止并发操作。

可能的问题:并发性能极低,可能导致锁冲突和死锁。

例子:

sql
事务 A: SELECT * FROM users WHERE age > 20;     (加锁)
事务 B: INSERT INTO users (id, age) VALUES (4, 25); (被阻塞,直到 A 提交)

隔离级别对比 ​

隔离级别脏读不可重复读幻读并发性能
读未提交是是是高
读已提交否是是中
可重复读 (默认)否否是*中
串行化否否否低

4、undolog redolog binlog? ​

Undo Log(回滚日志) ​

undo log 是 InnoDB 存储引擎用来记录事务执行前的旧数据版本的日志,主要用于事务回滚和一致性读(如 MVCC,多版本并发控制)。

作用 ​
  • 事务回滚:如果事务执行失败或手动回滚(如 ROLLBACK),undo log 提供旧数据,用于将数据库恢复到事务开始前的状态。
  • 一致性读:在隔离级别(如读已提交、可重复读)下,通过 undo log 生成数据的历史版本快照,保证事务读取到一致的数据,而不受其他事务修改的影响。
存储位置 ​
  • undo log 存储在 InnoDB 的 undo log segment 中,位于共享表空间(ibdata 文件)或独立的 undo tablespace(MySQL 5.6 及以上支持)。
  • 分为两种类型:
    • Insert Undo Log:记录插入操作的旧数据,仅在回滚时使用,事务提交后可删除。
    • Update/Delete Undo Log:记录更新或删除操作的旧数据,用于 MVCC,可能长期保留。
工作原理 ​

假设表 users 中有一行数据 id=1, age=25:

  • 事务 A 执行 UPDATE users SET age = 30 WHERE id = 1。
  • InnoDB 将旧值 age=25 写入 undo log。
  • 如果事务回滚,InnoDB 用 undo log 中的 age=25 恢复数据。
  • 如果事务提交,undo log 可能被标记为可清理(但 MVCC 可能暂时保留)。
特点 ​
  • 逻辑日志,记录的是操作的逆过程(如“将 age 从 30 改回 25”)。
  • 与事务一致性密切相关,是 MVCC 的核心组件。

Redo Log(重做日志) ​

redo log 是 InnoDB 存储引擎用来记录事务修改后的物理数据变化的日志,主要用于崩溃恢复(Crash Recovery),确保已提交事务的持久性。

作用 ​
  • 数据持久性:当事务提交时,redo log 先写入磁盘,即使内存中的数据页(Buffer Pool)尚未刷新到磁盘,崩溃后也能通过 redo log 恢复。
  • 提高性能:通过先写日志、再异步刷盘的方式,减少直接写数据文件的 I/O 开销。
存储位置 ​
  • 默认存储在磁盘上的 ib_logfile0 和 ib_logfile1 文件中(通常两个文件,循环使用)。
  • 属于物理日志,记录的是数据页的修改(如“在某页的偏移量 X 处写入值 Y”)。
工作原理 ​

还是表 users,事务 A 执行 UPDATE users SET age = 30 WHERE id = 1:

  • 修改操作先写入内存中的 Buffer Pool。
  • 同时将修改记录(如“修改页面 X,偏移 Y,值从 25 变为 30”)写入 redo log buffer。
  • 事务提交时,redo log buffer 刷入磁盘(ib_logfile)。
  • 如果崩溃,InnoDB 重启时读取 redo log,重放已提交的修改。
特点 ​
  • 顺序写磁盘,性能高(相比数据文件的随机写)。
  • 日志文件大小固定,写满后循环覆盖(未刷盘的数据会先持久化)。
  • 与 WAL(Write-Ahead Logging,预写日志)机制相关。

Binlog(二进制日志) ​

binlog 是 MySQL Server 层面的日志,记录所有对数据库的修改操作(如 INSERT、UPDATE、DELETE),主要用于主从复制和数据恢复。

作用 ​
  • 主从复制:主库将 binlog 传输到从库,从库重放这些操作以保持数据一致。
  • 数据恢复:结合全量备份和 binlog,可以实现时间点恢复(Point-in-Time Recovery, PITR)。
存储位置 ​
  • 存储在磁盘上的二进制文件(如 mysql-bin.000001),文件名和位置可配置。
  • 通过参数 log_bin 启用,默认关闭。
工作原理 ​

事务 A 执行 UPDATE users SET age = 30 WHERE id = 1:

  • 操作完成后,Server 层将语句或数据变化写入 binlog(格式取决于配置)。
  • 主库提交事务时,binlog 同步写入磁盘。
  • 从库读取 binlog,执行相同的修改。
格式 ​
  • Statement:记录 SQL 语句(如 UPDATE users SET age = 30 WHERE id = 1),简单但可能不精确。
  • Row:记录具体的数据行变化(如“将 id=1 的 age 从 25 改为 30”),精确但占用空间大。
  • Mixed:混合模式,根据情况选择 Statement 或 Row。
特点 ​
  • 逻辑日志,记录操作语句或数据变化。
  • 与存储引擎无关,MyISAM 也支持。
  • 可用于审计和跨库同步。

三者的区别 ​

特性Undo LogRedo LogBinlog
所属层InnoDB 存储引擎InnoDB 存储引擎MySQL Server 层
作用回滚、一致性读崩溃恢复、持久性主从复制、数据恢复
日志类型逻辑日志(旧数据)物理日志(数据页变化)逻辑日志(语句或行变化)
存储位置表空间(ibdata 或单独文件)ib_logfile 文件mysql-bin 文件
生命周期事务提交后可清理循环覆盖(刷盘后可重用)可配置保留时间或手动清理
是否必须是(支持事务和 MVCC)是(支持崩溃恢复)否(可选,用于复制)

联系 ​

  • 事务提交流程:

    1. 修改数据时,生成 undo log(支持回滚)。

    2. 将修改写入 redo log buffer(保证持久性)。

    3. 事务提交时,redo log 刷盘,同时记录 binlog(若启用)。

    4. binlog 刷盘后,事务完成。

  • 一致性:redo log 确保物理一致性,undo log 确保逻辑一致性,binlog 用于外部同步。

  • 崩溃恢复:redo log 重放提交事务,undo log 回滚未提交事务。

5、binlog的3种形式? ​

Statement(基于语句的日志格式,STATEMENT-BASED REPLICATION, SBR) ​

Statement 格式记录的是执行的原始 SQL 语句,而不是具体的行数据变化。

工作原理 ​
  • 每次写操作(如 INSERT、UPDATE、DELETE)发生时,MySQL 将完整的 SQL 语句写入 binlog。
  • 从库读取 binlog 并重新执行这些 SQL 语句,以实现数据同步。

例如,假设执行以下操作:

sql
UPDATE users SET age = age + 1 WHERE id = 1;

binlog 记录:

sql
UPDATE users SET age = age + 1 WHERE id = 1;

从库直接执行这条语句,更新对应的行。

Row(基于行的日志格式,ROW-BASED REPLICATION, RBR) ​

Row 格式记录的是具体的数据行变化,而不是 SQL 语句本身。它记录每行数据的“前镜像”(before image)和“后镜像”(after image)。

工作原理 ​
  • 对于每次写操作,MySQL 将受影响的每一行数据的具体变化写入 binlog。
  • 从库直接应用这些行级变化,无需重新执行 SQL。

例如,还是执行:

sql
UPDATE users SET age = age + 1 WHERE id = 1;

假设原始数据是 id=1, age=25:

binlog 记录:

sql
Before: id=1, age=25
After:  id=1, age=26

从库直接将 id=1 的 age 从 25 更新为 26。

Mixed(混合日志格式,MIXED-MODE REPLICATION) ​

Mixed 格式是 Statement 和 Row 的混合模式,MySQL 根据操作类型动态选择记录方式。通常优先使用 Statement,但在特定情况下切换为 Row。

工作原理 ​
  • 默认记录 SQL 语句(Statement)。
  • 遇到可能导致主从不一致的操作时,切换为记录行变化(Row)。
  • 切换的典型场景包括:
    • 使用非确定性函数(如 NOW()、UUID())。
    • 使用 INSERT ... ON DUPLICATE KEY UPDATE 或 LOAD DATA INFILE。
    • 执行涉及存储过程、触发器或复杂语句的操作。

示例:

  • 执行 INSERT INTO logs (time) VALUES (NOW());:

    binlog 切换为 Row 格式,记录具体行数据(如 time=2025-03-05 12:00:00)。

  • 执行 UPDATE users SET age = 30 WHERE id = 1;:

    binlog 使用 Statement 格式,记录语句本身。

三种格式对比表 ​

特性StatementRowMixed
记录内容SQL 语句具体行变化动态切换(语句或行)
日志大小小大中等
一致性较低(可能不一致)高(完全一致)高(智能优化)
可读性高(SQL 可读)低(二进制)中等(混合)
从库性能较低(需执行 SQL)高(直接应用变化)中等(视情况而定)
适用场景简单操作、审计高一致性、复杂操作通用场景

6、乐观锁悲观锁? ​

基本定义 ​

悲观锁 ​
  • 假设:假定并发冲突很常见,认为多个事务同时访问数据时很可能会发生冲突。
  • 策略:在操作数据前,先对数据加锁,确保其他事务无法同时访问或修改,直到当前事务完成并释放锁。
乐观锁 ​
  • 假设:假定并发冲突很少发生,认为大多数情况下多个事务不会同时修改同一数据。
  • 策略:不提前加锁,操作时直接读取数据,提交时检查数据是否被其他事务修改过,若冲突则回滚或重试。

工作原理 ​

悲观锁 ​

实现方式:

  • 通过数据库的锁机制(如行锁、表锁)实现。
  • 常见 SQL 关键字:SELECT ... FOR UPDATE(在 MySQL 中)。

流程:

  1. 事务开始时,锁定目标数据(例如,锁定某行)。
  2. 其他事务试图访问被锁定的数据时会被阻塞,直到锁释放。
  3. 当前事务完成(提交或回滚),释放锁。

示例:

sql
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 加行锁
UPDATE users SET age = 30 WHERE id = 1;
COMMIT;

事务执行期间,其他事务无法修改 id=1 的行。

乐观锁 ​

实现方式:

  • 通常通过版本号(version)或时间戳(timestamp)字段实现。
  • 不依赖数据库锁,而是通过应用程序逻辑控制。

流程:

  1. 读取数据时,记录当前的版本号。
  2. 更新数据时,检查版本号是否一致。
  3. 如果一致,则更新成功并递增版本号;否则,说明数据已被修改,事务失败。

示例:

sql
CREATE TABLE users (
    id INT PRIMARY KEY,
    age INT,
    version INT
);

操作:

sql
-- 事务 A
SELECT age, version FROM users WHERE id = 1; -- 读取 age=25, version=1
-- 事务 B 修改了数据
UPDATE users SET age = 26, version = 2 WHERE id = 1 AND version = 1;
-- 事务 A 尝试更新
UPDATE users SET age = 30, version = 2 WHERE id = 1 AND version = 1; -- 失败,因 version 已变为 2

主要区别 ​

特性悲观锁乐观锁
并发假设冲突频繁冲突罕见
锁机制提前加锁不加锁,提交时验证
实现方式数据库锁(FOR UPDATE 等)版本号、时间戳等逻辑控制
性能开销高(锁阻塞降低并发性)低(无锁,但冲突时需重试)
适用场景高冲突、高一致性需求低冲突、高并发读写
复杂度简单(依赖数据库)稍复杂(需应用层逻辑)

7、mysql怎么实现悲观锁? ​

使用 SELECT ... FOR UPDATE(行级排他锁) ​

对查询的行加排他锁(X Lock),阻止其他事务读写这些行,直到当前事务结束。

语法 ​
sql
SELECT column_list FROM table_name WHERE condition FOR UPDATE;
示例 ​

假设表 users:

sql
id | name | age
1  | Alice| 25
2  | Bob  | 30

事务 A:

sql
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 加排他锁
UPDATE users SET age = 26 WHERE id = 1;
COMMIT;

事务 A 执行 FOR UPDATE 时,id=1 的行被锁定。

事务 B 若尝试:

sql
SELECT * FROM users WHERE id = 1 FOR UPDATE;

会被阻塞,直到事务 A 提交或回滚。

使用 SELECT ... LOCK IN SHARE MODE(行级共享锁) ​

对查询的行加共享锁(S Lock),允许多个事务读取,但阻止写入,直到当前事务结束。

语法 ​
sql
SELECT column_list FROM table_name WHERE condition LOCK IN SHARE MODE;
示例 ​

事务 A:

sql
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- 加共享锁
-- 读取 age=25
COMMIT;

事务 B 可以同时执行:

sql
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- 允许读

但事务 B 若尝试:

sql
UPDATE users SET age = 30 WHERE id = 1; -- 被阻塞

会被阻塞,直到事务 A 结束。

使用 LOCK TABLES(表级锁) ​

对整个表加锁,分为读锁(READ)和写锁(WRITE)。

语法 ​
sql
LOCK TABLES table_name [READ | WRITE];
UNLOCK TABLES;
示例 ​

读锁:

sql
LOCK TABLES users READ;
SELECT * FROM users;
UNLOCK TABLES;

其他事务可以读,但不能写。

写锁:

sql
LOCK TABLES users WRITE;
UPDATE users SET age = age + 1;
UNLOCK TABLES;

其他事务无法读写。

8、SELECT...FOR UPDATE和 SELECT...LOCK IN SHARE MODE语句? ​

上面已讲过。

9、场景题:redis实现滑动窗口? ​

这里大家可以借助AI工具

10、 redis的持久化? ​

RDB(快照持久化) ​

RDB 是 Redis 的默认持久化方式,通过定期将内存中的数据集以二进制快照的形式保存到磁盘上,生成一个压缩的 .rdb 文件。

工作原理 ​

触发机制:

  • 手动触发:

    • SAVE:同步保存,阻塞 Redis 主进程。
    • BGSAVE:异步保存,fork 一个子进程执行快照操作,主进程继续处理请求。
  • 自动触发:

    通过配置文件中的 save 指令设置,例如:

    sql
    save 900 1    # 900 秒内至少 1 次键变更
    save 300 10   # 300 秒内至少 10 次键变更
    save 60 10000 # 60 秒内至少 10000 次键变更

    Redis 检测到满足条件时,自动调用 BGSAVE。

保存过程:

  1. Redis 调用 fork() 创建子进程。
  2. 子进程将内存数据写入临时文件(.rdb.tmp)。
  3. 保存完成后,临时文件替换旧的 .rdb 文件。

文件位置:由配置文件中的 dir 和 dbfilename 参数指定,默认是 ./redis.rdb。

数据恢复 ​

重启 Redis 时,若检测到 .rdb 文件,自动加载到内存中,恢复数据。

AOF(追加式持久化) ​

AOF 通过将每次写操作(修改数据的命令)追加到日志文件中保存,记录 Redis 的操作历史,从而实现持久化。重启时通过重放这些命令恢复数据。

工作原理 ​

记录方式:

  • 每次执行写命令(如 SET、DEL、INCR)时,命令以 Redis 协议格式追加到 AOF 文件。

  • 示例:执行 SET key value,AOF 文件记录:

    *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

同步策略(由 appendfsync 参数控制):

  • always:每次写命令后立即同步到磁盘,数据最安全,但性能最低。
  • everysec:每秒同步一次,默认选项,平衡性能和安全性(最多丢失 1 秒数据)。
  • no:由操作系统决定同步时机,性能最高,但数据丢失风险大。

文件位置:由 dir 和 appendfilename 参数指定,默认是 ./appendonly.aof。

重写机制(AOF Rewrite) ​

问题:AOF 文件会随写操作不断增长,可能变得很大。

解决:Redis 提供 AOF 重写功能,优化冗余命令,生成更紧凑的文件。

  • 手动触发:BGREWRITEAOF。

  • 自动触发:由参数控制:

    sql
    auto-aof-rewrite-percentage 100  # 文件增长 100% 时重写
    auto-aof-rewrite-min-size 64mb  # 文件至少 64MB 时才重写

重写过程:

  1. fork 子进程。
  2. 子进程根据当前内存数据生成新的 AOF 文件(只记录最终状态,例如将多次 INCR 合并为一个 SET)。
  3. 主进程继续追加新命令到旧 AOF 文件和缓冲区。
  4. 重写完成后,新文件替换旧文件,缓冲区内容追加到新文件。
数据恢复 ​

重启 Redis 时,若启用 AOF(appendonly yes),逐条执行 AOF 文件中的命令,重建数据集。

11、开启持久化模式会不会导致redis变慢? ​

会。

RDB ​

RDB 通过定期生成内存快照(.rdb 文件)实现持久化,通常由 BGSAVE 命令触发,fork 一个子进程完成保存。

影响:

  • fork 和快照保存可能导致短暂延迟(毫秒级),高内存高频场景更明显。
  • 通常较小,除非频繁触发或内存超大。

AOF ​

AOF 将每次写命令追加到日志文件(.aof),并根据 appendfsync 参数决定同步策略:

  • always:每次写后同步。
  • everysec:每秒同步(默认)。
  • no:操作系统决定。

影响:

  • always:显著变慢,写性能下降严重。
  • everysec:轻微变慢,影响可接受。
  • no:几乎不影响。

12、redis为什么快? ​

Redis(Remote Dictionary Server)以其极高的性能著称,常被用作缓存、消息队列和实时数据存储。它的“快”并非单一因素的结果,而是多个设计和实现上的优势共同作用的成果。

从下面这几个方面聊聊:

内存存储(In-Memory Storage) ​

原理 ​
  • Redis 是一个内存数据库,数据主要存储在 RAM 中,而不是磁盘上。
  • 内存访问速度比磁盘快几个数量级:
    • RAM 访问延迟:纳秒级(~10-100 ns)。
    • 磁盘访问延迟:毫秒级(机械硬盘 ~10ms,SSD ~0.1ms)。
影响 ​
  • 所有读写操作都在内存中完成,避免了磁盘 I/O 的瓶颈。
  • 即使开启持久化(RDB 或 AOF),核心操作仍基于内存,磁盘写入是异步或后台处理的。

单线程事件循环(Single-Threaded Event Loop) ​

原理 ​
  • Redis 使用单线程处理客户端请求,避免了多线程带来的上下文切换和锁竞争开销。
  • 采用事件驱动模型(基于 I/O 多路复用,如 epoll/select),通过非阻塞 I/O 处理多个客户端连接。
工作方式 ​
  • 事件循环:
    1. 监听客户端连接和命令(网络事件)。
    2. 将命令放入队列。
    3. 单线程按顺序处理队列中的命令。
  • 非阻塞:网络 I/O 和命令执行分离,单个线程高效利用 CPU。
影响 ​
  • 无锁竞争:无需线程同步(如互斥锁),消除了多线程模型的性能损耗。
  • 上下文切换少:单线程避免了线程切换的开销(每次切换 ~微秒级)。
  • 简单高效:逻辑清晰,指令执行顺序可预测。

高效的数据结构 ​

原理 ​
  • Redis 提供了丰富的数据结构(如 String、Hash、List、Set、Sorted Set),并针对每种结构进行了底层优化。
  • 这些数据结构使用高效的内存编码和算法,减少计算和内存开销。
具体实现 ​
  • SDS(Simple Dynamic String):
    • 替代 C 原生字符串,预分配空间,记录长度,避免频繁 realloc 和 strlen。
    • 读写复杂度:O(1)。
  • Hash 表:使用渐进式 rehash,均摊扩容成本,查询/插入复杂度接近 O(1)。
  • Skip List(跳跃表):用于 Sorted Set,查找/插入复杂度 O(log n),比平衡树更轻量。
  • 压缩列表(ziplist):小型 List/Set/Hash 使用紧凑内存布局,节省空间并加速遍历。
影响 ​
  • 数据操作复杂度低,执行效率高。
  • 内存使用率高,减少浪费。

I/O 多路复用(Multiplexing) ​

原理 ​
  • Redis 使用 epoll(Linux)、kqueue(BSD)或 select 等机制处理大量并发连接。
  • 单线程通过事件循环监听多个 socket,异步处理网络请求。
工作方式 ​
  • 一个线程管理成千上万客户端连接。
  • 当 socket 可读/可写时,触发事件,Redis 处理对应请求。
影响 ​
  • 高并发:支持数十万客户端同时连接,而无需为每个连接分配线程。
  • 低开销:网络层效率高,减少系统资源占用。

为什么比其他数据库快? ​

  • 对比传统磁盘数据库(如 MySQL):
    • MySQL 依赖磁盘 I/O 和复杂事务,单次查询延迟 1-10ms。
    • Redis 全内存操作,延迟 < 1ms。
  • 对比其他内存数据库(如 Memcached):
    • Memcached 单线程、无复杂数据结构,仅支持 KV。
    • Redis 提供丰富数据结构和持久化,性能仍接近 Memcached。

13、对网络安全相关了解吗?知道什么攻击? ​

恶意软件攻击 (Malware Attack) ​

恶意软件(Malware)是旨在破坏、窃取或控制系统的恶意代码的总称,包括病毒(Virus)、蠕虫(Worm)、木马(Trojan)、勒索软件(Ransomware)等。

原理:

  • 病毒:依附于合法程序,需用户交互(如打开附件)传播。
  • 蠕虫:自我复制,利用网络漏洞自动传播,无需用户干预。
  • 木马:伪装成合法软件,诱导用户安装后窃取数据或开启后门。
  • 勒索软件:加密用户数据,要求支付赎金以解密。

影响:数据丢失、系统瘫痪、财务损失。

钓鱼攻击 (Phishing Attack) ​

一种社会工程攻击,通过伪装成可信实体(如银行、公司)诱骗用户提供敏感信息(如密码、信用卡号)。

原理:

  • 发送伪造的电子邮件、短信或网站链接,诱导用户点击或输入信息。
  • 常结合鱼叉式钓鱼(Spear Phishing),针对特定目标定制内容。

影响:身份盗窃、账户被盗。

拒绝服务攻击 (Denial-of-Service, DoS) 和 分布式拒绝服务攻击 (DDoS) ​

  • 定义:
    • DoS:通过大量请求淹没目标系统,使其无法响应合法用户。
    • DDoS:利用多个受控设备(如僵尸网络)发起分布式攻击。
  • 原理:
    • 消耗目标的带宽、CPU 或内存资源。
    • 常见手段包括 SYN 洪水、UDP 洪水、HTTP 请求轰炸。
  • 影响:服务中断、网站宕机。

中间人攻击 (Man-in-the-Middle, MitM) ​

攻击者在通信双方之间拦截并可能篡改数据。

原理:

  • 通过 ARP 欺骗、DNS 劫持或伪造 Wi-Fi 热点窃听流量。
  • 可窃取未加密数据或注入恶意内容。

影响:敏感数据泄露、会话劫持。

SQL 注入攻击 (SQL Injection) ​

攻击者通过向数据库输入恶意 SQL 代码,获取未授权数据或操控数据库。

原理:

  • 利用未正确验证的用户输入(如表单字段),执行非预期查询。
  • 示例输入:' OR '1'='1 可绕过登录验证。

影响:数据库泄露、数据篡改。

密码攻击 (Password Attack) ​

尝试破解用户密码以获取访问权限。

  • 原理:
    • 暴力破解 (Brute Force):尝试所有可能组合。
    • 字典攻击 (Dictionary Attack):使用常见密码列表。
    • 凭据填充 (Credential Stuffing):利用泄露的用户名密码对尝试登录。
  • 影响:账户被盗、权限提升。

14、ping的时候用的什么协议? ​

在计算机网络中,ping 是一种常用的网络诊断工具,用于测试两台主机之间的连通性和往返时间(RTT,Round-Trip Time)。它主要依赖 ICMP(Internet Control Message Protocol,互联网控制消息协议) 来实现。

ICMP ​

  • ICMP 是 TCP/IP 协议族中的一种网络层协议,设计用于在 IP 网络中传输控制消息,而不是用户数据。
  • 它通常与 IP 协议(Internet Protocol)配合使用,IP 负责数据包的传输,而 ICMP 负责网络诊断和错误报告。

ping 的工作原理 ​

ping 使用 ICMP 的 Echo Request(回显请求) 和 Echo Reply(回显应答) 消息来测试网络连通性。

发送 Echo Request:

  • 本地主机(发起 ping 的设备)构造一个 ICMP Echo Request 消息。
  • 该消息包含一个唯一的标识符(Identifier)和序列号(Sequence Number),用于匹配请求和应答。
  • 数据包封装在 IP 数据包中,发送到目标主机。

目标主机响应:

  • 如果目标主机在线且允许 ICMP 响应,它会收到 Echo Request。
  • 目标主机构造一个 ICMP Echo Reply 消息,将原始请求的数据原样返回。
  • 应答数据包通过 IP 路由回传到源主机。

计算 RTT:

  • 源主机收到 Echo Reply 后,根据发送时间和接收时间计算往返时间(RTT)。

  • 输出结果,如:

    Reply from www.baidu.com: bytes=32 time=15ms TTL=117

15、你在浏览器中输入https://www.某网址.com并访问时,经历了什么过程? ​

URL 解析 ​

过程 ​
  • 输入 https://www.xxx.com。
  • 浏览器解析 URL,分解为以下部分:
    • 协议:https(超文本传输安全协议)。
    • 主机名:www.xxx.com(域名)。
    • 路径:默认 /(未指定具体路径时)。
    • 端口:隐式为 443(HTTPS 默认端口)。
细节 ​
  • 浏览器会检查输入是否合法(如语法错误)。
  • 如果省略协议(如只输入 www.xxx.com),现代浏览器通常默认补全为 https://(优先于 http://)。
  • 如果输入的是搜索关键词而非 URL,浏览器会将其交给默认搜索引擎处理。

DNS 域名解析 ​

过程 ​
  • 浏览器需要将域名 www.xxx.com 转换为服务器的 IP 地址,因为网络通信基于 IP。
  • DNS 解析过程如下:
    1. 检查浏览器缓存:浏览器先查看自身 DNS 缓存是否有 www.xxx.com 的 IP。
    2. 检查操作系统缓存:若无,查询本机 hosts 文件和系统 DNS 缓存(如 Windows 的 ipconfig /displaydns)。
    3. 本地 DNS 服务器:若仍无,向本地 DNS 服务器(通常由 ISP 提供,或如 8.8.8.8 的公共 DNS)发起请求。
    4. 递归解析:
      • 本地 DNS 服务器向根域名服务器(.)查询。
      • 根服务器返回 .com 的顶级域名服务器 (TLD) 地址。
      • TLD 服务器返回 xxx.com 的权威域名服务器地址。
      • 权威服务器返回 www.xxx.com 的 IP 地址(如 93.184.216.34)。
    5. 返回结果:本地 DNS 服务器将 IP 返回给操作系统和浏览器,并缓存结果。
细节 ​
  • 记录类型:通常查询 A 记录(IPv4 地址)或 AAAA 记录(IPv6 地址)。
  • 时间:DNS 解析通常耗时几毫秒到几十毫秒,取决于缓存命中率和网络延迟。
  • CDN:若网站使用内容分发网络(如 Cloudflare),DNS 可能返回就近节点的 IP。

TCP 连接建立 ​

过程 ​
  • 浏览器获取 IP 地址后,发起与目标服务器(93.184.216.34:443)的 TCP 连接。
  • TCP 三次握手:
    1. SYN:客户端发送 SYN 包(同步序列号),请求连接。
    2. SYN-ACK:服务器回复 SYN-ACK 包(确认客户端请求并发送自己的序列号)。
    3. ACK:客户端发送 ACK 包,确认连接建立。
细节 ​
  • 端口:HTTPS 使用 443 端口(HTTP 默认 80)。
  • 可靠性:TCP 确保数据有序、无损传输。
  • 时间:三次握手通常耗时一个往返时间(RTT),如 10-50ms。

TLS 安全握手 ​

过程 ​
  • 因为使用 HTTPS,浏览器与服务器在 TCP 连接之上建立 TLS(传输层安全协议)加密通道。

  • TLS 握手步骤:

    1. Client Hello:客户端发送支持的 TLS 版本(如 TLS 1.3)、加密算法(如 AES)、随机数等。

    2. Server Hello:服务器选择 TLS 版本和加密算法,返回随机数和证书(含公钥)。

    3. 证书验证:

      浏览器验证服务器证书:

      • 检查证书是否由受信任的 CA(证书颁发机构,如 Let’s Encrypt)签发。
      • 验证域名匹配(www.xxx.com)。
      • 检查证书有效期。
    4. 密钥交换:

      • 客户端生成会话密钥(Session Key),用服务器公钥加密后发送。
      • 服务器用私钥解密,双方协商出对称密钥。
    5. 握手完成:双方交换 “Finished” 消息,确认加密通道建立。

HTTP 请求发送 ​

过程 ​
  • 浏览器通过加密的 TLS 通道发送 HTTP 请求:

    GET / HTTP/1.1
    Host: www.xxx.com
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
    Accept: text/html,application/xhtml+xml,...
    Connection: keep-alive
  • 请求包括:

    • 方法:GET(获取资源)。
    • 路径:/(默认主页)。
    • 头信息:如 Host、User-Agent 等。
细节 ​
  • HTTP/1.1:支持 keep-alive,复用 TCP 连接。
  • HTTP/2:若服务器支持,可并行发送多个请求,效率更高。

服务器响应 ​

过程 ​

服务器接收请求后:

  1. 解密:用 TLS 解密请求。

  2. 路由:根据 Host 和路径(如 /)定位资源。

  3. 处理:运行后端逻辑(如查询数据库、生成动态页面)。

  4. 响应:构造 HTTP 响应:

    http
    HTTP/1.1 200 OK
    Content-Type: text/html
    Content-Length: 1234
    ...
    <html><body>Hello, World!</body></html>
细节 ​
  • 状态码:
    • 200 OK:成功。
    • 404 Not Found:资源不存在。
    • 301 Moved Permanently:重定向。
  • 时间:取决于服务器性能和业务逻辑复杂性(几毫秒到几秒)。

页面渲染 ​

过程 ​

浏览器将 HTML 渲染为可视页面:

  1. 构建 DOM 树:解析 HTML,生成文档对象模型。
  2. 加载 CSS:下载并解析 <link> 中的 CSS,构建 CSSOM(CSS 对象模型)。
  3. 渲染树:结合 DOM 和 CSSOM,确定页面结构和样式。
  4. 布局(Layout):计算元素位置和大小。
  5. 绘制(Painting):将像素渲染到屏幕。
  6. 执行 JavaScript:加载并运行 <script> 中的代码,可能修改 DOM。
细节 ​
  • 并行加载:浏览器同时下载 CSS、JS、图片等资源。
  • 阻塞:<script> 默认阻塞渲染,可用 async 或 defer 优化。
  • 时间:渲染时间从几十毫秒到几秒,视页面复杂性。

16、让用户访问旧网址时自动跳转到新网址该怎么做? ​

实现方法主要涉及 HTTP 重定向,可以从客户端、服务器端或 DNS 层面实现。

HTTP 重定向的基本原理 ​

HTTP 重定向通过服务器返回特定的状态码(3xx 系列)通知客户端跳转到新地址。常见的重定向状态码包括:

  • 301 Moved Permanently:永久重定向,告诉浏览器和搜索引擎新地址是永久替代旧地址。
  • 302 Found:临时重定向,适合短期跳转,不影响搜索引擎索引。
工作流程 ​
  1. 用户访问旧网址。
  2. 服务器返回重定向响应(如 301 或 302),包含新网址(在 Location 头中)。
  3. 浏览器接收响应,自动跳转到新网址。
  4. 用户看到新网址的内容。

17、手撕归并排序? ​

归并排序通过将数组递归地分成小块,分别排序后再合并,最终得到有序数组。

归并排序的原理 ​

归并排序的核心是“分”和“治”:

  • 分(Divide):将数组递归地分成两半,直到每个子数组只剩一个元素(天然有序)。
  • 治(Conquer):将两个有序子数组合并成一个更大的有序数组。
工作流程 ​
  1. 如果数组长度大于 1,将其分成两部分(通常取中间点)。
  2. 递归地对左右两部分进行归并排序。
  3. 将排序好的左右子数组合并为一个有序数组。
特点 ​
  • 稳定性:归并排序是稳定的排序算法(相同元素的相对顺序不变)。
  • 空间需求:需要额外的辅助空间来合并子数组。

实现步骤 ​

(1) 划分函数(mergeSort) ​
  • 输入:待排序数组、起始索引、结束索引。
  • 逻辑:
    • 如果起始索引 < 结束索引(子数组长度 > 1),计算中间点。
    • 递归调用自身,分别排序左半部分和右半部分。
    • 调用合并函数处理排序后的子数组。
(2) 合并函数(merge) ​
  • 输入:数组、左子数组范围(left 到 mid)、右子数组范围(mid+1 到 right)。
  • 逻辑:
    • 创建临时数组存储合并结果。
    • 比较左右子数组的元素,按顺序放入临时数组。
    • 将临时数组拷贝回原数组。

参考代码(C++) ​

c++
#include <iostream>
#include <vector>

// 合并两个有序子数组
void merge(std::vector<int>& arr, int left, int mid, int right) {
    // 计算左右子数组的大小
    int n1 = mid - left + 1;  // 左子数组长度
    int n2 = right - mid;     // 右子数组长度

    // 创建临时数组
    std::vector<int> leftArr(n1);
    std::vector<int> rightArr(n2);

    // 将数据拷贝到临时数组
    for (int i = 0; i < n1; i++) {
        leftArr[i] = arr[left + i];
    }
    for (int j = 0; j < n2; j++) {
        rightArr[j] = arr[mid + 1 + j];
    }

    // 合并两个子数组
    int i = 0;    // 左子数组索引
    int j = 0;    // 右子数组索引
    int k = left; // 原数组合并位置

    while (i < n1 && j < n2) {
        if (leftArr[i] <= rightArr[j]) { // 保持稳定性,使用 <=
            arr[k] = leftArr[i];
            i++;
        } else {
            arr[k] = rightArr[j];
            j++;
        }
        k++;
    }

    // 拷贝剩余元素(如果有)
    while (i < n1) {
        arr[k] = leftArr[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = rightArr[j];
        j++;
        k++;
    }
}

// 递归划分数组
void mergeSort(std::vector<int>& arr, int left, int right) {
    if (left < right) { // 子数组长度大于 1
        int mid = left + (right - left) / 2; // 防止溢出
        mergeSort(arr, left, mid);          // 排序左半部分
        mergeSort(arr, mid + 1, right);     // 排序右半部分
        merge(arr, left, mid, right);       // 合并结果
    }
}

// 打印数组
void printArray(const std::vector<int>& arr) {
    for (int num : arr) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> arr = {38, 27, 43, 3, 9, 82, 10};
    std::cout << "Original array: ";
    printArray(arr);

    mergeSort(arr, 0, arr.size() - 1);

    std::cout << "Sorted array: ";
    printArray(arr);

    return 0;
}