数据存储架构:一致性、故障模型与工程选型

#云原生架构

数据存储架构:一致性、故障模型与工程选型

学习深度: ⭐⭐⭐⭐ 文档类型: 理论与工程决策 核心结论: 选型不是给数据库贴 CAP 标签,而是明确读写模型、一致性需求、故障模型、恢复目标和团队运维能力。


目录

  1. 选型主线
  2. 一致性与 CAP 的正确使用方式
  3. 多模型数据库选型
  4. Redis、缓存与一致性边界
  5. 对象存储与数据湖
  6. 生产检查清单
  7. 参考来源

选型主线

基础设施文章常见问题是把系统简单归类为 CP/AP,或者用“强一致”“高可用”“秒级故障转移”这类词直接下结论。生产系统里这些词必须带边界:

  • 读写模型: 单主写、多主写、分片写、追加写、批量导入、随机点查、范围查询、聚合分析。
  • 一致性模型: 读己之写、单对象线性一致、多对象事务、快照隔离、最终一致、会话一致。
  • 故障模型: 节点宕机、网络分区、磁盘损坏、时钟漂移、复制延迟、跨可用区抖动、运维误操作。
  • 恢复目标: RPO 能否丢数据,RTO 能停多久,恢复后是否需要重放消息或修复缓存。
  • 运维复杂度: 分片、备份恢复、版本升级、容量再平衡、热点治理、索引治理、跨地域容灾。

一个可靠的选型问题应该长这样:

问题需要明确的边界
谁是写入事实源?数据库、日志、事件流、对象存储,还是外部系统
写入是否能丢?已确认写入是否允许在故障切换中回滚
读到旧值的影响是什么?展示延迟、业务错误、资金风险、库存超卖
是否需要跨对象事务?单文档、单分片、多行、多分片、跨服务
是否能接受异步修复?对账、补偿任务、幂等重放、人工介入
谁来运维?自建团队、云厂商托管、数据库专职团队

一致性与 CAP 的正确使用方式

CAP 不是选型标签

CAP 定理讨论的是发生网络分区时,系统不能同时保证线性一致性和每个请求都得到非错误响应。它不是数据库永久属性标签,也不能直接推出“MongoDB 是 CP”“Redis Sentinel 是 CP”这类结论。

更实用的表达是:

  • 分区发生时,系统是拒绝部分请求、降级读写,还是允许分叉后再合并。
  • 写入确认前,需要同步到多少副本。
  • 读请求读主节点、读副本,还是读仲裁结果。
  • 故障恢复后,已确认写入是否可能回滚。

常见一致性层级

模型含义常见代价
线性一致性所有客户端看到一个符合真实时间顺序的单对象历史跨副本协调,延迟更高,分区时可能不可用
快照隔离事务读到一个一致快照,写冲突按规则处理仍需理解写偏斜、唯一约束、事务范围
读己之写同一会话能读到自己已经确认的写入需要会话、路由或一致性读配置
单调读同一客户端不会先读到新值再读到旧值需要会话粘性或版本约束
最终一致无新写入后,副本最终收敛需要冲突处理、重试、对账

“强一致”要说明对象和范围

“数据库强一致、缓存强一致”容易误导。更准确的写法是:

  • PostgreSQL 单实例事务可以提供严格的 ACID 语义;流复制副本读取是否新鲜取决于同步/异步复制和读路由。
  • MongoDB 需要结合 read concern、write concern、read preference 和事务范围讨论一致性。
  • Redis 主从复制默认异步;Sentinel 或 Cluster 提供自动故障发现和切换,但不等于无数据丢失。
  • 缓存与数据库之间通常不是强一致系统,除非设计了版本、失效、事务消息、幂等补偿和读写路径约束。

多模型数据库选型

关系型数据库

关系型数据库仍然是默认事实源的首选,尤其当系统需要:

  • 明确的实体关系和约束。
  • 事务、唯一性、外键或复杂查询。
  • 可解释的索引、执行计划和变更审计。
  • 团队熟悉 SQL、备份恢复和迁移流程。

