1. 数据库基础

数据库基础概念

关系型 vs 非关系型(RDBMS vs NoSQL)

维度

RDBMS (MySQL/PostgreSQL)

NoSQL (Redis/MongoDB/Neo4j)

核心数据结构

二维表(B+树存储)

KV、BSON文档、跳表、LSM-Tree

Schema

强 Schema:修改字段需 DDL 锁表

Schemaless:字段随存随取,开发灵活

一致性

追求强一致性(ACID)

追求最终一致性(BASE/CAP理论)

水平扩展性

难(需分库分表)

易(原生支持分片 Sharding)

OLTP(在线事务处理) vs OLAP(在线分析处理)

类型

场景

OLTP

在线交易(高并发、短事务),侧重低延迟、高并发、写强一致性。主要瓶颈在磁盘 I/O。

OLAP

数据分析(复杂查询、大数据),侧重高吞吐量、复杂聚合查询。利用 MPP(大规模并行处理)架构。

行存储 vs 列存储

类型

特点

场景

优点

行存

一行数据一起存(MySQL)

适合写多

由于 CPU Cache Line 的预读机制,SELECT * 或单行更新的效率极高

列存

一列数据一起存(ClickHouse)

适合分析

极高的压缩比(同类数据压缩率可达 10:1)和向量化执行(利用 SIMD 指令单指令多数据),在 SUM/AVG 等聚合计算时性能比行存高出百倍

ACID 与事务

ACID四大特性

特性

含义

实现

A

原子性(要么全成功,要么全失败)

靠 Undo Log 实现。记录逆向操作,失败则“回放”以恢复。

C

一致性(数据正确)

终极目标。靠 A、I、D 以及约束(主键、外键、触发器)共同保证。

I

隔离性(互不干扰)

靠 锁机制 + MVCC 实现。

D

持久性(数据不丢)

靠 Redo Log 实现。采用 WAL (Write-Ahead Logging) 技术,数据先写日志再刷盘。

事务并发问题:

问题

说明

脏读

读到未提交数据

不可重复读

同一数据两次不一样

值变

幻读

行数变化

行变

事务隔离级别:

隔离级别

脏读

不可重复读

幻读

实现机制

Read Uncommitted

几乎不加锁

Read Committed

语句级 Read View

Repeatable Read

✅*

事务级 Read View + Gap Lock

Serializable

悲观锁 (读写全排队)

MySQL 的 RR 级别在很大程度上通过 Next-Key Lock 解决了幻读,但并没有完全消除(如:事务 A 插入后事务 B 成功更新该行,事务 A 再次查询可见)。

MVCC(多版本并发控制)

本质是读历史版本,避免加锁

核心机制:

  • 版本链:每行数据有两个隐藏列 trx_id(最后修改事务 ID)和 roll_pointer(指向 Undo Log 的指针)。

  • Read View (快照):包含当前活跃事务列表。

  • 可见性判断:读取时对比 trx_id。如果该 ID 在活跃列表中,说明未提交,则追溯 Undo Log 链寻找旧版本,实现“非阻塞读”。

优点:

  • 提高并发

  • 减少锁竞争

锁机制

类型

说明

共享锁

读锁

排他锁

写锁

粒度

  • 行锁(高并发)

  • 表锁(低并发)

乐观锁 vs 悲观锁

类型

思想

乐观锁

不锁,冲突再处理(版本号)

悲观锁

先锁再操作

机制

  • 意向锁 (IS/IX):表级锁。解决“加表锁时需全表扫描查行锁”的问题,将 O(n) 复杂度降低为 O(1)。

  • 间隙锁 (Gap Lock):锁定一个区间,禁止在该区间插入数据。

  • Next-Key Lock:行锁 + 间隙锁。是 InnoDB 默认的锁算法。

  • 悲观锁 vs 乐观锁

    • 悲观锁SELECT ... FOR UPDATE。适用于写竞争激烈的场景。

    • 乐观锁WHERE version = v+1。适用于读多写少,基于 CAS 思想。

