数据库心智模型:页与缓存、事务、索引、复制与恢复

一句话模型

数据库的多数故障来自假设与实际的偏差:以为有索引、以为事务隔离、以为从库一致、以为备份可恢复。

核心概念

1. 页、缓冲池与持久化

数据库以页(page)为读写单位,InnoDB 默认 16KB,PostgreSQL 默认 8KB。查询先经过缓冲池(buffer pool / shared buffers),命中则直接读内存,未命中才从磁盘读入。修改先在内存页上完成,同时写 WAL/redo 日志,再由后台线程刷脏页、做 checkpoint。这个设计把随机写变成顺序写,是数据库吞吐的基础。

WAL 的意义是“先写日志再写数据”:崩溃后可以用日志重放未落盘的修改。边界在于,WAL 保证的是已提交事务的持久性,不保护磁盘损坏、误删和逻辑错误;fsync、刷盘策略和存储缓存都会影响真正的持久性。把 synchronous_commit 或 innodb_flush_log_at_trx_commit 调低,是在用持久性换延迟。

失败时的表现包括:脏页刷不过来导致写入抖动、checkpoint 尖峰、WAL 或 redo 目录被写满、崩溃恢复时间过长。看到“偶发提交变慢”,要区分是锁等待、WAL 刷盘还是存储 IO 饱和,而不是笼统归因于“数据库慢”。

2. 事务与隔离级别

事务把多条语句打包成原子、一致、隔离、持久的操作。隔离级别定义并发事务之间能看到什么:Read Committed 每条语句取新快照,Repeatable Read 在事务内保持快照,Serializable 通过锁或冲突检测追求可串行化。MVCC 让读写不互相阻塞,代价是旧版本堆积和清理压力。

隔离级别不是“越高越安全”这么简单。Read Committed 可能出现不可重复读;Repeatable Read 在不同数据库里语义不同,MySQL 的 RR 还会用间隙锁减少幻读,但仍可能出现写偏斜;Serializable 需要应用处理序列化失败并重试。边界是:数据库只能约束它看得到的数据,跨服务、跨库的业务不变量需要分布式事务、Saga 或 outbox 等机制。

失败时的表现是锁等待、死锁、序列化失败、长事务导致 MVCC 膨胀,以及“读取到不存在的组合”这类业务异常。排查时要看 pg_locks、information_schema.innodb_trx、等待事件和事务持有时长,而不是只看 SQL 文本。

BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;
COMMIT;

3. 索引与执行计划

索引是用额外空间换取访问路径的数据结构。B+ 树索引适合等值、范围和排序,联合索引遵循最左前缀,覆盖索引可以避免回表。查询优化器根据统计信息估算选择率,比较全表扫描和索引扫描的成本,生成执行计划。执行计划才是数据库实际怎么读数据的答案。

“有索引”不等于“会走索引”。对列做函数或类型转换、隐式字符集转换、选择率过低、统计信息过期、联合索引跳过最左列,都会让优化器放弃索引。边界在于:优化器是基于统计的估算器,不是证明器;数据分布变化、参数化查询和 plan cache 都可能让同一个 SQL 在不同时间得到不同计划。

失败时的表现是全表扫描、扫描行数远超返回行数、临时表和文件排序、慢查询日志里同一条 SQL 时快时慢。排查顺序是看执行计划、实际行数与估算行数、索引定义和统计信息,再决定加索引、改 SQL 还是调整 schema。

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC LIMIT 20;

4. 复制与一致性

复制把主库的日志(MySQL binlog、PostgreSQL WAL)传到从库并重放。异步复制延迟最低但可能丢数据;半同步至少一个从库确认;同步复制保证多数派落盘但增加写延迟。复制解决的是可用性、读扩展和故障切换,不解决备份和逻辑错误。

一致性是分层的:复制能保证最终看到相同数据,但不保证读请求立刻看到刚写入的值。异步从库上的“读到旧数据”是设计使然,不是 bug。需要读己之写时,可以路由到主库、等待 GTID/LSN、使用会话粘滞,或接受业务层的延迟。边界在于:故障切换时还要处理复制延迟、脑裂和旧主库重新加入。

