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

基础概念

  • 文档 (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 宕机。

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

分布式锁

在分布式系统中,当多个独立的进程(可能分布在不同的服务器上)需要竞争同一个资源时,Java 原生的 synchronizedReentrantLock 就失效了(因为它们只能锁住同一个 JVM 进程内的线程)。

这时候就需要 Redis 分布式锁:它相当于一个存放在公共区域的“令牌”,所有服务器都能看到它,谁先抢到谁就能操作资源。

核心原理

Redis 分布式锁最核心的逻辑其实只有一行命令: SET lock_key unique_id NX PX 30000

  • NX (Not Exists): 只有当 key 不存在时才设置成功。这保证了互斥性(只有一个抢锁成功)。

  • PX 30000: 自动过期时间(30秒)。这保证了安全性,防止某个服务器宕机后锁一直不释放,导致死锁。

  • unique_id: 每一个客户端生成的唯一 ID。这保证了解铃还须系铃人,防止 A 加的锁被 B 误删。

解决问题

实现一个稳健的分布式锁,必须处理以下三个经典问题:

释放锁的原子性

释放锁时,我们需要先判断“锁是不是我的”,再执行“删除”。这两步如果不是原子的,可能发生:判断完是我的,锁刚好过期了,别人抢到了,然后你执行了删除,把别人的锁删了。

  • 解决方案: 使用 Lua 脚本。Redis 执行 Lua 脚本是原子的。

锁过期了,任务还没跑完(续期问题)

如果你设了 30 秒过期,但业务逻辑跑了 40 秒,锁提前释放会导致并发问题。

  • 解决方案: 看门狗机制 (Watch Dog)。在锁即将过期时,开启一个后台线程自动延长过期时间。

Redis 集群宕机(高可用问题)

如果在主节点加了锁,还没同步到从节点主机就挂了,从节点变成主节点后,别人又能加锁成功。

  • 解决方案: Redlock (红锁) 算法。向过半数的独立 Redis 节点申请加锁,只有大多数都成功才算真正成功。

在 Java 开发中,我们很少手写 Lua 脚本去实现这些,而是直接使用 Redisson。它完美解决了上面提到的所有痛点。

Redisson 的优势

  1. 自动续期: 默认开启看门狗,每隔 10 秒检查一次,业务不结束锁不释放。

  2. 重入锁: 支持同一个线程多次加锁(类似 ReentrantLock)。

  3. 阻塞等待: 抢不到锁的线程会订阅信号量,而不是疯狂循环重试,节省 CPU。

性能优化

  • 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 并清理缓存。这样可以解耦业务代码,且保证最终一致。