1. 数据库基础
数据库基础概念
关系型 vs 非关系型(RDBMS vs NoSQL)
OLTP(在线事务处理) vs OLAP(在线分析处理)
行存储 vs 列存储
ACID 与事务
ACID四大特性
事务并发问题:
事务隔离级别:
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)
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底层原理:
聚簇索引与二级索引
聚簇索引 (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 处理后交给下一个。
多文档事务 (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) 选择策略:
范围分片 (Range-based): 根据键值范围切分。
优点: 范围查询快。
风险: 容易产生写热点(如自增 ID)。
哈希分片 (Hash-based): 根据键值的哈希值切分。
优点: 写入压力极其平衡。
缺点: 范围查询性能差(可能需要全分片扫描)。
避免热点问题: 好的分片键应具备 高基数(取值多)和 分布均匀 的特点。避免使用时间戳或自增序列作为单一部分的分片键。
4. Redis 深入
数据结构与底层实现
Redis 的对象系统(redisObject)与底层物理结构(Encoding)是解耦的。
跳表 (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):
惰性删除: 访问时才检查是否过期。若过期则删除。
定期删除: 每隔一段时间抽取一部分 Key 检查并删除。
内存淘汰策略 (Eviction Policy):
LRU (Least Recently Used): 淘汰最久未使用的。
LFU (Least Frequently Used): 淘汰使用频率最低的(4.0 引入)。
Random / TTL: 随机淘汰或淘汰即将过期的。
高可用架构
主从复制: 解决单点故障,读写分离。全量同步(RDB)+ 增量同步(命令流)。
哨兵 (Sentinel): 监控主从状态。主库故障时,自动完成主从切换(Failover),保证高可用。
Redis Cluster (分片): 真正的分布式方案。采用 哈希槽 (Hash Slot) 机制(共有 16384 个槽),通过逻辑分片实现无中心化水平扩容。
缓存三大问题
分布式锁
SETNX (Set if Not Exists): 基础方案。需配合
EXPIRE使用,但要注意原子性(使用 Lua 脚本)。RedLock (红锁): 为了解决单机 Redis 宕机导致锁失效提出的算法。
原理: 在 N 个独立的 Redis 实例上获取锁,半数以上成功才算获取。
争议: 分布式专家 Martin Kleppmann 曾质疑其安全性(受系统时钟漂移影响)。在工程实践中,若追求极致可靠,建议使用 ZooKeeper。
性能优化
Pipeline (管道): 将多条命令打包一次性发送,大幅减少网络 RTT(往返延迟)。
避免大 Key: 超过 10KB 的 String 或包含万级元素的集合。
后果: 阻塞单线程,引发集群重分配倾斜,导致网络拥塞。
禁用危险命令: 如
KEYS *(全量遍历,会阻塞 Redis)。建议改用SCAN。连接池: 避免频繁创建和销毁 TCP 连接。
5. 系统设计结合
如何设计一个高并发系统
设计高并发系统通常遵循“分而治之”和“空间换时间”的原则。
核心分层架构:
缓存层 (Caching):
本地缓存 (Guava/Caffeine): 毫秒级响应,减少网络开销。
分布式缓存 (Redis): 支撑海量并发读。
读写分离 (Read/Write Splitting):
通过 MySQL 主从架构,主库负责写,从库负责读。
实现方式: 代理层(如 MyCat、ShardingSphere)或应用层配置双数据源。
分库分表 (Sharding):
解决单机磁盘 IO 和 CPU 瓶颈。
垂直拆分: 按业务解耦,减少表关联。
水平拆分: 数据分片,提升吞吐量。
异步化 (Asynchrony):
利用消息队列(Kafka/RocketMQ)削峰填谷,将非核心链路(如发短信、加积分)异步处理。
数据一致性问题
在分布式环境下,CAP 定理告诉我们无法同时满足强一致性和高可用性。
一致性模型:
强一致性: 更新后,任何后续访问都能读到最新值(性能损耗巨大)。
最终一致性: 允许短时间内数据不一致,但经过一段时间后,数据达到一致(互联网主流方案)。
分布式事务解决方案:
2PC (Two-Phase Commit):
准备阶段 + 提交阶段。存在同步阻塞和单点故障问题。
3PC (Three-Phase Commit):
引入 CanCommit 阶段和超时机制,减轻了阻塞,但仍无法完美解决脑裂问题。
Saga 模式:
将长事务拆分为多个本地事务。如果某个环节失败,则执行补偿操作(正向操作的反向逻辑)。适合长流程业务。
TCC (Try-Confirm-Cancel):
侵入性强,需要业务逻辑实现资源预留、确认和撤销。
本地消息表 / 事务消息:
利用 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 并清理缓存。这样可以解耦业务代码,且保证最终一致。