【系统架构师案例题-知识点】数据库与缓存设计

本文聚焦系统架构师案例题中的数据库与缓存设计,重点说明关系型数据库设计、NoSQL 选型、分库分表、读写分离、缓存策略、缓存故障模式以及缓存与数据库一致性问题,并结合电商、支付、内容平台、搜索系统、推荐系统等真实软件行业场景说明这些技术为什么会出现、各自解决什么问题、工程上该如何取舍。

阅读时可以按三个层次把握:先理解数据为什么会成为瓶颈,再理解数据库和缓存分别解决哪一类问题,最后把题干中的业务信号翻译成卷面表达。

一、先建立整体认识

数据库与缓存设计的核心,不是"会不会背名词",而是看清系统到底在为什么付成本。

一个系统最早往往只有一套关系数据库,数据正确、事务清晰、开发也直接。但随着业务增长,问题会逐步出现:

  • 读请求越来越多,数据库开始扛不住查询压力
  • 写请求越来越集中,单库容量和吞吐接近上限
  • 表越来越大,索引越来越重,慢 SQL 和锁冲突开始频繁出现
  • 页面越来越复杂,一个接口需要拼很多表,查询响应时间持续升高
  • 业务变化越来越快,某些数据结构已经不适合硬塞进固定表结构

所以,数据库与缓存设计本质上是在解决五类问题:

  • 如何在数据正确和查询效率之间平衡
  • 如何根据数据特点选择合适的存储模型
  • 如何把单库单表的容量和性能瓶颈拆开
  • 如何用缓存扛住热点流量
  • 如何在引入缓存之后控制数据不一致和故障扩散

二、关系型数据库设计

1. 为什么要规范化

怎么理解: 规范化的目标不是为了考试好记,而是为了让数据结构更稳定、冗余更少、更新更不容易出错。

最典型的问题是:如果一份用户信息被冗余在订单表、地址表、营销表很多地方,那么用户改一次手机号,就可能有多处要一起改;如果漏改一处,系统就会出现"同一个用户在不同页面看到不同手机号"的异常。这就是典型的数据冗余和更新异常。

常见范式可以这样记:

范式 重点要求 直观理解
1NF 字段原子化 一个字段里不要再塞列表、对象、组合值
2NF 消除部分依赖 非主属性不能只依赖联合主键的一部分
3NF 消除传递依赖 非主属性不要通过别的非主属性间接依赖主键
BCNF 决定因素都是候选键 比 3NF 更严格,进一步消除异常

行业里的理解方式: 用户、订单、商品、支付这些核心业务表之所以要认真建模,就是因为它们承载的是长期稳定的数据边界。关系库在这里最大的价值,是结构清晰、约束明确、事务能力强。

2. 为什么又会反规范化

如果规范化一直做到极致,查询就会越来越依赖多表关联。对高并发系统来说,这会带来新的问题:读链路变长、SQL 更复杂、索引更难维护、接口响应更慢。

所以工程上经常会走到第二步:在核心数据仍保持规范化的前提下,对读路径做反规范化

怎么理解: 反规范化不是"设计变差了",而是在承认一个现实:很多业务更怕查询慢,而不是多存一份可控冗余。

常见做法有:

手段 在行业里的典型表现
冗余列 订单表直接冗余用户名、商品快照、收货地址快照
派生列 订单总金额、评论数、点赞数直接存结果,不每次实时聚合
预聚合表 报表系统提前汇总日活、GMV、转化率
拆大字段 把正文、图片、扩展 JSON 从主表拆到扩展表

什么时候适合做: 读多写少、查询性能敏感、页面拼装复杂的场景最常见。例如电商订单详情页、内容平台文章页、BI 看板、运营后台统计页。

代价是什么: 一旦引入冗余,就必须接受同步更新、补偿修复和一致性校验的成本。所以规范化解决"数据对不对",反规范化解决"数据查得快不快"。


三、NoSQL 与搜索系统

1. 为什么关系数据库不够用

很多人第一次接触 NoSQL,会误以为它是在"替代 MySQL"。其实更准确的理解是:NoSQL 是在补关系库不擅长的那一部分能力

关系型数据库最擅长的是:

  • 结构清晰的数据建模
  • 事务一致性
  • 复杂条件查询
  • 强约束和强关系

但它不一定擅长:

  • 极高并发热点访问
  • 灵活多变的半结构化数据
  • 海量时序写入
  • 复杂图关系遍历
  • 全文搜索和多维检索

所以 NoSQL 的本质,不是"更先进",而是"更偏科"。选型时要先看你的问题是什么。

2. 常见类型怎么选