死锁产生原因 & 如何避免

原因:

  • 资源循环等待

解决:

  • 按顺序加锁

  • 减少事务时间

索引基础

索引原理(B+树 vs Hash)

类型

特点

原理

B+树

支持范围查询

非叶子节点不存数据,仅存键值。这使得节点分叉多(Fan-out 大),3 层高度即可支撑千万级数据,磁盘 I/O 极少。叶子节点通过双向链表串联,完美支持 BETWEENORDER BY

Hash

只支持等值

O(1) 查找,但不支持范围检索、无法排序、存在 Hash 冲突。

MySQL使用的是B+树

聚簇索引 vs 非聚簇索引

  • 聚簇索引 (Clustered Index):叶子节点就是数据本身。MySQL 主键默认即是,全表只有一套。

  • 非聚簇索引 (Secondary Index):叶子节点存的是主键值

  • 回表 (Look-up):通过非聚簇索引查到主键,再回主键索引查整行。

  • 覆盖索引 (Covering Index):索引字段包含了 SELECT 所需的所有字段。无需回表,是性能优化的“银弹”。

索引失效场景

  • 左模糊LIKE '%abc' 导致无法利用 B+ 树的字符序。

  • 函数/表达式WHERE YEAR(date) = 2024。函数改变了值的逻辑顺序,索引失效。

  • 隐式转换:字符串字段传入数字,MySQL 内部调用 CAST,同函数操作。

  • 最左前缀:复合索引 (A, B, C)。如果跳过 A 查 B,因为 B 仅在 A 相等的范围内有序,全局看是乱序的。

SQL能力

2. MySQL 深入

存储引擎(InnoDB vs MyISAM)

在 MySQL 5.5 之后,InnoDB 成为默认存储引擎。两者的差异不仅是功能,更是底层设计哲学的不同。

特性

InnoDB

MyISAM

事务支持

支持 (ACID)

不支持

锁定粒度

行级锁(适合高并发)

表级锁(并发受限)

外键

支持

不支持

可靠性

具有崩溃恢复能力 (Redo Log)

崩溃后可能损坏索引文件

存储结构

索引与数据绑定(聚簇)

索引与数据分离(非聚簇)

InnoDB底层原理:

聚簇索引与二级索引

  • 聚簇索引 (Clustered Index): 数据行实际存储在 B+ 树的叶子节点中。一张表有且仅有一个聚簇索引(通常是主键)。

  • 二级索引 (Secondary Index): 叶子节点存储的是主键值

  • 回表 (Look-up): 当通过二级索引查询非索引字段时,MySQL 先找到主键值,再回到聚簇索引中查找完整行记录。

内存管理机制

  • 页结构 (Page): InnoDB 磁盘管理的最小单位,默认 16KB

  • Buffer Pool: 极其重要的缓存层。

    • 读取数据时,先看 Buffer Pool 是否有命中,没有则从磁盘加载并缓存。

    • 修改数据时,先修改 Buffer Pool 中的页,并标记为“脏页”,由后台线程异步刷盘。

  • Change Buffer: 针对非唯一二级索引的写优化。如果目标页不在内存中,先将修改记录在 Change Buffer,等未来该页被读取时再进行 Merge。

索引深入:B+ 树与优化

B+ 树结构细节

相比 B 树,B+ 树非叶子节点不存数据,仅存键值。这使得每一页能容纳更多索引项,降低树的高度(通常 3-4 层即可支撑千万级数据),减少磁盘 I/O

核心原则与技术

  • 最左前缀原则: 联合索引 (a, b, c),查询条件必须从左往右匹配。如 where a=1 and c=3 只能用到 a 部分的索引。

  • 索引下推 (ICP): 在索引遍历过程中,对索引中包含的字段先做判断,过滤掉不满足条件的行,减少回表次数。

  • 覆盖索引: Select 的字段全部在索引树中,无需回表。