PostgreSQL、MySQL 等数据库不是只能“垂直扩展”。现代生产系统常用分区表、读副本、连接池、逻辑复制、分库分表或云厂商托管能力扩展容量。但这些扩展会把复杂度转移到写入路由、跨分片事务、全局唯一约束和故障恢复上。

MongoDB 与文档模型

MongoDB 适合以聚合根为中心的文档建模:一个业务对象的大部分读取可以在一个文档内完成,字段演进较快,写入吞吐和水平扩展更重要。

需要修正几个常见简化:

  • MongoDB 不是“跨文档弱 ACID”。它支持多文档事务,但事务范围、性能、分片拓扑和运维复杂度都要评估。
  • 默认读关注不等于线性一致。默认读取可能读到尚未 majority committed、之后可回滚的数据,具体取决于部署和配置。
  • 使用 writeConcern: "majority"readConcern: "majority"readConcern: "snapshot"readConcern: "linearizable" 时,要理解各自适用范围和性能成本。
  • 文档模型不是免 Schema。生产系统仍需要字段版本、索引治理、文档大小约束、归档策略和兼容性测试。

MongoDB 更适合:

  • 订单详情、配置、内容管理、设备状态等天然聚合文档。
  • 写入模式清晰、查询路径可预先设计的业务。
  • 可通过分片键把写入和读取分散到多个分片的场景。

不适合把 MongoDB 当作:

  • 任意 JOIN 的替代品。
  • 无约束的数据垃圾桶。
  • 需要大量跨集合事务和临时分析查询的唯一事实源。

键值、列式、时序与图数据库

类型适合主要风险
键值数据库简单点查、会话、计数、限流、临时状态数据模型贫乏,复杂查询和一致性常需应用承担
列式数据库日志分析、报表、宽表聚合、低频更新单行更新慢,明细点查和事务能力有限
时序数据库指标、IoT、监控、按时间窗口聚合高基数标签、保留策略、降采样设计
图数据库多跳关系、路径查询、知识图谱数据导入、权限、规模和团队熟悉度
对象存储非结构化对象、备份、数据湖、静态资源小文件、元数据、延迟、事务语义缺失

选型矩阵

需求优先考虑继续追问
交易、账务、库存PostgreSQL/MySQL 等关系型数据库隔离级别、幂等键、审计、备份恢复
聚合文档读写MongoDB 或关系型 JSONB是否需要跨文档事务、分片键、索引规模
高速缓存和临时状态Redis是否允许丢失、过期策略、故障切换语义
大规模分析ClickHouse、BigQuery、Snowflake、Doris 等数据新鲜度、导入链路、更新删除成本
指标和告警Prometheus、Mimir、VictoriaMetrics、InfluxDB 等高基数、保留周期、远程写可靠性
文件和数据湖S3 兼容对象存储、MinIO、云对象存储一致性语义、生命周期、加密、权限

Redis、缓存与一致性边界

Redis Cluster 的固定事实

Redis Cluster 使用固定的 16384 个 hash slot。键通过 CRC16 计算槽位,带 hash tag 的键可以被放到同一个槽中。每个主节点负责一部分槽,槽迁移用于扩容、缩容和再平衡。

需要避免的错误表述:

  • 不要说 hash slot 数量可按集群规模配置。开源 Redis Cluster 的槽数固定为 16384。
  • 不要把 Cluster 说成“天然强一致”。Redis 复制默认异步,主节点故障时可能丢失最近已确认给客户端但尚未复制到新主节点的数据。
  • 不要承诺固定故障转移时间。实际时间受超时配置、网络、节点数量、复制延迟、客户端重连和 DNS/连接池行为影响。
  • 不要忽略客户端。Cluster 客户端必须正确处理 MOVEDASK、重试、连接池刷新和多 key 同槽约束。

