一、MySQL
1.1 InnoDB 的 ACID 是如何保证的?
ACID 是数据库事务的四大特性,InnoDB 通过以下机制保证:
原子性(Atomicity)
原子性由 undo log 保证。当事务执行过程中发生错误或用户主动回滚时,InnoDB 会根据 undo log 的内容将数据回滚到事务开始前的状态。
-- 事务执行过程BEGIN;UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 记录 undoUPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 记录 undoCOMMIT;
-- 执行失败时,根据 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. 写入 binlog4. redo log 标记为 commit5. 后续由后台线程将脏页刷新到磁盘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 ViewBEGIN;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_0user_id % 4 = 1 -> users_1user_id % 4 = 2 -> users_2user_id % 4 = 3 -> users_3分库分表的挑战:
- 跨表查询(需要聚合)
- 分页查询(需要归并排序)
- 分布式事务(需要 2PC 或 TCC)
二、Redis
2.1 Redis 的数据类型有哪些?
Redis 支持 5 种基本数据类型和 3 种特殊类型:
| 类型 | 底层结构 | 常用场景 |
|---|---|---|
| String | SDS(简单动态字符串) | 缓存、计数器、限流 |
| Hash | Dict + ziplist | 对象存储、购物车 |
| List | QuickList | 消息队列、任务队列 |
| Set | Dict + intset | 标签、好友关系、去重 |
| Sorted Set | ziplist/Dict + skiplist | 排行榜、延迟队列 |
2.2 String 的底层实现
Redis 3.2 之前使用简单动态字符串(SDS):
struct sdshdr { int len; // 已使用长度 int free; // 剩余长度 char buf[]; // 字节数组}SDS 的优势:
- O(1) 获取字符串长度:
len字段 - 避免缓冲区溢出:自动扩展空间
- 减少内存重分配:空间预分配和惰性释放
2.3 Hash 扩容
Hash 采用渐进式 rehash解决大字典扩容时的性能问题:
// 字典结构struct dict { dictht ht[2]; // 两个哈希表 int rehashidx; // rehash 进度,-1 表示未进行}- ht[0] 存储真实数据,ht[1] 预分配空间
- 每次操作后,顺带迁移一个桶(rehashindex)
- 全部迁移完成后,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 yesappendfsync everysec # 每秒同步,性能和安全性折中AOF 是增量日志,记录每次写操作,可配置同步策略(always/everysec/no)。数据完整性高,但文件体积可能大于 RDB。
混合持久化(Redis 4.0+):
aof-use-rdb-preamble yes混合模式下,AOF 文件以 RDB 格式开头,后续增量操作以 AOF 格式追加。兼具快速恢复和完整性。
2.5 缓存穿透、击穿、雪崩
缓存穿透:查询不存在的数据穿过缓存直达数据库
解决方案:
- 布隆过滤器(Bloom Filter)
- 缓存空值(set null)
缓存击穿:热点 key 过期瞬间大量请求穿透到数据库
解决方案:
- 互斥锁(setnx + 过期时间)
- 永不过期(后台线程异步更新)
缓存雪崩:大量 key 同时过期或 Redis 宕机
解决方案:
- 过期时间加随机偏移
- Redis 集群高可用
- 限流降级
2.6 Redis 分布式锁
-- 加锁(Lua 脚本保证原子性)if redis.call('setnx', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) == 1 then return 1else return 0end
-- 解锁(Lua 脚本,检查 value 防止误删)if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1])else return 0end分布式锁要点:
- 原子性:加锁和解锁必须是原子操作
- 防误删:只有持有锁的客户端才能释放锁
- TTL:设置过期时间防止死锁
- 可重入(可选):记录持有锁的客户端 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 与数据库对比
| 维度 | MySQL | Elasticsearch |
|---|---|---|
| 事务支持 | ACID 完整支持 | 不支持事务 |
| 全文搜索 | LIKE 性能差 | 专为搜索优化 |
| 写入性能 | 较低 | 高(近实时) |
| 数据量级 | 亿级 | 十亿级 |
| 适用场景 | OLTP | 全文搜索、日志分析 |
四、Kafka
4.1 Kafka 的特性
Kafka 是分布式流处理平台,具有以下核心特性:
- 持久化:消息持久化到磁盘,顺序写入保证高吞吐
- 分区:按分区实现并行处理和水平扩展
- 副本:多副本冗余,保证高可用
- ** 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 之间的顺序无法保证五、参考
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






