mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1840 字
5 分钟
中间件面试题
2024-01-08

一、MySQL#

1.1 InnoDB 的 ACID 是如何保证的?#

ACID 是数据库事务的四大特性,InnoDB 通过以下机制保证:

原子性(Atomicity)#

原子性由 undo log 保证。当事务执行过程中发生错误或用户主动回滚时,InnoDB 会根据 undo log 的内容将数据回滚到事务开始前的状态。

-- 事务执行过程
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 记录 undo
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 记录 undo
COMMIT;
-- 执行失败时,根据 undo log 回滚
ROLLBACK; -- 从 undo log 恢复原始数据

undo log 是一种逻辑日志,记录了每个修改的反操作。它存放在系统表空间中(MySQL 5.6 之前)或独立的 undo 表空间中。

持久性(Durability)#

持久性由 redo log 保证。当事务提交时,事务期间的所有修改先写入 redo log(物理日志),然后再写入磁盘。系统崩溃后重启时,InnoDB 会读取 redo log 将未持久化的修改重新应用到磁盘。

事务提交流程:
1. 事务修改数据(写入 Buffer Pool)
2. 写入 redo log(prepare 状态)
3. 写入 binlog
4. redo log 标记为 commit
5. 后续由后台线程将脏页刷新到磁盘

redo log 是物理日志,记录的是「页的修改」而非 SQL 语句,因此恢复速度更快。

隔离性(Isolation)#

隔离性由锁机制MVCC(多版本并发控制)共同保证。InnoDB 实现了四种隔离级别:

隔离级别脏读不可重复读幻读
Read Uncommitted可能可能可能
Read Committed不可能可能可能
Repeatable Read(默认)不可能不可能可能
Serializable不可能不可能不可能

MVCC 原理

InnoDB 为每一行数据添加两个隐藏列:DB_TRX_ID(最近修改的事务 ID)和 DB_ROLL_PTR(指向 undo log 的指针)。读取时,根据事务的 Read View 判断哪个版本的数据对当前事务可见。

-- Read Committed:每次读取都生成新的 Read View
BEGIN;
SELECT ...; -- 生成 Read View A
-- 另一个事务提交
SELECT ...; -- 重新生成 Read View B,可以看到新提交的数据
-- Repeatable Read:事务开始时生成 Read View,整个事务期间复用
BEGIN;
SELECT ...; -- 生成 Read View A
-- 另一个事务提交
SELECT ...; -- 仍使用 Read View A,看不到新提交的数据

一致性(Consistency)#

一致性是 ACID 的最终目标。原子性、隔离性、持久性共同保证了一致性。只要这三个特性保证,一致性就能保证。

1.2 InnoDB 索引原理#

InnoDB 使用 B+ 树作为索引结构。B+ 树是多路平衡查找树,所有数据都存储在叶子节点,叶子节点之间通过链表连接。

B+ 树结构(简化):
[50, 100]
/ \
[10, 30] [70, 90, 120]
/ \ \
[5,8] [15,20] [60,65] [110,130]...
↓ ↓ ↓ ↓
链表连接所有叶子节点(范围查询友好)

主键索引(聚簇索引):叶子节点存储完整数据行

二级索引(非聚簇索引):叶子节点存储主键值,查询时需要回表

-- 二级索引查询过程
SELECT * FROM users WHERE name = 'Alice';
-- 1. 在 name 索引上找到 Alice 的主键 ID
-- 2. 用主键 ID 回表查询完整数据
-- 3. 返回结果

1.3 索引失效的场景#

-- 1. 索引列参与计算
SELECT * FROM orders WHERE YEAR(created_at) = 2024; -- 失效
-- 2. 索引列使用函数
SELECT * FROM users WHERE LEFT(name, 3) = 'Ali'; -- 失效
-- 3. 隐式类型转换
SELECT * FROM users WHERE phone = 13800138000; -- phone 是 varchar,失效
-- 4. LIKE 以 % 开头
SELECT * FROM users WHERE name LIKE '%lice'; -- 失效
SELECT * FROM users WHERE name LIKE 'Ali%'; -- 有效(可以用索引)
-- 5. OR 前后条件不一致
SELECT * FROM users WHERE id = 1 OR name = 'Alice'; -- 失效(id 有索引,name 无索引)

1.4 分库分表#

当单表数据量超过千万级别时,需要考虑分库分表:

垂直拆分:按业务模块拆分,不同模块放在不同库或表

水平拆分:将同一表的数据按某个维度(如 user_id、创建时间)拆分到多张表

-- 按 user_id 取模分表
user_id % 4 = 0 -> users_0
user_id % 4 = 1 -> users_1
user_id % 4 = 2 -> users_2
user_id % 4 = 3 -> users_3

分库分表的挑战:

  • 跨表查询(需要聚合)
  • 分页查询(需要归并排序)
  • 分布式事务(需要 2PC 或 TCC)

二、Redis#

2.1 Redis 的数据类型有哪些?#

Redis 支持 5 种基本数据类型和 3 种特殊类型:

类型底层结构常用场景
StringSDS(简单动态字符串)缓存、计数器、限流
HashDict + ziplist对象存储、购物车
ListQuickList消息队列、任务队列
SetDict + intset标签、好友关系、去重
Sorted Setziplist/Dict + skiplist排行榜、延迟队列

2.2 String 的底层实现#

Redis 3.2 之前使用简单动态字符串(SDS):

struct sdshdr {
int len; // 已使用长度
int free; // 剩余长度
char buf[]; // 字节数组
}

