早期数据库使用 层次模型(IMS)或 网状模型(IDMS),数据通过指针连接,查询需要编写复杂的导航式代码。关系模型由 Edgar Codd 在 1970 年提出,核心思想是:数据以二维表形式呈现,表与表之间通过共同字段关联,而不需要使用物理指针。
关系 (Relation) 即二维表,每一行是一个 元组 (Tuple),每一列是一个 属性 (Attribute)。关系模型建立在集合论基础上:关系是 笛卡尔积的子集。
键 (Key):
范式 (Normal Forms): 关系模型的规范化设计原则,减少数据冗余和异常:
-- 创建学生表 (关系)
CREATE TABLE students (
student_id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
major VARCHAR(50)
);
-- 创建课程表
CREATE TABLE courses (
course_id VARCHAR(20) PRIMARY KEY,
title VARCHAR(100) NOT NULL,
credit INT
);
-- 创建选课表 (关联表,外键引用)
CREATE TABLE enrollments (
student_id INT,
course_id VARCHAR(20),
grade INT,
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students(student_id),
FOREIGN KEY (course_id) REFERENCES courses(course_id)
);
-- 声明式查询:JOIN 关联表
SELECT s.name, c.title, e.grade
FROM students s
JOIN enrollments e ON s.student_id = e.student_id
JOIN courses c ON e.course_id = c.course_id
WHERE s.major = 'CS';
MySQL 早期默认使用 MyISAM 存储引擎,它不支持事务、外键,只支持表级锁,崩溃后需要修复。InnoDB 由 Innobase Oy 公司开发,2005 年被 Oracle 收购,成为 MySQL 5.5 的默认存储引擎。它提供了事务支持、行级锁、外键约束、MVCC 和崩溃恢复等关键特性。
页 (Page) 是 InnoDB 的 I/O 基本单位,默认 16KB。页内包含数据记录、页头、页尾,通过链表组织起来。
聚簇索引 (Clustered Index): InnoDB 将主键与数据行存储在一起,数据行的物理顺序按照主键排列。这意味着:
-- 查看表的存储引擎
SHOW TABLE STATUS WHERE Name = 'users';
-- 查看 InnoDB 状态
SHOW ENGINE INNODB STATUS\G
-- 设置缓冲池大小 (全局变量)
SET GLOBAL innodb_buffer_pool_size = 2147483648; -- 2GB
-- 查看缓冲池使用情况
SHOW GLOBAL STATUS LIKE 'innodb_buffer_pool_%';
数据库索引需要解决的核心问题是:如何在磁盘上高效地查找、插入、删除数据。磁盘 I/O 比内存慢几个数量级,所以索引结构的目标是最小化 I/O 次数。二叉查找树(BST)每层一次 I/O,查找 100 万条记录需要约 20 次 I/O(2^20 ≈ 100 万),而 B+ 树只需 3-4 次。
B+ 树的关键特征:
B+ 树与 B 树的对比:
| 特性 | B 树 | B+ 树 |
|---|---|---|
| 数据存储 | 所有节点都存储数据 | 仅叶子节点存储数据 |
| 内部节点 | 存储键和数据 | 仅存储键 (更多空间用于扇出) |
| 叶子节点链表 | 无 | 有,支持顺序访问 |
| 范围查询 | 需多次树遍历 | 叶子链表高效扫描 |
| 数据聚集 | 数据分散在树中 | 数据集中在叶子层 |
-- 创建索引
CREATE INDEX idx_name ON users(name);
-- 查看索引
SHOW INDEX FROM users;
-- 分析查询使用索引情况
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- 覆盖索引 (索引包含所有查询字段,无需回表)
CREATE INDEX idx_name_age ON users(name, age);
SELECT name, age FROM users WHERE name = '张三';
在数据库系统中,多个用户可能同时操作同一份数据,系统也可能在任意时刻崩溃。如果没有任何保障机制,可能会出现部分更新(如银行转账,扣款成功但加款失败)、数据不一致(如并发读取未提交的数据)等问题。
原子性: 通过 undo 日志 实现。事务执行过程中,所有修改记录到 undo log,如果事务失败,根据 undo log 将数据恢复到修改前的状态。
一致性: 通过数据库约束(主键、外键、唯一约束、触发器)和事务操作的正确性保证。
隔离性: 通过 锁机制 和 MVCC 实现,详见后面章节。
持久性: 通过 redo 日志 实现。事务提交前,所有修改先写入 redo log(顺序写,快于随机写),系统崩溃后通过 redo log 重新应用修改。
-- 银行转账事务: 原子性保证 A 扣款和 B 加款同时成功或同时失败
START TRANSACTION;
UPDATE accounts SET balance = balance - 100
WHERE account_id = 1001;
UPDATE accounts SET balance = balance + 100
WHERE account_id = 1002;
-- 如果两步都成功,提交事务 (持久化)
COMMIT;
-- 如果任何一步失败,回滚 (原子性回滚)
ROLLBACK;
当多个事务并发执行时,如果没有适当的隔离措施,会出现三种常见异常:
SQL 标准定义了四种隔离级别,对应解决的问题不同:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现机制 |
|---|---|---|---|---|
| 读未提交 (Read Uncommitted) | ⚠️ 可能 | ⚠️ 可能 | ⚠️ 可能 | 无锁,直接读 |
| 读已提交 (Read Committed) | ✅ 避免 | ⚠️ 可能 | ⚠️ 可能 | MVCC 快照读(普通SELECT不加锁),每次SELECT生成新ReadView |
| 可重复读 (Repeatable Read) | ✅ 避免 | ✅ 避免 | ⚠️ 可能 | MVCC 快照读,整个事务首次SELECT生成ReadView并复用 |
| 可串行化 (Serializable) | ✅ 避免 | ✅ 避免 | ✅ 避免 | 严格两阶段锁 (2PL) |
MySQL/InnoDB 默认隔离级别: 可重复读 (Repeatable Read)。RC和RR都使用MVCC快照读,普通SELECT不加锁。两者区别在于ReadView生成时机:RC每次SELECT都生成新ReadView,RR整个事务只在首次SELECT生成一次ReadView并复用——这是可重复读的根因。幻读解决需区分:快照读靠MVCC,当前读(如SELECT FOR UPDATE)靠Next-Key Lock。
-- 查看当前事务隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 演示脏读 (在 READ UNCOMMITTED 级别下)
-- 事务 A
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE accounts SET balance = 1000 WHERE id = 1;
-- 事务 B
SELECT balance FROM accounts WHERE id = 1; -- 读到 1000 (脏读!)
-- 事务 A 回滚
ROLLBACK;
-- 事务 B 再次读,变成 100 (数据无效)
数据库需要支持多个事务同时执行,但又要保证数据一致性。锁机制是并发控制的基础手段,通过让事务在访问数据前加锁,确保同一数据在同一时刻只能被一个事务修改。
锁类型:
死锁 (Deadlock): 两个事务互相持有对方需要的锁,导致互相等待。InnoDB 通过超时等待和死锁检测机制解决:
-- 查看当前锁信息
SHOW ENGINE INNODB STATUS\G
-- 手动加锁
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 加 X 锁
-- 设置锁超时时间
SET SESSION innodb_lock_wait_timeout = 10; -- 10 秒
-- 死锁示例 (两个事务)
-- 事务 A
START TRANSACTION;
UPDATE accounts SET balance = 200 WHERE id = 1;
UPDATE accounts SET balance = 300 WHERE id = 2;
-- 事务 B
START TRANSACTION;
UPDATE accounts SET balance = 400 WHERE id = 2;
UPDATE accounts SET balance = 500 WHERE id = 1;
-- InnoDB 会检测到死锁,回滚其中一个事务
在传统锁机制中,读操作和写操作互斥:读操作需要加 S 锁,写操作需要加 X 锁,二者不能共存。这导致并发性能受限。MVCC (Multi-Version Concurrency Control) 的设计目标是:读操作不阻塞写操作,写操作也不阻塞读操作,从而实现更高的并发度。
版本链 (Version Chain): InnoDB 在行数据中隐藏两个列:
一致性视图 (Consistent View): 事务开始时,系统分配一个 Read View,包含:
可见性规则: 对于一行数据,其 DB_TRX_ID 与视图比较:
-- 事务 A (读事务,使用一致性视图)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;
-- 读到的是事务 A 开始时的快照版本
-- 事务 B (写事务,修改数据)
START TRANSACTION;
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;
-- 事务 A 再次查询 (仍读旧版本,可重复读)
SELECT balance FROM accounts WHERE id = 1;
-- 结果与第一次一致,未读到 B 的修改
-- 如果事务 A 执行 SELECT ... FOR UPDATE
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 加锁读,会读到最新的已提交版本 (500)
数据库系统在运行时可能随时崩溃(断电、进程崩溃、操作系统故障),此时内存中的数据可能还未刷入磁盘。如果不做任何保护,提交的数据可能丢失,未提交的数据可能部分写入。
redo log (重做日志): 物理日志,记录页级别的修改。事务提交前,先写入 redo log,保证即使内存数据未刷盘,系统崩溃后也可通过 redo log 重做修改。redo log 的写入是顺序写,性能远高于随机写。
undo log (撤销日志): 逻辑日志,记录修改前的数据。用于事务回滚和 MVCC 版本管理。每条 undo 记录包含事务 ID 和修改前的值。
binlog (二进制日志): 逻辑日志,记录SQL 语句的修改操作。用于主从复制、时间点恢复和审计。binlog 在存储引擎之上,在事务提交后写入。
崩溃恢复流程:
# 查看 redo log 配置
SHOW GLOBAL VARIABLES LIKE 'innodb_log%';
# 查看 binlog 配置
SHOW GLOBAL VARIABLES LIKE 'log_bin%';
# 查看当前 binlog 文件列表
SHOW BINARY LOGS;
# 刷新 binlog (产生新文件)
FLUSH LOGS;
# 从 binlog 恢复数据
mysqlbinlog /var/lib/mysql/mysql-bin.000001 | mysql -u root -p
# 查看 redo log 使用情况
SHOW ENGINE INNODB STATUS\G
# 输出中包含: Log sequence number, Log flushed up to 等