极客时间:后端存储实战课

2023-05-275 出链

https://time.geekbang.org/column/intro/100046801

以电商不同发展阶段的场景为蓝本,介绍不同阶段遇到的问题及相关解决办法。

  • 创业篇:用尽可能简单的架构,正确地保存业务事实
  • 高速增长篇:保护数据库
  • 海量数据篇:分布和异构

如何设计一个数据系统提到的:在不断变化的 Workload 下,如何演进数据模型、读写路径和分布式策略。

核心观点

  • 存储设计的起点不是技术选型,而是业务读写模式:读多还是写多、是否强一致、数据量增长速度、查询是点查/范围查/全文检索还是聚合分析
  • 交易系统的第一目标始终是正确性。订单、账户、库存、支付这些数据一旦出错,后果通常不是页面慢一点,而是资金损失、对账困难和业务状态混乱。
  • 高并发系统的第一优化目标也不是更快,而是先保护 MySQL主库。缓存、读写分离、搜索引擎、异步化,本质上都在帮助主库从高频流量中脱身。
  • 不同类型的数据需要不同类型的存储:MySQL 负责交易主数据,Redis 负责热点缓存,Elasticsearch 负责全文搜索,对象存储负责文件,分析型或分布式存储负责点击流和日志。
  • 分库分表不是默认答案,而是最后一招。在那之前,应该先做 SQL 和索引优化、缓存、读写分离、归档和冷热分离。

一页速记

  • 先看业务,再选存储
  • 交易系统核心是数据不能错
  • 创建幂等靠唯一 ID + 唯一约束
  • 更新幂等靠 version 乐观锁
  • 单库一致性靠事务
  • 跨系统一致性优先考虑最终一致
  • 高并发先缓存,先保护 MySQL
  • 搜索交给 Elasticsearch
  • 数据增长先归档、冷热分离,再考虑分库分表
  • 高可用靠备份、Binlog、复制、切换、恢复
  • 图片文件用对象存储
  • 日志、点击流别继续塞交易库
  • 分库分表是最后一招

实战要点

1. 交易系统先保正确

课程最重要的一条线索,是把订单、账户、库存这类系统统一看成交易数据系统。这类系统最关键的问题从来不是吞吐,而是数据不能错

创建订单时,不能只靠前端防重,因为用户重复点击、网络超时、RPC 重试、网关重试都会制造重复请求。更稳妥的做法是:

  • 预先生成全局唯一订单号
  • 用订单号作为主键
  • 利用唯一约束兜底实现创建幂等

这相当于把重复请求转成重复写同一个主键

更新订单时,重点不是重复创建,而是并发覆盖和 ABA 问题。课程给出的典型做法是给订单表加 version 字段,用乐观锁保证:

  • 更新时携带旧版本号
  • SQL 里比较版本号
  • 更新成功后 version + 1

本质上,这是让更新操作具备我改的是我看到的那个版本的约束。

这一套思路不仅适用于订单系统,也能迁移到很多需要幂等和并发保护的业务里。它和 一致性、高并发系统实战课 里的很多问题是同一类。

2. 账户系统必须同时保存结果和过程

账户系统这部分的重点不只是事务,而是一个很实用的设计习惯:

  • 余额表保存当前结果
  • 流水表保存完整过程

如果只有余额没有流水,系统虽然能查到现在还剩多少钱,但无法解释为什么变成这样,也没法做审计、回放和对账。

课程对事务的使用也很务实。事务解决的是单库内多次写操作的一致性,例如:

  • 更新余额
  • 写入账户流水

这两步必须在同一个事务里完成,否则就会出现余额改了但没流水或者有流水但余额没改的错误状态。

这里最值得记住的不是 ACID 的定义,而是:

  • 事务是管理复杂性的手段
  • ACID 是目标,不是免费能力
  • 实际系统总在一致性和性能之间做隔离级别上的取舍

3. 跨系统一致性优先考虑最终一致

课程在分布式事务这块给出的判断很清晰:

  • 强一致需求特别高时,可以考虑 2PC
  • 更常见、更实用的是本地消息表和最终一致性

这背后的原因不是最终一致更先进,而是强一致通常更贵、更慢、更脆弱。系统一旦拆成多个服务、多个数据库,单库事务就失效了。真正的工程做法是:

  • 先把大事务拆成多个本地事务
  • 再设计消息、重试、补偿和幂等

也就是说,跨系统一致性的关键不只是某个事务协议,而是整条链路是否支持:

  • 幂等
  • 可重试
  • 可补偿
  • 可观测

这部分和 设计数据密集型应用、分布式系统 放在一起看会更清楚。

4. 高并发的第一目标是保护主库

商品详情页、首页、搜索结果页这类场景,最典型的特点是:

  • 读多写少
  • 热点集中
  • 请求量远高于交易链路

课程对这类问题的思路不是一味扩数据库,而是先做存储分层:

  • 热点数据走缓存
  • 搜索类查询交给 ES
  • 交易主数据仍然以 MySQL 为准

这里有个很重要的现实判断:

缓存的第一目标不是“更快”,而是“让数据库别被打死”。

Redis 在课程里的定位就很明确,它是高并发下的热点承压层,不是唯一事实来源。也正因为如此,使用缓存时真正麻烦的问题不是“怎么读得更快”,而是:

  • 缓存穿透
  • 缓存击穿
  • 缓存雪崩
  • 缓存和数据库的一致性

课程后面提到 MySQL 到 Redis 同步时,也在强调同一个方向:不要把所有缓存刷新逻辑都塞进业务代码,应该尽量借助 Binlog、消息队列或异步链路,把缓存更新做成一条更稳定的数据同步机制。

5. 搜索、检索、分析不要硬塞给 MySQL

课程用 Elasticsearch 讲了一个很常见的误区:很多系统不是不知道 ES 好用,而是总想让 MySQL 继续兼顾全文检索。

问题在于两者擅长的事根本不同:

  • MySQL 擅长事务、约束、点查、范围查
  • ES 擅长倒排索引和全文搜索

所以搜索系统更像是搜索视图,而不是交易主库。它解决的是:

  • 分词
  • 召回
  • 相关性排序
  • 高性能检索

同样的思路也适用于点击流、日志、埋点、监控这类海量追加写数据。课程后面在讲海量数据、NewSQL、RocksDB 时,本质上都在说明:

不同访问模式应该由不同存储系统分工承担,而不是让一套数据库兼顾所有事情。

6. 分库分表是最后一招

这门课非常值得保留的一点,是它没有把分库分表包装成一种高级架构标配,而是明确指出它的代价:

  • 跨分片查询复杂
  • 跨分片事务困难
  • 主键设计复杂
  • 扩容复杂
  • 数据迁移复杂
  • 运维复杂

因此处理数据量增长时,更推荐的顺序是:

  1. 先优化 SQL、索引、表结构
  2. 再用缓存承接读流量
  3. 再做读写分离
  4. 再做归档和冷热分离
  5. 最后才考虑分库分表

这个顺序很实用,因为很多系统其实在前 3 到 4 步就已经能把问题解决掉,没必要过早引入分片复杂度。它和 CS++分库分表CS++数据分区 可以互相参照。

进一步延伸

评论