Skip to content

第 6 讲|数据库的出现

数据量少的时候,JSON、CSV、纯文本文件就能解决问题——简单、直接、不需要任何额外组件。但场景一旦升级到"多人同时读写、按条件查询、按时序追加",文件方案会迅速暴露一系列根本性缺陷:并发写入互相覆盖、按条件查找要扫整个文件、误删后恢复困难、字段格式难以统一校验。数据库就是为解决这一系列问题而设计的系统

本讲从文件方案的局限出发,讲清楚数据库的核心概念:表、SQL、事务、索引、缓存、复制与备份。这些是后续所有业务架构(单机、集群、微服务、分布式)的共同基础。

一、文件方案的局限

把文件方案的问题逐一列出,数据库的价值就一目了然:

文件方案混乱后需要数据库表来管理数据

问题具体表现
查询慢按条件查找需要从头扫整个文件,百万行级数据基本不可用
并发难两个进程同时写文件,后写的覆盖先写的,没有并发控制机制
格式脆弱JSON 少一个逗号、CSV 多一个换行,整份文件无法解析
约束少字段类型、必填、唯一性、外键关联,全靠业务代码自己校验
恢复麻烦误删一行后只能从备份恢复,且备份可能也已损坏
事务缺失"扣库存 + 写订单" 这种必须整体成功或失败的操作无法保证原子性

数据库的"表"看起来与结构化文件类似——都是行和列——但多出了字段类型、约束、索引、事务、查询语言这些能力。这些机制是文件方案天然不具备的。

二、表与 SQL

表的结构

数据库中最基本的概念是(table)。表由若干列(column)和若干行(row)组成:

idnameemailstatuscreated_at
1alicealice@example.comactive2026-06-10 09:00
2bobbob@example.cominactive2026-06-10 10:30

每一行代表一条记录,每一列代表一个字段。字段在建表时声明类型:id 是整数、name 是字符串、created_at 是时间戳——类型在写入时被强制校验,不符合类型的数据会被拒绝。

SQL:操作数据库的标准语言

SQL(Structured Query Language)是关系型数据库的标准操作语言,核心动词:

SQL 关键字操作示例
SELECT查询SELECT name FROM users WHERE id = 1;
INSERT插入INSERT INTO users (name, email) VALUES (...);
UPDATE修改UPDATE users SET status = 'inactive' WHERE id = 1;
DELETE删除DELETE FROM users WHERE id = 1;
CREATE TABLE建表CREATE TABLE users (id INT PRIMARY KEY, ...);
JOIN多表关联SELECT u.name, t.title FROM users u JOIN tasks t ON t.user_id = u.id;
GROUP BY聚合SELECT status, COUNT(*) FROM users GROUP BY status;

SQL 语法学起来不难,但写好 SQL 是另一回事——索引设计、查询优化、连接池管理、慢查询定位是数据库相关工作的核心技能,需要长期积累。

数据库的几大类别

根据数据结构和场景,数据库分为几大类:

类别代表数据结构典型用途
关系型MySQL、PostgreSQL、Oracle、SQL Server行列表,固定 schema用户、订单、权限、审计等核心业务数据
文档型MongoDB、CouchDBJSON 文档内容管理、嵌套结构、schema 易变化
键值型Redis、Memcachedkey-value缓存、会话、计数器、消息队列
列式ClickHouse、Cassandra按列存储OLAP 分析、大数据聚合
检索型Elasticsearch、OpenSearch倒排索引全文检索、日志检索
时序型InfluxDB、TimescaleDB、Prometheus时间戳 + 标签 + 数值监控指标、IoT
图数据库Neo4j、JanusGraph节点 + 边社交关系、推荐、风控

业务系统通常同时使用多种数据库:MySQL 存核心业务、Redis 做缓存、Elasticsearch 做搜索、ClickHouse 做分析,各自负责擅长的场景。

三、事务:必须整体成功或失败

事务(Transaction)是数据库最核心的能力之一。它解决的问题是:一组操作必须整体成功或整体失败,不能停在中间状态

事务要求一组动作整体成功或整体回滚

转账场景的经典例子

最经典的例子是转账:A 账户扣 100,B 账户加 100。如果扣完 A 的钱程序崩溃,B 那边没加上,钱凭空消失了 100——这种状态在金融系统里是绝对不能出现的。

事务把"扣 A + 加 B"包在一起,要么两步都成功提交,要么任何一步失败就整体回滚:

sql
BEGIN;                                                  -- 开始事务
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
COMMIT;                                                  -- 全部成功,提交
-- 或
ROLLBACK;                                                -- 任一步失败,回滚

ACID 四特性

事务保证的四个特性合称 ACID:

特性含义
Atomicity 原子性一组操作整体成功或整体失败,不会停在中间状态
Consistency 一致性操作前后数据必须满足所有约束(外键、唯一、检查约束)
Isolation 隔离性并发事务之间的影响受控制,看起来像串行执行
Durability 持久性提交后的数据不因进程崩溃或断电丢失

隔离级别:并发事务的可见性控制

并发执行的事务之间,如何看到对方的修改,由隔离级别控制。SQL 标准定义了四种:

隔离级别脏读不可重复读幻读性能
Read Uncommitted可能可能可能最高
Read Committed不会可能可能
Repeatable Read不会不会可能
Serializable不会不会不会最低

MySQL InnoDB 默认 Repeatable Read,PostgreSQL 默认 Read Committed。隔离级别越高,并发性能越差——通常不动默认值,有特殊需求时显式调整。

业务场景中需要事务的常见动作

  • 金融:转账、扣款、结算
  • 电商:扣库存 + 生成订单 + 写入支付流水
  • 任务系统:创建任务 + 写审计日志 + 发送通知
  • 配置变更:更新配置 + 记录版本号
  • 审批流:状态变更 + 写入审批历史

凡是"一组关联操作不能停在中间状态"的场景,都需要事务包裹。

四、索引

索引像目录帮助数据库少翻表

索引是数据库的查找目录

索引(Index)是数据库为某些字段建立的额外查找结构,作用类似于书的目录——没有索引,找一个用户要翻整本书(全表扫描);有索引,直接定位到第几页(索引查找)。

最常见的索引数据结构是 B+ 树(MySQL、PostgreSQL 默认),还有哈希索引、全文索引、空间索引等。索引的本质是用额外的存储和写入开销,换取查询速度

索引带来的性能差异

一个具体例子:任务表 100 万行,接口要查询"状态为 failed 且最近 1 天创建"的任务。

sql
SELECT * FROM tasks WHERE status = 'failed' AND created_at >= '2026-06-22';

无索引时:数据库扫描全表 100 万行,逐条判断,通常需要数秒。

加索引后:

sql
CREATE INDEX idx_status_created ON tasks (status, created_at);

数据库通过索引直接定位到满足条件的行,通常在几十毫秒内返回。

这种"加索引后接口从 5 秒变成 50 毫秒"的提升在慢 SQL 优化中非常常见——绝大多数性能问题的根因都是缺少合适的索引。

索引不是越多越好

索引带来代价:

代价影响
占用磁盘空间每个索引都是一份额外的数据
拖慢写入INSERT/UPDATE/DELETE 时必须同步维护所有相关索引
占用内存查询时索引需要加载到内存才高效

一般原则:

  • 经常出现在 WHERE、JOIN、ORDER BY 中的字段适合建索引
  • 极少查询、频繁更新的字段不适合建索引
  • 重复值很多的字段(如性别)单独建索引意义不大
  • 联合索引比多个单字段索引更高效——但联合索引有"最左前缀"原则(详见 MySQL 文档)

慢查询排查的标准流程

接口慢且怀疑数据库时,标准排查路径:

sql
-- 1. 看执行计划,确认是否走索引
EXPLAIN SELECT * FROM tasks WHERE status = 'failed' AND created_at >= '2026-06-22';
-- 看 type 列:ALL 全表扫描、index 索引扫描、range 范围扫描、ref 索引等值

-- 2. 看慢查询日志(MySQL)
SHOW VARIABLES LIKE 'slow_query_log%';

-- 3. 看当前正在执行的慢查询
SHOW FULL PROCESSLIST;

绝大多数慢查询的根因是:应该走索引但走了全表扫描——执行计划的 type = ALL 是最直接的信号。

五、缓存

数据库再快也有上限。有些数据被频繁读取但很少变化,每次都查数据库会造成不必要的负担。缓存就是把热数据放在更快的存储(通常是 Redis)中,应用先查缓存,缓存未命中再查数据库并回填缓存。

适合缓存的数据特征

  • 读多写少 — 读写比例 10:1 以上
  • 变化不频繁 — 几分钟到几小时不变
  • 可容忍轻微延迟 — 缓存与数据库可能短暂不一致