类型 解决什么问题 代表产品 软件行业里的典型场景
键值型 极快读写、简单查找 Redis、Memcached 缓存、Session、验证码、排行榜
文档型 结构灵活、对象聚合存储 MongoDB、CouchDB 内容管理、画像标签、表单系统
列族型 海量写入、宽表、时序/日志类数据 HBase、Cassandra 埋点、监控、日志、设备上报
图数据库 关系遍历和路径查询 Neo4j、JanusGraph 社交关系、风控关联、知识图谱
搜索引擎 全文搜索、多字段检索、排序聚合 Elasticsearch 商品搜索、文章搜索、日志检索

怎么考: 题干如果强调的是"关系复杂、需要路径查询",优先想到图数据库;如果强调"全文检索、分词、高亮、相关度排序",优先想到搜索引擎;如果强调"高频读写、热点缓存、排行榜",优先想到 Redis。

3. MongoDB 为什么常见

MongoDB 适合的不是"所有数据",而是结构经常变化、对象聚合明显、字段不固定的数据。

比如内容平台的文章文档、低代码平台的动态表单、用户画像标签、地图 POI 信息,都很适合文档模型。因为这些数据天然就是"一个对象一坨信息",硬拆成很多关系表反而让开发和查询都变复杂。

它常被看中的能力包括:

  • 文档模型天然贴近 JSON 接口
  • schema 相对灵活,适合快速迭代
  • 支持分片,水平扩展能力较好
  • 聚合能力和地理空间查询能力较强

在地理空间场景里,MongoDB 支持 GeoJSON2dsphere 索引,这也是它常出现在地图、门店、轨迹类系统中的原因。

4. Redis 为什么不只是缓存

Redis 经常被简单描述成"缓存",但它真正强的地方在于:内存级性能 + 多种数据结构 + 简单直接的操作语义

常见数据结构可以这样记:

结构 最常见用途
String 缓存值、计数器、分布式锁
Hash 用户属性、对象字段缓存
List 最新列表、简单队列
Set 去重、标签集合、共同关注
ZSet 排行榜、优先队列、延迟任务

为什么 ZSet 很常考: 因为它同时有"集合"和"排序"两种能力。像积分榜、热搜榜、直播间贡献榜,本质上都很适合用 member + score 表示。

常见命令通常围绕这几个点考:

  • ZADD:写入成员和分数
  • ZRANGE / ZREVRANGE:按排序取区间
  • ZSCORE:查单个成员分数
  • ZRANK / ZREVRANK:查排名
  • ZCOUNT:统计区间数量

四、分库分表与读写分离

1. 为什么数据库会走到拆分

数据库拆分不是"上来就该做"的架构动作,而是当单库单表真的扛不住时才有意义。

常见触发点有:

  • 单表数据量太大,索引和查询明显变慢
  • 单库磁盘、CPU、IO 接近瓶颈
  • 某些热点业务流量远高于其他业务
  • 不同业务模块的生命周期和扩容需求完全不同

2. 分库分表到底在拆什么

水平拆分 是按行拆,把不同记录分散到不同库或表;垂直拆分 是按业务边界或字段类型拆,把不同功能的数据分开放。

常见分片策略可以这样理解:

策略 适合怎么理解 主要问题
范围分片 按 ID、时间区间分布 容易形成热点,如最新时间段写入过热
哈希分片 尽量打散,追求均匀 扩容时数据迁移成本高
一致性哈希 为了降低节点增减时迁移范围 设计和实现更复杂

行业里的例子:

  • 订单表按用户 ID 或订单 ID 分片,是典型水平分片
  • 用户库、订单库、支付库分开,是典型垂直分库
  • 日志表按天或按月分表,是时间维度分片的常见形态

怎么考: 题干如果强调"单表过大、单机容量不足、热点明显、业务边界清晰",通常就是在考分片思路。

3. 读写分离解决什么问题

如果系统的主要压力来自读多写少,最直接的办法不一定是分片,而是先做读写分离。

怎么理解: 主库负责写,从库负责读。这样写路径仍然集中控制,读路径则可以横向扩展。

它常见于:

  • 商品详情、文章详情、配置查询这类高频读取场景
  • 后台报表、运营查询、历史记录查询
  • 读流量明显高于写流量的业务

真正要注意的点:

  • 主从复制存在延迟,刚写完马上读可能读到旧值
  • 不是所有读都能走从库,强一致读通常还要回主库
  • 应用层、中间件层都需要处理读写路由

所以读写分离提升的是吞吐,但它不会自动解决一致性问题。


五、缓存设计

1. 为什么系统离不开缓存

缓存的本质,不是"让系统更高级",而是把那些重复、高频、热点的读请求挡在数据库前面。

最常见的行业场景包括:

  • 商品详情页、文章详情页反复访问同一份数据
  • 首页推荐、热榜、用户画像反复被读取
  • 秒杀、大促、热点活动在极短时间内打爆某些 key