SQL优化

EXPLAIN 执行计划

通过 EXPLAIN 分析 SQL 是调优的第一步:

  • type: 性能排序:system > const > eq_ref > ref > range > index > ALL (全表扫描,需优化)。

  • key: 实际使用的索引。

  • rows: 预估扫描的行数,越小越好。

  • extra: Using index:使用了覆盖索引。

    • Using filesort:使用了外部排序(需优化)。

    • Using temporary:使用了临时表。

深分页优化

LIMIT 1000000, 10 会导致 MySQL 扫描前一百万行并丢弃。

  • 优化方案: 延迟关联。先通过覆盖索引找出主键 ID,再关联原表获取数据。

事务与MVCC

三大日志

  • Redo Log (重做日志): 物理日志。保证事务的持久性。在崩溃时恢复未刷盘的脏页。

  • Undo Log (回滚日志): 逻辑日志。保证事务的原子性,并支撑 MVCC

  • Binlog (归档日志): Server 层日志。用于主从复制和数据恢复。

MVCC (多版本并发控制)

MVCC 实现了“读-写”并行,避免了大部分加锁操作。

  • 实现原理: 依靠每行记录后的隐藏列(DB_TRX_ID, DB_ROLL_PTR)和 Undo Log 版本链

  • Read View: 事务启动时生成的快照。

    • RC 级别: 每次查询都生成新 Read View。

    • RR 级别: 仅第一次查询生成 Read View,保证了可重复读。

锁机制

锁类型

  • Record Lock (行锁): 锁住单条索引记录。

  • Gap Lock (间隙锁): 锁住索引记录之间的间隙,不包括记录本身。

  • Next-Key Lock: 行锁 + 间隙锁。解决幻读的核心。

为什么产生幻读? 当一个事务在读取某个范围内的记录时,另一个事务又在该范围内插入了新记录,导致前一个事务再次读取时出现了“幻影”行。InnoDB 在 RR 级别下通过 Next-Key Lock 锁住范围,防止其他事务插入,从而杜绝幻读。

主从复制 & 高可用

复制流程

  • 主库记录变更到 Binlog

  • 从库 I/O 线程读取主库 Binlog 并写入自身的 Relay Log (中继日志)

  • 从库 SQL 线程重放 Relay Log。

Binlog 格式

  • Statement: 记录 SQL 原文。优点:日志量小。缺点:可能导致函数执行不一致(如 NOW())。

  • Row: 记录行的实际变更。优点:最安全,准确。缺点:日志量巨大。

  • Mixed: 混合模式,由系统判断。

分库分表

当单表突破 2000万行 或磁盘 IO 达到瓶颈时,需要考虑拆分。

  • 垂直拆分: 按业务分。如 User 表和 Order 表存入不同数据库。

  • 水平拆分: 按行拆。将同一张表的数据通过某种规则分散到多个库/表。

  • 分片策略: * Hash: 数据分布均匀,但扩容迁移复杂。

    • Range: 扩容简单,但容易产生热点数据(如最近一月的订单全在最后一个库)。

  • 分布式 ID: * 雪花算法 (Snowflake): 推荐方案。时间戳 + 机器 ID + 序列号,生成全局唯一、趋势递增的 ID。

3. MongoDB 深入(NoSQL核心)

基础概念

  • 文档 (Document): MongoDB 的最小数据单元,类似于 JSON 对象,但以 BSON(Binary JSON)格式存储。

  • 集合 (Collection): 一组文档的集合,相当于关系型数据库的“表”,但通常是 Schema-less(无模式)的。

  • BSON 的优势: * 更丰富的数据类型: 支持 Date、BinData(二进制)、Decimal128 等。

    • 遍历速度快: 预留了长度字段,解析时无需像 JSON 那样扫描整个字符串。