SDS 的优势:

  1. O(1) 获取字符串长度len 字段
  2. 避免缓冲区溢出:自动扩展空间
  3. 减少内存重分配:空间预分配和惰性释放

2.3 Hash 扩容#

Hash 采用渐进式 rehash解决大字典扩容时的性能问题:

// 字典结构
struct dict {
dictht ht[2]; // 两个哈希表
int rehashidx; // rehash 进度,-1 表示未进行
}
  1. ht[0] 存储真实数据,ht[1] 预分配空间
  2. 每次操作后,顺带迁移一个桶(rehashindex)
  3. 全部迁移完成后,ht[0] 和 ht[1] 交换

2.4 持久化机制#

Redis 支持两种持久化方式:

RDB(快照)

# 配置定时生成
save 60 1000 # 60 秒内 1000 次写操作则触发
save 300 10 # 5 分钟内 10 次写操作则触发
save 900 1 # 15 分钟内 1 次写操作则触发

RDB 是全量快照,适合备份和灾难恢复,但可能丢失最近一次快照后的数据。

AOF(追加日志)

appendonly yes
appendfsync everysec # 每秒同步,性能和安全性折中

AOF 是增量日志,记录每次写操作,可配置同步策略(always/everysec/no)。数据完整性高,但文件体积可能大于 RDB。

混合持久化(Redis 4.0+):

aof-use-rdb-preamble yes

混合模式下,AOF 文件以 RDB 格式开头,后续增量操作以 AOF 格式追加。兼具快速恢复和完整性。

2.5 缓存穿透、击穿、雪崩#

缓存穿透:查询不存在的数据穿过缓存直达数据库

解决方案:

  1. 布隆过滤器(Bloom Filter)
  2. 缓存空值(set null)

缓存击穿:热点 key 过期瞬间大量请求穿透到数据库

解决方案:

  1. 互斥锁(setnx + 过期时间)
  2. 永不过期(后台线程异步更新)

缓存雪崩:大量 key 同时过期或 Redis 宕机

解决方案:

  1. 过期时间加随机偏移
  2. Redis 集群高可用
  3. 限流降级

2.6 Redis 分布式锁#

-- 加锁(Lua 脚本保证原子性)
if redis.call('setnx', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) == 1 then
return 1
else
return 0
end
-- 解锁(Lua 脚本,检查 value 防止误删)
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end

分布式锁要点:

  1. 原子性:加锁和解锁必须是原子操作
  2. 防误删:只有持有锁的客户端才能释放锁
  3. TTL:设置过期时间防止死锁
  4. 可重入(可选):记录持有锁的客户端 ID 和计数

三、Elasticsearch#

3.1 倒排索引原理#

传统索引是「文档→词」的映射(正排索引),倒排索引是「词→文档」的映射:

文档1: "Go 是互联网时代的编程语言"
文档2: "Python 是胶水语言"
// 正排索引
文档1 → [Go, 是, 互联网, 时代, 的, 编程, 语言]
文档2 → [Python, 是, 胶水, 语言]
// 倒排索引
Go → [文档1]
是 → [文档1, 文档2]
互联网 → [文档1]
Python → [文档2]
...

倒排索引使得按词搜索非常高效,适合全文搜索场景。

3.2 分片和副本#

ES 通过分片实现横向扩展:

索引 → 分片 0, 分片 1, 分片 2
↓ ↓ ↓
副本 0 副本 1 副本 2(可配置)
  • 主分片:处理读写请求
  • 副本分片:提供数据冗余和读扩展
  • 副本分片通过主分片同步(document replication)

3.3 ES 与数据库对比#

维度MySQLElasticsearch
事务支持ACID 完整支持不支持事务
全文搜索LIKE 性能差专为搜索优化
写入性能较低高(近实时)
数据量级亿级十亿级
适用场景OLTP全文搜索、日志分析

四、Kafka#

4.1 Kafka 的特性#

Kafka 是分布式流处理平台,具有以下核心特性:

  1. 持久化:消息持久化到磁盘,顺序写入保证高吞吐
  2. 分区:按分区实现并行处理和水平扩展
  3. 副本:多副本冗余,保证高可用
  4. ** Exactly-once**:支持精确一次语义

4.2 分区与消费者组#

Topic: orders
分区 0: P0, P1, P2, P3... (消息)
分区 1: ...
分区 2: ...
消费者组 A: [C1, C2] → 各消费 2 个分区
消费者组 B: [C3, C4, C5] → 各消费 1 个分区

同一消费者组内的消费者不能消费同一分区(负载均衡),不同消费者组可以消费同一分区(广播)。

4.3 消息可靠性保证#

At Least Once:至少一次,可能重复但不会丢失

At Most Once:最多一次,可能丢失但不重复

Exactly Once:精确一次,通过幂等生产者 + 事务实现

// 幂等生产者配置
props.put("enable.idempotence", true);
// 事务配置
props.put("transactional.id", producerId);
producer.initTransactions();
producer.beginTransaction();
producer.send(record);
producer.commitTransaction();

4.4 顺序保证#

Kafka 只保证单个分区内的消息有序。要保证全局有序,只能使用单分区:

分区 0: [msg1, msg2, msg3] → 有序
分区 1: [msg_a, msg_b, msg_c] → 有序
但 msg1 和 msg_a 之间的顺序无法保证

五、参考#


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

中间件面试题
https://blog.souloss.com/posts/interview/midware/
作者
Souloss
发布于
2024-01-08
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时