如果这些请求全都落到数据库,数据库很快就会成为瓶颈。所以缓存设计的核心问题是:哪些数据值得缓存、缓存多久、更新时怎么处理、故障时怎么兜底。

2. 常见缓存策略怎么理解

2.1 Cache-Aside:最常见,也最像真实工程

这几乎是大多数业务系统的默认方案。

读流程:

  1. 先查缓存
  2. 命中直接返回
  3. 未命中则查数据库
  4. 把结果写回缓存

写流程:

  1. 先更新数据库
  2. 再删除缓存

为什么不是"先更新数据库再更新缓存": 因为缓存不是数据的最终来源,它只是数据库前面的一层加速层。看起来"更新数据库后顺手更新缓存"很自然,但在并发写的情况下,较早请求的旧值有可能在后面又把缓存覆盖掉,结果缓存里反而变成旧数据。相比之下,更新数据库后删除缓存更稳,因为后续读请求会从数据库重新加载最新值。

为什么是"先更库,再删缓存": 如果先删缓存,数据库还没更新完时来了一个读请求,就会把旧数据重新写回缓存。这是缓存不一致最经典的来源之一。

2.2 Read-Through:把读回源交给缓存层

应用只跟缓存打交道,缓存自己决定未命中时如何加载数据库。它适合那些希望把缓存逻辑从业务代码里抽出去的场景,但实际业务里最常见的仍然是 Cache-Aside。

2.3 Write-Through:同步把缓存和数据库都写掉

它的好处是写完之后缓存和数据库天然更一致,缺点也很直接:写延迟会上升,因为数据库必须一起成功。

这类模式更适合对透明性要求高、写路径较可控的场景,不是大多数互联网业务的默认选项。

2.4 Write-Behind:先写缓存,稍后异步落库

这是典型的"用一致性换性能"方案。它适合那些极端追求写吞吐、能接受缓存层承担更多持久化责任的场景,但一旦缓存故障,数据丢失风险会更高。

3. 怎么选,怎么考

策略 一致性 性能 更像什么场景
Cache-Aside 最终一致 通用互联网业务
Read-Through 最终一致 希望统一缓存回源逻辑
Write-Through 更强一致 较低 写路径可控、强调同步落库
Write-Behind 弱一致 特定高吞吐写场景

考试里如果没有特别说明,优先写 Cache-Aside,因为它最符合真实系统的常见实现。


六、缓存的三类典型问题

缓存问题很容易混,真正好记的方法不是死背名词,而是看数据库被打穿的原因分别是什么。

1. 缓存穿透:查的是根本不存在的数据

怎么理解: 请求的 key 在缓存里没有,在数据库里也没有,于是每次都直接打到数据库。

这类问题常见于恶意攻击、错误参数、枚举式探测。比如不断请求不存在的商品 ID、用户 ID、订单号。

常见解决方法有:

方案 适合怎么理解
缓存空值 查不到也缓存一个短期空结果,避免反复打库
布隆过滤器 先做一次"可能存在性"判断,不可能存在的直接拦掉
参数校验 对非法 ID、非法格式、明显异常请求先拦截

布隆过滤器为什么常考: 因为它的特点很鲜明。它能高效判断"一个元素一定不存在",但不能保证"一定存在",会有误判。

2. 缓存击穿:查的是很热,但刚好过期的数据

怎么理解: 一个热点 key 平时都被缓存挡住了,但它一过期,瞬间大量并发一起打到数据库。

比如一个爆款商品详情、一个热搜词、一个首页配置 key,在某个时间点突然失效,数据库就会被这一个热点点位打爆。

常见方案有:

方案 适合怎么理解
互斥锁 只允许一个线程去回源加载,其他线程等待
逻辑过期 数据先返回旧值,后台异步刷新新值
热点 key 永不过期 对极热点数据人工管理生命周期

3. 缓存雪崩:不是一个点炸,是一大片一起炸

怎么理解: 大量 key 同时过期,或者整个缓存集群不可用,结果本该被缓存挡住的请求一起冲向数据库。

这比击穿更危险,因为击穿通常是一个热点 key,雪崩是整个缓存层失效。

常见方案有:

方案 适合怎么理解
过期时间加随机值 把集中失效打散
多级缓存 本地缓存 + Redis 缓存,降低同一层全部失效的冲击
缓存高可用 哨兵、Cluster、主从等,减少单点故障
限流降级 数据库前再做最后一层保护
缓存预热 系统上线前先把热点数据准备好

4. 三者怎么区分,怎么考