数据模型设计

MongoDB 设计的核心思想是:为应用程序的查询路径设计数据模型。

  • 嵌入 (Embedding): 将相关数据保存在同一个文档中。

    • 优点: 单次 IO 即可获取完整数据(高读性能),保证了原子性。

    • 适用: 数据间存在“一对一”或“一对少量”关系(如用户地址)。

  • 引用 (Referencing): 通过 ID 关联不同集合。

    • 优点: 减少数据冗余,适合大批量数据的更新。

    • 适用: “一对多”且“多”的一端数据量巨大,或多对多关系。

  • 反范式设计: 为了查询性能,允许适度的数据冗余。

索引优化

MongoDB 的索引同样基于 B-tree 结构(注意:MySQL 聚簇索引通常是 B+ 树)。

  • 多键索引 (Multikey Index): 针对数组字段创建索引,MongoDB 会为数组中的每个元素创建一个索引项。

  • TTL 索引: 设置过期时间,自动删除过期的文档(常用于日志、验证码、临时会话)。

  • 地理位置索引 (Geospatial): 支持 2dsphere(球体坐标)和 2d(平面坐标),支持 $near$geoWithin 查询。

查询与聚合 (Aggregation Pipeline)

聚合管道是 MongoDB 处理复杂业务逻辑的神器,它类似 Unix 的管道命令,数据流经过一个 Stage 处理后交给下一个。

阶段 (Stage)

功能描述

对应 SQL 概念

$match

过滤文档

WHERE / HAVING

$group

将文档分组并进行聚合计算

GROUP BY + 聚合函数

$project

修改文档结构(增加/删除字段)

SELECT 指定列

$sort

排序

ORDER BY

$lookup

左外连接其他集合

LEFT OUTER JOIN

$unwind

将数组拆分为多条独立的文档

(无直接对应)

多文档事务 (Mongo 4.x+)

MongoDB 虽然是 NoSQL,但在 4.0 以后支持了 ACID 事务

  • 范围: 支持跨集合、跨文档的事务。

  • 限制: 1. 必须在副本集分片集群环境下运行。 2. 事务执行时间有默认限制(通常为 60 秒),超过则自动中止。 3. 不建议在事务中进行大规模的数据修改,以免阻塞写操作。

副本集 (Replica Set):高可用核心

副本集是一组维护相同数据集的 mongod 进程。

  • Primary (主节点): 负责所有的写操作。

  • Secondary (从节点): 通过同步主节点的 oplog(操作日志)来复制数据。

  • 自动故障转移: 当主节点宕机,从节点会通过选举(心跳检测)产生新的 Primary。

  • 读写分离: 客户端可以通过 Read Preference 设置从 Secondary 读取(适合报表、查询)。

分片 (Sharding)

当数据量达到 TB 级别,单机性能触达天花板时,分片是终极手段。

  • 架构组件:

    • Shard: 实际存储数据的节点。

    • Config Servers: 存储元数据和分片映射信息。

    • Mongos: 路由层,客户端直接连接 Mongos,它负责将请求转发到对应分片。

分片键 (Shard Key) 选择策略:

  1. 范围分片 (Range-based): 根据键值范围切分。

    • 优点: 范围查询快。

    • 风险: 容易产生写热点(如自增 ID)。

  2. 哈希分片 (Hash-based): 根据键值的哈希值切分。

    • 优点: 写入压力极其平衡。

    • 缺点: 范围查询性能差(可能需要全分片扫描)。

避免热点问题: 好的分片键应具备 高基数(取值多)和 分布均匀 的特点。避免使用时间戳或自增序列作为单一部分的分片键。

4. Redis 深入

数据结构与底层实现

Redis 的对象系统(redisObject)与底层物理结构(Encoding)是解耦的。

对象类型

常用底层实现 (Encoding)

关键技术点

String

SDS (Simple Dynamic String)