失败时的表现是复制延迟上涨、relay log 堆积、从库 SQL 线程报错停止、主从数据不一致、切换后数据回退。看延迟不能只看 Seconds_Behind_Master,还要看 GTID 差距、WAL 位点和实际重放速度。

5. 连接与并发

每个连接都消耗内存、文件描述符和服务端线程或进程资源。连接池复用连接,避免频繁握手和认证;服务端 max_connections 限制上限;PgBouncer、ProxySQL 在应用和数据库之间做多路复用。连接数不是并发能力的直接度量,真正决定吞吐的是 CPU、IO、锁和内存。

连接池配置要小于数据库上限,并给运维和复制留余量。池太小会让请求排队,池太大则会把压力直接传给数据库,引发上下文切换、锁竞争和内存膨胀。边界是:每个应用实例都有自己的池,实例扩缩容会成倍放大连接数;事务模式与会话模式的多路复用语义也不同。

失败时的表现是 too many connections、连接超时、池等待、应用线程被占满,或者数据库 CPU 不高但吞吐上不去。排查要同时看应用池指标、数据库连接数和等待事件,不能只调大 max_connections。

6. 备份与恢复

备份分为逻辑备份(导出 SQL 或数据文件)和物理备份(复制数据文件),恢复目标由 RPO(能丢多少数据)和 RTO(多久恢复)定义。全量备份加 WAL/binlog 可以实现时间点恢复(PITR);快照和从库只能作为补充。备份必须独立存放,并定期做恢复演练。

边界在于:备份文件存在不等于可恢复。介质损坏、WAL 链断裂、备份期间 schema 变化、权限和版本不匹配,都可能让恢复失败;从库会同步误删,不能替代备份。恢复时间也常被低估,尤其是大库、冷存储和需要重放大量日志的场景。

失败时的表现是恢复时发现备份不完整、PITR 找不到需要的日志、恢复耗时超过 RTO,或者恢复后数据与预期不一致。只有经过演练、记录耗时并验证数据一致性的备份,才算真正的备份。

概念关系

组件 读什么 写什么 失败后的现象
客户端与连接池 SQL、连接配置 连接、事务 池耗尽、连接超时
解析器与优化器 SQL、统计信息 执行计划 选错索引、全表扫描
缓冲池与存储引擎 数据页、索引页 脏页、undo/redo IO 抖动、checkpoint 尖峰
WAL/binlog 已提交事务 日志记录 日志写满、复制延迟
主库 读写请求 数据页、日志 锁等待、死锁、连接打满
从库 relay log、WAL 重放后的数据页 延迟、复制中断、读到旧数据
备份与恢复 全量备份、日志 可恢复的数据副本 RPO 超标、恢复失败

写入从连接进入,经优化器生成计划,在缓冲池中修改页面并写日志;日志传到从库重放,备份系统则独立保存全量副本和日志链。任何一层对不上,都会在故障或切换时暴露。

常见误解

读完自测

合上页面,先回答,再展开对照。

  1. 为什么“有索引”不等于“查询会走索引”?应该看什么证据?
  2. 异步复制下读到旧数据为什么不是 bug?如何实现读己之写?
  3. 为什么从库不能替代备份?一个可信的备份需要满足什么?
答案
  1. 优化器根据统计信息估算成本,函数、隐式类型转换、低选择率、过期统计和最左前缀不匹配都会让索引失效;应看执行计划、估算行数与实际行数、索引定义和统计信息,而不是只看索引是否存在。
  2. 异步复制只保证最终重放,从库在延迟窗口内看到旧快照是设计使然;读己之写可以路由到主库、等待 GTID/LSN 追平、使用会话粘滞,或在业务层接受并处理延迟。
  3. 从库会同步误删、逻辑错误和损坏,无法提供独立恢复点;可信备份必须独立存放、覆盖 RPO、保留完整日志链,并经过定期恢复演练和一致性验证。

相关条目