Sentinel 与 Cluster 的边界

能力Redis SentinelRedis Cluster
主要目标单主多副本的监控、通知和自动故障切换分片、复制、故障切换和槽迁移
横向扩展不负责分片通过 hash slot 分片
一致性默认异步复制,故障切换可能丢最近写入默认异步复制,分片内故障切换也可能丢最近写入
客户端要求需要发现当前主节点需要 Cluster-aware 客户端处理重定向
运维重点quorum、down-after、failover-timeout、脑裂防护槽迁移、分片热点、客户端重定向、副本布局

WAIT 可以让客户端等待写入传播到指定数量副本,但它不是完整事务提交协议,也不能把 Redis 变成严格同步复制数据库。它适合降低故障窗口,不适合承诺绝对不丢。

缓存不是事实源

缓存设计的核心不是“怎样做到缓存和数据库强一致”,而是明确缓存可以错多久、错了怎么修:

模式适合风险与治理
Cache Aside大多数读多写少场景删除缓存失败、读旧值、缓存击穿;需要 TTL、版本、重试
Read Through统一读路径缓存层耦合数据访问,异常处理复杂
Write Through写缓存同步写后端写路径延迟增加,后端失败语义要清楚
Write Behind高吞吐异步写数据丢失和乱序风险,需要日志、幂等和重放

更可靠的工程做法:

  • 数据库仍是事实源,缓存只保存可再生数据。
  • 写数据库成功后删除或更新缓存,失败要进入重试队列或补偿任务。
  • 缓存值带版本号或更新时间,避免旧值覆盖新值。
  • 热点 key 使用互斥重建、逻辑过期、请求合并或分片 key。
  • 对库存、余额、权限等敏感数据,不把缓存命中当作最终判断。

对象存储与数据湖

对象存储适合大对象、静态资产、备份、日志归档和数据湖。它通常提供 HTTP API、bucket、object key、metadata、版本控制、生命周期策略和访问控制。

现代对象存储的一致性不能一概写成“最终一致”。例如 AWS S3 已经为所有区域的对象 PUT、GET、LIST 等操作提供强读后写一致性;但不同 S3 兼容系统、跨区域复制、网关缓存、生命周期任务和客户端缓存仍可能带来可见性延迟。文章或架构文档应明确引用具体产品和操作范围。

对象存储的边界:

  • 不提供 POSIX 文件系统语义,目录通常只是 key 前缀。
  • 小文件过多会放大元数据、请求费用和查询开销。
  • 覆盖对象、并发写和跨对象事务需要应用设计。
  • 数据湖要配合表格式和元数据系统,如 Iceberg、Delta Lake、Hive Metastore 或 Glue Catalog。
  • 备份对象要考虑版本控制、对象锁、跨账号隔离和恢复演练。

生产检查清单

选型前

  • 写清楚主业务对象、读写比例、峰值吞吐、延迟目标。
  • 明确哪些数据不能丢,哪些数据可以异步重建。
  • 明确一致性需求是单对象、单分片、多行、多文档还是跨服务。
  • 明确备份频率、恢复时间、恢复后的校验和对账方案。
  • 明确扩容路径和退场路径,避免只验证 happy path。

上线前

  • 做一次恢复演练,而不是只配置备份任务。
  • 压测包括读、写、索引构建、批量导入、热点 key、故障切换。
  • 验证客户端在主从切换、连接断开、超时、重试时的行为。
  • 记录每类错误是否可重试,重试是否幂等。
  • 监控复制延迟、慢查询、连接数、磁盘、内存、缓存命中率、淘汰数。

事故后

  • 先确认事实源和最后成功写入位置。
  • 分离“系统已恢复服务”和“数据已完成修复”两个状态。
  • 对缓存、搜索索引、分析表、物化视图做一致性校验。
  • 把故障窗口、丢失范围、补偿任务和业务影响写入复盘。

参考来源