预分配冗余空间,O(1) 获取长度,二进制安全,杜绝缓冲区溢出。

List

quicklist

双向链表 + 压缩列表的结合体,兼顾空间效率与插入性能。

Hash

ziplist / dict

小量数据用压缩列表;大量数据转为 dict(哈希表)。

Set

intset / dict

纯整数且量小时用整数集合。

ZSet

ziplist / skiplist

跳表 (SkipList):O(log N) 复杂度,通过多级索引实现类似二分查找。

$unwind

将数组拆分为多条独立的文档

(无直接对应)

跳表 (SkipList) 细节

跳表是 ZSet 的灵魂。它在链表的基础上增加了多级索引,每一层索引跳过部分节点。相比平衡树,跳表在并发竞争下无需旋转操作,实现更简单且范围查询效率极高。

持久化机制:RDB vs AOF

为了保证数据不因宕机丢失,Redis 提供了两种互补的持久化方案。

  • RDB (Redis DataBase):

    • 原理: 定期将内存快照写入磁盘(Binary Snapshot)。

    • 优点: 恢复速度极快,适合备份。

    • 缺点: 容易丢失两次快照之间的数据;fork 子进程进行全量快照时属于重量级操作。

  • AOF (Append Only File):

    • 原理: 记录每一条写命令到日志中。

    • 策略: always (实时), everysec (每秒, 性能与安全的折中), no (交给 OS)。

    • AOF 重写: 自动瘦身,将多条命令合并为等效的一条(如 INCR 100次 合并为 SET 100)。

最佳实践: 生产环境通常采用 混合持久化(RDB 作为全量基准 + AOF 作为增量补丁)。

过期删除与内存淘汰

当内存触达上限或 Key 到期时,Redis 如何处理?

  • 过期删除策略 (Expired Keys):

    1. 惰性删除: 访问时才检查是否过期。若过期则删除。

    2. 定期删除: 每隔一段时间抽取一部分 Key 检查并删除。

  • 内存淘汰策略 (Eviction Policy):

    • LRU (Least Recently Used): 淘汰最久未使用的。

    • LFU (Least Frequently Used): 淘汰使用频率最低的(4.0 引入)。

    • Random / TTL: 随机淘汰或淘汰即将过期的。

高可用架构

  • 主从复制: 解决单点故障,读写分离。全量同步(RDB)+ 增量同步(命令流)。

  • 哨兵 (Sentinel): 监控主从状态。主库故障时,自动完成主从切换(Failover),保证高可用。

  • Redis Cluster (分片): 真正的分布式方案。采用 哈希槽 (Hash Slot) 机制(共有 16384 个槽),通过逻辑分片实现无中心化水平扩容。

缓存三大问题

问题

定义

解决方案

缓存穿透

查询不存在的数据,请求直达数据库。

布隆过滤器 (Bloom Filter)、缓存空对象。

缓存击穿

热点 Key 突然失效,海量请求瞬间压垮数据库。

互斥锁 (Mutex)、逻辑过期(不设置物理过期)。

缓存雪崩

大量 Key 同时失效或 Redis 宕机。

随机失效时间、多级缓存、熔断降级。

分布式锁

  • SETNX (Set if Not Exists): 基础方案。需配合 EXPIRE 使用,但要注意原子性(使用 Lua 脚本)。

  • RedLock (红锁): 为了解决单机 Redis 宕机导致锁失效提出的算法。

    • 原理: 在 N 个独立的 Redis 实例上获取锁,半数以上成功才算获取。

    • 争议: 分布式专家 Martin Kleppmann 曾质疑其安全性(受系统时钟漂移影响)。在工程实践中,若追求极致可靠,建议使用 ZooKeeper