问题 核心特征 一句话区分
穿透 数据本来就不存在 缓存和数据库都没有
击穿 单个热点 key 失效 一个点被并发打爆
雪崩 大量 key 同时失效或缓存层故障 一大片流量同时压库

答题时最好先判断是哪一种,再写对应方案,避免把三类问题的答案混着写。


七、缓存与数据库一致性

1. 为什么这件事总会出问题

缓存和数据库是两个独立系统,对它们的更新不是一个原子操作。只要存在并发,只要有时间窗口,就可能出现短暂不一致。

最经典的冲突场景是:

  • 线程 A 更新数据库
  • 线程 A 删除缓存之前
  • 线程 B 恰好读缓存未命中,于是回源数据库
  • 如果 B 读到的是旧值,再把旧值写回缓存,就形成了脏缓存

所以缓存一致性问题的根源,不是某一行代码写错了,而是缓存和数据库天然是两个时钟、两套状态、两次操作

2. 工程上通常怎么做

方案 核心思路 更适合什么场景
先更库再删缓存 最经典的 Cache-Aside 写法 通用业务系统
延迟双删 删除缓存两次,尽量覆盖并发窗口 能接受实现更复杂的场景
MQ 异步删缓存 更新数据库后发消息删缓存,可重试 强调可靠删除
Binlog 驱动刷新 监听数据库变更后同步缓存 统一化、平台化治理
读写锁/强约束 用更强同步手段换一致性 少量强一致场景
Write-Through 把写入统一放到缓存层 特定框架或特定存储模型

最常见的标准答案: 如果没有特别苛刻的强一致要求,通常优先采用 先更新数据库,再删除缓存。它不是绝对零不一致,而是在复杂度和效果之间非常均衡。

3. 软件行业里怎么取舍

  • 商品详情、文章详情、用户主页这类读多写少场景,通常接受短暂不一致
  • 库存、价格、账户余额这类对错误特别敏感的场景,会更谨慎,甚至降低缓存比例
  • 平台化团队会更偏向 MQ、Binlog、统一缓存框架来收敛一致性治理

考试里如果问"为什么不能完全一致",答题重点要落在:性能、复杂度、并发窗口和业务容忍度之间的权衡


八、答题模板

1. 数据库设计题

对于题目中的 [业务场景] ,如果重点是保证数据结构稳定、减少冗余和更新异常,应优先采用 规范化设计 ;如果重点是提升查询效率、减少关联查询成本,则可在核心数据正确的前提下进行 适度反规范化 ,例如 [冗余列/派生列/预聚合]

2. NoSQL 选型题

该场景适合选用 [产品或类型] 。因为其主要业务特征是 [高并发/结构灵活/海量写入/复杂关系/全文检索] ,而 [产品能力] 恰好能够满足这一需求,具体表现为 [1~2 条能力]

3. 缓存流程题

写 Cache-Aside 时,标准顺序通常是:

  1. 读先查缓存
  2. 未命中再查数据库
  3. 查询结果回填缓存
  4. 写时先更新数据库
  5. 再删除缓存

4. 缓存问题分析题

题目中的问题属于 [穿透/击穿/雪崩] 。其根本原因是 [不存在数据反复访问/热点 key 过期/大量 key 同时失效] 。可采用 [2~3 个方案] 进行治理,分别从 [拦截不存在请求/保护回源加载/避免大面积同时失效] 等方面降低数据库压力。

5. 一致性分析题

缓存与数据库不一致的根本原因在于两者属于独立存储,对数据库更新与缓存操作之间不存在原子性保证。在并发场景下,更新库和删缓存之间存在时间窗口,因此通常采用 先更新数据库,再删除缓存 作为折中方案,并结合 延迟双删、MQ 重试、Binlog 同步 等机制进一步降低不一致概率。

相关推荐
不剪发的Tony老师2 小时前
DBcooper:一款面向开发者的现代数据库客户端
数据库·sql
添砖java‘’3 小时前
MYSQL数据类型
数据库·mysql
qq_372154233 小时前
如何配置表中某列的排序权重_全文索引配置与权重分配
jvm·数据库·python
2501_914245933 小时前
CSS如何使用-nth-of-type精确选择列表项_通过元素类型限制提升样式健壮性
jvm·数据库·python
吕源林3 小时前
Golang如何做本地缓存加速_Golang本地缓存教程【核心】
jvm·数据库·python
Magic@3 小时前
Redis学习[1] ——基本概念和数据类型
linux·开发语言·数据库·c++·redis·学习
程序员大志3 小时前
系统架构设计师:最大流量问题
系统架构
你觉得脆皮鸡好吃吗3 小时前
SQL注入 基础防御
数据库·sql
池佳齐3 小时前
软考高级系统架构设计师备考(十九):数据库系统—数据库设计
数据库·系统架构