典型场景:用户权限、配置项、字典表、热门商品、排行榜、统计接口的聚合结果。

不适合缓存的数据

  • 强一致要求的数据(如账户余额)
  • 写多读少的数据
  • 数据量极大且访问随机(命中率低)

缓存引入的四个经典问题

问题含义处理
缓存穿透大量查询根本不存在的数据,缓存里没有,全部打到数据库缓存空值(短 TTL)、布隆过滤器
缓存击穿某个热点 key 突然过期,瞬间大量请求同时查数据库互斥锁、热点 key 永不过期
缓存雪崩大量 key 同时过期(或缓存整体挂),数据库压力暴涨过期时间加随机抖动、多级缓存、降级
数据不一致数据库更新了但缓存还是旧值主动失效、延迟双删、订阅 binlog 同步

缓存的核心原则:缓存只是优化手段

缓存绝不能作为唯一数据来源。缓存里丢了数据,系统必须能从数据库恢复出来。把数据只放缓存(不持久化到数据库),Redis 重启或数据被淘汰后业务就瘫了——这不是合理的架构,而是把缓存当数据库用的滥用。

业务架构的不变量:数据库是事实来源(source of truth),缓存只是临时副本

六、复制与备份

数据库的高可用和容灾,主要依赖以下三种机制:

复制和备份覆盖的问题不同,不能互相替代

机制解决什么问题
主从复制主库故障时从库可接管;读请求可分摊到从库
备份误删、表损坏、机房整体故障后能恢复数据
binlog / WAL记录所有变更过程,支持基于时间点恢复(PITR)与复制

主从复制:实时同步

主从复制的工作原理:主库执行的每一次数据变更,通过 binlog(MySQL)或 WAL(PostgreSQL)同步到从库,从库重放这些变更以保持数据一致。

主从复制的两个核心用途:

  1. 故障切换:主库挂了,从库提升为主继续提供服务,业务停机时间可控
  2. 读写分离:写请求只给主库,读请求分摊到多个从库,扩展读容量

备份:防御数据损坏

备份是把数据库的某一时刻状态完整保存到独立存储(对象存储、磁带、远程机房)。备份的核心价值是应对"数据损坏"和"误操作"——这些场景下复制完全无效。

常见备份策略:

策略频率保留
全量备份每日 / 每周数天至数月
增量备份每小时 / 每天配合全量恢复
binlog 持续归档实时至少最近一周

复制不能替代备份

这是必须记住的核心认知:主从复制是异步的(或半同步)——主库上误删一张表 DROP TABLE users,这个 DROP 操作会被同步到所有从库,所有副本里这张表都会被删除

这种场景下只有从备份恢复才是出路。复制无法防御:

  • 误操作:DROP DATABASEDELETE 未带 WHERE、UPDATE 影响范围错
  • 应用 bug 写脏数据
  • 安全事件:被入侵后批量删除/篡改数据
  • 表结构损坏

备份不能替代高可用

反过来,备份恢复需要时间——几十 GB 的数据库恢复可能需要数十分钟到几小时,期间业务完全不可用。

如果业务对可用性有要求(如 99.95%),光靠"挂了从备份恢复"远远不够——必须有主从切换、集群方案,保证故障时分钟级甚至秒级切换。

生产环境的标准配置

生产数据库的合理配置组合:

  • 主从复制 — 保证主库故障时分钟级切换
  • 定期全量备份 + 增量 + binlog 归档 — 保证误操作时可恢复到任意时间点
  • 备份异地存储 — 防御机房级故障
  • 定期演练恢复 — 确认备份是真的能用(很多团队备份从没成功恢复过)

备份未演练 = 没有备份——只有真正恢复过的备份才是可靠的备份。这是数据库容灾里最容易被忽略但最重要的原则。

三种机制的覆盖关系

故障类型主从复制备份binlog
主库进程崩溃✓ 切换从库✗ 太慢✗ 单独无用
主库硬件故障✓ 切换从库△ 可恢复但慢
误删数据✗ 复制把删除同步走了✓ 从备份恢复✓ PITR 恢复到删除前
表结构损坏✗ 同上
机房整体故障△ 需要异地从库✓ 异地备份
入侵后数据篡改✓(但需保留时间够长)

三种机制必须同时存在,任一缺失都会让某类故障无法处理。