性能优化

  • Pipeline (管道): 将多条命令打包一次性发送,大幅减少网络 RTT(往返延迟)。

  • 避免大 Key: 超过 10KB 的 String 或包含万级元素的集合。

    • 后果: 阻塞单线程,引发集群重分配倾斜,导致网络拥塞。

  • 禁用危险命令:KEYS *(全量遍历,会阻塞 Redis)。建议改用 SCAN

  • 连接池: 避免频繁创建和销毁 TCP 连接。

5. 系统设计结合

如何设计一个高并发系统

设计高并发系统通常遵循“分而治之”和“空间换时间”的原则。

核心分层架构:

  1. 缓存层 (Caching):

    • 本地缓存 (Guava/Caffeine): 毫秒级响应,减少网络开销。

    • 分布式缓存 (Redis): 支撑海量并发读。

  2. 读写分离 (Read/Write Splitting):

    • 通过 MySQL 主从架构,主库负责写,从库负责读。

    • 实现方式: 代理层(如 MyCat、ShardingSphere)或应用层配置双数据源。

  3. 分库分表 (Sharding):

    • 解决单机磁盘 IO 和 CPU 瓶颈。

    • 垂直拆分: 按业务解耦,减少表关联。

    • 水平拆分: 数据分片,提升吞吐量。

  4. 异步化 (Asynchrony):

    • 利用消息队列(Kafka/RocketMQ)削峰填谷,将非核心链路(如发短信、加积分)异步处理。

数据一致性问题

在分布式环境下,CAP 定理告诉我们无法同时满足强一致性和高可用性。

一致性模型:

  • 强一致性: 更新后,任何后续访问都能读到最新值(性能损耗巨大)。

  • 最终一致性: 允许短时间内数据不一致,但经过一段时间后,数据达到一致(互联网主流方案)。

分布式事务解决方案:

  1. 2PC (Two-Phase Commit):

    • 准备阶段 + 提交阶段。存在同步阻塞单点故障问题。

  2. 3PC (Three-Phase Commit):

    • 引入 CanCommit 阶段和超时机制,减轻了阻塞,但仍无法完美解决脑裂问题。

  3. Saga 模式:

    • 将长事务拆分为多个本地事务。如果某个环节失败,则执行补偿操作(正向操作的反向逻辑)。适合长流程业务。

  4. TCC (Try-Confirm-Cancel):

    • 侵入性强,需要业务逻辑实现资源预留、确认和撤销。

  5. 本地消息表 / 事务消息:

    • 利用 MQ 的半消息机制,保证本地事务与消息发送的原子性,实现最终一致性

常见混合架构

现代系统极少只用一种数据库,通常是根据数据特性组合使用。

MySQL + Redis (缓存架构)

  • 场景: 大多数互联网应用。

  • 策略: Cache Aside Pattern(旁路缓存)。

    • 读:先读 Redis,未命中读 MySQL 并回写 Redis。

    • 写:先更新 MySQL,再删除 Redis(避免双写不一致)。

MySQL + MongoDB (多模型存储)

  • 场景: 社交平台、电商商品系统。

  • 分工:

    • MySQL: 存储核心账户、订单、金额等对事务要求极高的数据。

    • MongoDB: 存储非结构化数据,如商品属性扩展、用户评论、点赞列表。

CQRS (命令查询职责分离)

  • 核心思想: 将“写(Command)”和“读(Query)”模型完全分离。

  • 实现:

    • 写操作进入 MySQL,通过 Binlog 或消息队列实时同步到 Elasticsearch 或另一个针对查询优化的数据库(如 ClickHouse)。

    • 优势: 读写互不干扰,可以针对读请求进行极端的反范式优化。

缓存与 DB 的一致性

先更数据库再删缓存,如果删除失败怎么办?

  • 方案 A:重试机制。 将删除失败的 Key 放入消息队列。

  • 方案 B:订阅 Binlog。 使用 Canal 监听 MySQL 变更,由 Canal 异步消费 Binlog 并清理缓存。这样可以解耦业务代码,且保证最终一致。