HBase 复习

基础认知 - 基础认知 - HBase 和 MySQL、Redis、Elasticsearch、Hive 的区别 - HBase - 一个建立在 Hadoop/HDFS 上的、支持联机、实时读写的分布式数据库,适合承载超大表 - 面向宽表/稀疏表 - 以 RowKey 为核心访问路径 - 强项是海量数据下的主键访问和范围扫描 - MySQL - 关系型数据库 - 强 SQL 能力 - 强事务 - 适合 OLTP 业务系统 - Redis - 内存型数据存储 - 超低延迟 - 丰富数据结构 - 更像高性能数据结构引擎/缓存平台 - ElasticSerach - 分布式搜索引擎 - 文档模型 - 擅长全文检索、相关性排序、聚合分析 - 不是传统事务数据库 - Hive - 数据仓库 - 面向离线分析 - 适合大规模批处理、报表、数仓 - 不适合在线高并发事务场景 - 列式/列族模型是什么 - 列族模型 - 看起来像一张表 - 但是每一行并不是必须拥有同样的列 - HBase 把列分成若干个 Column Family (列族) - 同一个列族里面的数据会被一起管理和物理存储 - 每一条具体的数据单元是一个 Cell,由行键、列族、列限定符、事件戳和值共同确定 - HBase 是一个按 RowKey 组织、按列族管理、按单元格存值、支持多版本的稀疏宽表结构 - 模型示例 - 层次结构 - Table -> Row -> Column Family -> Column Qualifier -> Cell(Value + TimeStamp) - 一张用户表 user_profile,定义了两个列族 info + stat - 一行数据 - RowKey = user_1001 - info:name = "Alice" - info:city = "Tokyo" - stat:login_cnt = 25 - info、stat 是列族 - name、city、login_cnt 是列限定符 - 完整列名是 family:qualifier - 每个值都带 timestamp,可以保留多个版本 - 列族、列、Cell - RowKey - 是一行数据的主键,也是 HBase 最核心的访问入口 - 查 HBase,本质是在查某个 RowKey,或者扫描一段 RowKey 的范围 - 决定这一行是谁 - 决定数据在字典序上的排序方式 - 决定大部分查询的性能上限 - Column Family - 是 HBase 中必须定义的逻辑分组 - 列族在创建表时就确定好 - HBase shell 和 API 文档都以先创建表,定义 family,再在 family 下写具体列的方式使用 HBase - 列族是一组经常一起出现,一起管理,一起存储策略配置的数据 - Column Qualifier(列限定符) - 列限定符就是列族下面具体的列名 - 列限定符不需要像 MySQL 字段在建表时全部固定列出来 - 列族通常固定,但是 qualifier 可以按需动态增加。 - HBase 适合列很多,而且不同记录拥有的列不一样的场景 - Cell - Cell 是最小存储单元 - 一个 Cell 由这些部分唯一定位 - row - family - qualifier - timestamp - value - TimeStamp/Version - HBase 天然支持多版本 - 同一个 row + family + qualifier,可以在不同 timestamp 下保存多个值 - Get/Scan API 也支持按照版本范围取数据 - HBase 的默认思维是:一个单元格可以有历史版本 - 为什么要有列族 - HBase 不只是逻辑上分组,还会影响物理管理和存储策略 - 列族可以把访问模式相近的数据放在一起 - 列族可以让不同数据有不同的存储策略 - 列族可以让稀疏表更自然 - 稀疏宽表 - 稀疏 - 不是每一行都有同样的列 - 举例 - 商品 A:颜色、尺寸、品牌 - 商品 B:品牌、功率、电压 - 商品 C:材质、长度、颜色、重量 - 如果用 MySQL,固定列会导致很多字段为空 - HBase 只保存实际存在的 family:qualifier->value 即可 - HBase 在列族下使用动态 qualifier - 宽表 - 列非常多,甚至 qualifier 的数量可以远大于传统关系表的字段数 - HBase 为什么是 Bigtable 风格数据库 - 什么是 Bigtable - Bigtable 是 Google 提出的一个分布式存储系统,用来存放结构化数据,典型特点 - 面向超大规模数据 - 稀疏,可扩展的表 - 以 row key 为核心组织数据 - 支持列族 - 支持多版本 - 支持按键范围扫描 - 以 RowKey 为中心,而不是以 SQL 为中心 - Bigtable 风格数据库核心访问方式 - 按 RowKey 精准查 - 按 RowKey 前缀或范围扫描 - 设计 schema 时优先围绕查询路径设计 row key - 设计数据库时,优先设计 RowKey 和访问路径,而不是优先设计复杂 SQL 数据模型与表设计 - 数据模型与表设计 - RowKey 设计原则 - 总纲 - RowKey 不是随便选一个主键,而是 HBase 里面最重要的访问索引、排序依据和分布依据。 - 每行只有一个被索引的值,就是 row key - 本质 - 数据如何定位 - 数据怎么按字典序排序 - 数据怎么在集群中分布,会不会热点 - RowKey 为什么重要 - 最快、最强、最自然的访问路径就是按照 RowKey 去查和按范围扫 - RowKey 设计不好 - 查询路径不顺 - scan 范围过大 - 热点集中 - 读写吞吐不均衡 - 很难靠 SQL 或者二级索引补救 - 核心原则 - 先按查询方式设计,不要按照字段含义设计 - 最常见的查询是什么 - 查询是点查还是范围查 - 查询维度谁排第一 - 结果取最近一条、最近 N 条还是一个时间段 - 让最常用的过滤维度出现在 RowKey 前部 - Row Key 前缀直接决定 - 哪些数据会排在一起 - 哪些数据可以用 prefix/range 高效扫描 - Row Key 要支持高频查询的自然范围扫描 - HBase 擅长的不是随意条件查询,而是连续键空间扫描 - 建议围绕 row key prefix 设计 schema - 例如查询设备时序数据 - 按设备查最近数据 - 按设备查某段时间数据 - device_id#reverse_ts - 避免单 key 递增直接写入,防止热点 - 不断递增的 rowKey 最新写入通常会不断打到键空间尾部,把写压力集中到少数 region - 在可扫描性和均匀分布之间做平衡 - 分桶:bucket(user_id)#user_id#reverse_ts - 时间类场景通常需要显式设计时间顺序 - rowKey 要短、稳定、可解析 - 常见设计模式 - 单主键模型 - user_id - 一行就是一个主键 - 点查为主 - 不需要时间维度 - 主维度 + 时间 - user_id#ts - 主维度 + 倒序时间 - user_id#reverse_ts - 桶 + 主维度 + 时间 - bucket(user_id)%16#user_id#reverse_ts - 数据集前缀 + 业务键 - U#1001 - 列族应该怎么拆 - 为什么列族拆分重要 - 列族会把同一族的一组列物理共置 - 列族带有独立的存储属性 - 每一行都拥有相同的列族集合 - 未写入的单元格不占用实际空间 - 列族拆分总原则 - 同查:经常一起读的字段,放在一起 - 同配:需要相同存储策略的字段,适合放在一起 - 同寿命:生命周期相近的字段,适合放在一起 - 同频率:更新频率相近的字段,适合放在一起 - 拆分口诀 - 基础资料一族、频繁指标一族、短期日志一族、打对象单独一族 - 版本机制 - 什么是版本 - 同一个字段可能保存多份历史值 - 每一份历史值靠 timestamp 区分 - HBase 官方文档把 Cell 展示为包含 row、column、timestamp 和 value - timestamp 和 version 的关系 - timestamp 是版本标识 - 在 HBase 里,每写入一个 Cell,都要有对应的 timestamp - timestamp 由系统自动生成,也可以由自己指定 - version 是字段保留的历史份数 - 按列族配置,说明最多留多少份历史数据 - TTL、压缩、Bloom Filter - TTL 是什么 - Time To Live,生存时间 - 是 cell contents 的生存时间,单位是 s - 是列族级的数据过期策略 - TTL 解决的问题 - 控制历史数据无限膨胀 - 降低存储成本 - 让冷热数据边界更为清晰 - 压缩是什么 - 压缩就是把 HBase 存到 StoreFile/HFile 里面的数据,用某种压缩算法压小 - 可以在 ColumnFamily 上启用压缩,且不需要重建表 - 用 CPU 换磁盘空间、换 I/O 带宽 - Bloom Filter 是什么 - 是一种概率型存在性判断结构 - 帮助你快速判断这个 StoreFile 里面大概率没有你想要的数据,就不去做无效读取了 - 热点问题 - 什么是热点 - HBase/Bigtable 这类系统的数据是按照 rowKey 的字段序排列的 - 相邻的 Key 也会落在相邻的 key range 上,这些 key range 被分配给特定的 Region/RegionServer 来服务 - 如果请求集中在某一个 RowKey/某一小段连续 RowKey/某个不断增长的最新 key 区间,负载就不会均匀摊开,造成热点 架构原理 - 架构原理 - HMaster 做什么 - HMaster 的定位 - HMaster 负责协调和管理,真正承担读写请求的是 RegionServer - HMaster 类似调度中心和元数据/运维控制器 - HMaster 的核心职责 - 管理整个集群的 Region 分配 - HBase 的表会被切分成很多 Region - 集群启动时给 Region 找归属的 RegionServer - RegionServer 宕机后重新分配它原来负责 Region - 负载均衡主动迁移 Region - 监控 RegionServer 的存活状态 - 处理 RegionServer 故障后的恢复 - 做负载均衡 - 处理 DDL 和 schema 管理 - 参与主备切换和集群高可用 - RegionServer 做什么 - RegionServer 的定位 - 是数据面 - 真正保存并服务 Region - 真正执行 Put/Get/Scan/Delete - 真正把数据从内存刷到 File - 真正做 compaction 和 split - RegionServer 的核心职责 - 持有并管理 Region - HMaster 决定 Region 给谁 - RegionServer 真正把 Region 跑起来 - 处理客户端读请求 - 处理客户端写请求 - 管理每个 Region 内部的 Store/MemStore/StoreFile - 运行后台维护线程:flush、compaction、WAL 滚动、split - 管理 BlockCache - 执行部分 Region 级维护操作 - Put 流程 - 客户端根据元数据找到 Region 所在的 Region Server - 请求发到该 Region Server - RegionServer 找到这个 Region 对应 family 的 Store - 先写 WAL - 在把数据写到 Store 里面的 MemStore - 后台达到条件后,MemStoreFlusher 把数据 flush 到 StoreFile - 之后 CompactSplitThread/MajorCompactionChecker 再持续维护这些文件 - Get/Scan 流程 - 客户端根据元数据找到 Region 所在的 Region Server - RegionServer 在对应 Region 的 Store 中查找 - 优先利用 BlockCache - 必要时再读 StoreFile/HFile - 利用索引和 BloomFilter 缩小无效读取 - 返回结果给客户端 - Region 是什么 - Region 定义 - Region 是一段连续的、按 RowKey 排序的行范围 - 表的层级结构 Table->Region->Store->MemStore/StoreFile->Block - 为什么要有 Region - 做分布式切片 - 支持扩展 - 支持负载均衡 - Region 结构 - 一个 Region 内部,每个 ColumFile 对应一个 Store,每个 Store 有自己的 MemStore 和若干 StoreFile - Region 如何分配 - 同一时刻,一个 Region 只由一个 RegionServer 提供服务 - 一个 RegionServer 可以持有很多 Region - HMaster 负责 Region 给谁、是否迁移、是否均衡 - RegionServer 负责真正持有 Region 并处理这个 Region 的读写 - Region 为什么需要切分 - 单个 Region 太大,迁移和恢复成本高 - 该 Region 所在 RegionServer 负载可能过重 - 并行度不够,一大段数据只能由一个 Region 服务 - 热点更集中,难以分散 - split 的目的,是把一个过大的 key range 拆分成两个更小的 key range,来提升可扩展性和分布性 - 什么时候会 split - 由 RegionSplitPolicy 决定 - 当 Region 增长到某个阈值/满足当前 split policy 的条件 - WAL 是什么 - 为什么需要 WAL - HBase 不是收到请求就立即写成 HFile - 写请求先进入内存里面的 MemStore - MemStore 是内存结构,掉电或进程崩溃会丢 - WAL 为还没落成 HFile 的持久化阶段兜底 - 写入请求里的 cells 会一直保留,直到成功持久化到 WAL 和 MemStore - MemStore 负责写入速度,WAL 负责稳定性 - 为什么叫 Write-Ahead - 先把将要发生的修改记录到日志中,再依赖后续流程把它整理进正式数据文件 - 这条写操作在正式长期存储结构整理完成之前 - 已经被写入到一个可恢复的日志里 - WAL 处在的位置/Put 操作链路 - 客户端把 Put 请求发送到目标 RegionServer - RegionServer 把这次修改追加到 WAL - 同时把数据写入目标 Store 的 MemStore - 之后后台线程再把 MemStore Flush 成 StoreFile/HFile - 最后通过 compaction 整理文件 - WAL 记录的内容 - WAL 记录的是这次写操作的增量编辑记录 - WAL 写入的内容加做 WALEdit,有 WAL.Entry、WALKey 类型来表示一条日志条目及其键 - MemStore、StoreFile/HFile、BlockCache - 概述 - MemStore:写入时的内存缓冲区 - StoreFile/HFile:Flush 之后落盘的正式数据文件 - BlockCache:读路径上的数据块缓存 - 在一个 Region 里,每个列族对应一个 Store,每个 Store 有自己的 MemStore 和若干 StoreFile - MemStore 是什么 - MemStore 是 Store 在内存中的缓冲区 - 客户端写入数据, RegionServer 会先把修改写入 WAL,并把数据存放在对应的 Store 的 MemStore - 后续达到条件后,再 flush 到磁盘形成 StoreFile - 主要解决写的快的问题,如果每次 Put/Delete 都直接改磁盘主文件,开销会很大 - 先写入内存,可以把随机小写聚合起来,再批量 flush 到磁盘 - MemStore 只是内存态,不是最终查询文件,内容会在后续 flush 后变为 StoreFile/HFile - StoreFile 是什么 - 是 Store 在磁盘上的数据文件 - MemStore 不会只 flush 一次,每次 flush 会生成新的 StoreFile - 当 BlockCache miss 且 MemStore 没有目标数据,RegionStore 会去 HFile/StoreFile 中查 - HFile 是什么 - 是底层文件格式,StoreFile 是 HBase 在 RegionServer/Store 这一层使用的数据文件抽象 - 一个 StoreFile 对应一个 HFile - BlockCache 是什么 - 是 HBase 读路径上的数据库缓存 - 缓存的是从 HFile 里读出来的 block - 解决的是读的快的问题 - Flush、Compaction、Split 的流程 - Flush 是什么 - 定义 - 把某个 Store 的 MemStore 内容写到磁盘,生成新的 StoreFile - 为什么必须 Flush - MemStore 在内存中,不能无限增长 - flush 后发生什么 - flush 完成后,这批数据会变成新的 StoreFile,被纳入该 Store 的文件集合。 - Flush 触发条件 - 内存压力触发 - 时间触发 - 运维/内部流程触发 - Compaction 是什么 - Compaction 是把一个 Store 多个 StoreFile 文件合并整理成更少的新文件 - Minor Compaction 和 Major Compaction 的区别 - Minor Compaction - 把若干个较小的 StoreFile 合并成更少的较大文件,但通常不是把该 Store 的所有文件一次性全部重写 - Major Compaction - 更彻底地重写该 Store 的文件集合 - Split 是什么 - Split 是把一个 Region 沿 RowKey 边界切成两个子 Region - Zookeeper 的作用 - 核心作用 - 主节点选举:决定谁是 active HMaster - 服务发现:让客户端知道该连谁 - 节点状态感知:知道哪些服务还活着 - 协调关键元数据入口 - 为什么 HBase 需要 ZooKeeper - HBase 把协调问题外包给 ZooKeeper,把自己更多精力放在存储和读写路径上 - Zookeeper 不做什么 - 不存储业务明细数据 - 不承担高吞吐读写 - 不等于 HMaster 实际操作 - 实操 - 启动单机 HBase - 单机模式是什么 - 单机模式是 HBase 最基础的步数形态 - 在 standalone 模式下,所有 HBase 守护进程都运行在一个 JVM 里 - Docker 启动 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/03/24/20260324134824394.png,252,116) - 进入容器进行最小验证命令 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/03/24/20260324135239051.png,625,612) - 用 Shell 建表、删表、put/get/scan - 进入 shell - hbase shell - 命令基本结构 - 表名:test - row key:row1 - 列:cf:a(列族:列限定符) - create 建表 - create 'test','cf' - 创建表 test,在表中预定义列族 cf - put 写入数据 - put 'test', 'row1', 'cf:a', 'value1' - 'test':表名 - 'row1':row key - 'cf:a':列 - 'value1':值 - 本质:往某个表的某一行、不同列写值 - scan 扫描数据 - scan 'test' - 按 RowKey 顺序把表里面的一批数据扫出来 - get 读一行数据 - get 'test', 'row1' - 按 Rowkey 精确读取某一行 - get 是点查,scan 是范围扫 - disable 和 drop - disable 的作用:先把表停用,把表从可服务状态切到不可服务状态 - drop 的作用:删除表 - 创建 namespace - 什么是 namespace - 如果创建表时未指定 namespace,则表存放在 default namespace 下 - create 'test','cf' 本质等价于 'default:test' - 为什么要有 namespace - 表分组管理 - 避免表名冲突 - 便于做配额/约束 - namespace 是 HBase 里按业务域组织表的基本单位 - hbase 存在哪些 namespace - default - 用户表默认所在 namespace - hbase - 系统 namespace,保留给 hbase 内部表 - 创建 namespace - create_namespace 'demo' - 查看 namespace - list_namespace - 在 namespace 下建表 - create 'demo:test', 'cf' - 写入和读取 - put 'demo:test', 'row1', 'cf:a', 'value1' - get 'demo:test', 'row1' - scan 'demo:test' - 删除 namespace - drop_namespace 'demo' - 设置版本数、TTL、压缩 - 列族级配置 - family 是物理和策略边界 - VERSIONS、TTL、COMPRESSION 这类配置,默认思维都是这个 family 怎么存,不是这一列怎么存 - alter 是什么命令 - alter 用来修改已有表的 schema 或表/列族相关配置 - VERSIONS - 控制什么 - 同一个 row+family+qualifier 最多保存多少个历史版本 - 怎么改 - alter 'test', NAME => 'cf', VERSIONS => 5 - TTL - 控制什么 - 这个列族里面的数据默认能活多久 - 怎么改 - alter 'test', NAME => 'cf', TTL => 2592000 - COMPRESSION - 控制什么 - 这个列族的 StoreFile/HFile 落盘时用什么压缩算法 - 怎么改 - alter 'test', NAME => 'cf', COMPRESSION => 'SNAPPY' - 验证修改 - describe 'test' - 查看 Region 分布 - web ui - localhost:16010 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/03/24/20260324154801170.png,600,400) - 简单 filter 查询 - 为什么要用 filter - 不想整表扫出来自己筛 - 只看某些 row key 前缀/只看某些列名前缀/只取前 N 行/只保留某个列值满足条件的行 - 让 RegionServer 在服务端先过滤(server-side filtering) - filter 的基本写法 - scan '表明', { FILTER => "过滤器表达式" } - 准备练习数据 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/03/24/20260324155538852.png,475,206) - 5 类简单 filter - PrefixFilter:按 row key 前缀过滤 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/03/24/20260324155813044.png,801,131) - PageFilter:限制返回多少行 - scan 'demo:filter_test', { FILTER => "PageFilter(2)" } - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/03/24/20260324160025865.png,799,138) - ColumnPrefixFilter:按列限定符前缀过滤 - scan 'demo:filter_test', { FILTER => "ColumnPrefixFilter('na')" } - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/03/24/20260324160109778.png,801,134) - SingleColumnValueFilter:按某一列的值筛行 - scan 'demo:filter_test', { FILTER => "SingleColumnValueFilter('cf', 'city', =, 'binary:Tokyo')" } - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/03/24/20260324160214742.png,941,100) - ValueFilter:按值过滤列 - 如果只是做简单的 family:qualifier:value 等值判断,推荐限制后再配 ValueFilter,这样能避免扫描无关的 family/column - 组合使用 filter - "PrefixFilter ('Row') AND PageFilter (1) AND FirstKeyOnlyFilter ()" Java 客户端开发 - Java 客户端开发 - Connection/Admin/Table - 职责边界 - Connection:集群级入口 - 封装了到实际服务器和 ZooKeeper 的连接 - Admin:管理面接口 - Table:单表数据面接口 - 代码关系 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/03/24/20260324161657372.png,584,113) - 最小 Java 示例

三月 19, 2026

nosql 复习.md

CAP 与一致性模型 - CAP 与一致性模型 - CAP 定理 - 组成 - C(Consistency,一致性):所有客户端在同一时刻读到的数据一致 - A(Availability,可用性):每个请求都能在有限时间内得到非错误响应 - P(Partition tolerance,分区容错):系统在网络分区时仍然能提供服务 - 关键结论 - 发生网络分区时,无法同时满足 C 和 A - 分布式系统里 P 几乎是必选项(网络一定会分区/超时/抖动) - 一致性 - 线性一致性 - 定义:所有操作看起来像某个全局时间顺序瞬间发生,读一定能看到最近完成的读 - 工程含义:通常需要 leader+共识+或严格 quorum+顺序约束 - 代价:跨节点协调->延迟变高;分区时更可能不可用 - 顺序一致:比线性一致弱 - 只要存在一个全局顺序能解释所有的操作,但不要去符合真实时间,不要去最近完成的写立即可见 - 因果一致:社交消息系统常见 - 保证有因果关系的操作顺序一致;无因果关系的并发操作允许不同节点看到不同顺序 - 先发帖再评论,所有人必须看到帖再评论,但两个人同时发帖,顺序可能不一样 - 最终一致:AP 常见的一致性目标 - 定义:如果不再有新的更新,所有副本都最终收敛到一个值 - 允许:短时间读到旧值,不同客户端看到不同值 - 收敛:复制、反熵、读修复、后台合并、冲突解决策略 - Quorum(N/R/W) - 数据库用 quorum 思路来调一致性/可用性 - 定义 - N:副本数 - W:写成功需要确认的副本数 - R:读成功需要读取的副本数 - 关键结论 - 若 R+W>N,读写集合必然有交集->通常能读到最新写(接近强一致,但还要考虑版本选择/时钟/并发写) - 若 R+W<=N,可能读不到最新写(更偏最终一致/读旧值概率更高) - 举例 - W=2,R=2->R+W=4>3:读写交集至少一个副本,读更可能拿到最新 - W=1,R=1->2<=3:读很可能读到旧副本 - 分区时怎么选 C/A - 更一致:提高 W 或 R,分区更容易失败 - 更可用:降低 W 或 R,分区时能继续响应,可能读旧/冲突 - 读修复与 hinted handoff - Read Repair(读修复):读时发现副本版本不一致,把新值回写到旧副本,加速收敛 - Hinted Handoff:某副本暂时不可达,协调者先把代写的 hint 记录下来,等它恢复再补写 - 冲突从哪里来?怎么解决? - AP 系统分区时允许两边都写 -> 一定会出现冲突,关键是冲突语义 - LWW(Last Write Wins) - 用事件戳/版本号选最新覆盖旧值 - 风险:依赖时钟 - 优点:实现简单,很多常见足够 - 向量时钟/版本向量 - 判断两个更新是否并发(不可比较)还是有先后关系(可比较) - 并发写需要应用层合并 - CRDT - 通过数学结构保证并发合并可收敛 - 一致性 != 事务 ACID - CAP 的 C:指副本之间对外可见的读写一致语义 - ACID 里的 C:Consistency(约束一致性)是事务前后满足约束 - 事务强一致可能跨分区不可用;最终一致系统也可以在单分区内做局部事务 - 典型场景 - 必须强一致 - 余额扣款、库存扣减不超卖、唯一性约束、权限变更立即生效 - 宁可失败重试/排队,也不能错 - 可以最终一致 - 点赞数、播放量、推荐特征、日志埋点、监控指标 - 短时间误差可接受,最终对齐即可 - 折中:读写分离 + 关键路径强一致 - 下单扣库存走强一致存储:报表/BI 走 ClickHouse 最终一致导入 - 缓存允许短暂不一致,通过失效策略、异步刷新、补偿修正 分布式存储底层 - 分布式存储底层 - 分布式存储底层总览 - Client/SDK - 路由层:根据 Key 找到 Shard/Leader - 分片层:每个分片负责一段 key 空间 - 复制层:每个分片有 N 个副本 - 一致层/共识层:决定写要等谁确认,谁是 Leader,如何提交日志 - 存储引擎层:WAL + Memtable + SST/BTREE 等 - 后台维护:compaction、rebalance、修复、反熵、GC - 数据分片 - 为什么必须分片 - 目标:水平扩展,吞吐=节点数线性增长 - 分片策略对比 - Hash 分片 - 优点:数据分布均匀,天然抗热点 - 缺点:范围查询弱,扩容时大量搬迁 - 适用:KV,用户 ID 随机读写,计数器,缓存 - Range 分片 - 优点:范围查询强 - 缺点:容易热点,需要 split/merge - 适用:按事件排序的日志、时序、OLAP 分区表 - 一致性哈希 + 虚拟节点 - 目标:扩容/缩容时减少迁移量 - 一致性哈希结论:加/减一个节点只影响环上相邻的一段 key - 虚拟节点:每台机器对应多个 vnode,负载更均衡,迁移更细粒度 - 路由:请求如何找到分片 - Client-side routing(客户端算) - Client 拿到分片拓扑,自己算目标节点 - 优点:少一跳、性能好 - 缺点:拓扑变更要更新客户端(gossip/配置中心) - Proxy/Router(中间代理) - 由 Proxy 接请求,负责路由与重试 - 优点:客户端简单,拓扑变更透明 - 缺点:proxy 可能成为瓶颈/单点 - Coordinator (协调节点) - 写入/查询可能由协调者拆分到多个分片再聚合 - 常用于分析系统/分布式 SQL - Rebalance(扩缩容/再均衡) - 扩容 - 数据迁移:搬迁期间读写怎么保证正确性 - 双写/转发:迁移期间某些 key 可能在 old/new 两处 - 限流:迁移会抢光 IO/CPU,影响线上延迟 - 策略 - 迁移期间双写,再切流 - 迁移期间转发(写 old,old 转发到 new) - 分批搬迁+限速+热点优点处理 - 稳定窗口切换元数据 - 热点问题 - 热点是什么 - Hot key:单个 key QPS 极高 - Hot partition:单个分片承担大量 key 或大量访问 - 热点为什么致命 - 分布式扩展依赖均匀分布,热点让整体吞吐被最热分片锁死 - 延迟抖动:最热节点 CPU/网卡/锁竞争导致 p99 爆炸 - 治理手段 - Key 打散(加盐/分桶) - counter:global 拆分成 counter:global:{0..99},读时聚合,写时随机桶 - 代价:读放大/需要聚合 - 读写分离 + 多副本读 - 热读 key:允许从 follower/replica 读,或者使用 cache 层 - 本地缓存 + 请求合并 - 在网关/服务端合并通同 key 并发请求,防止缓存击穿引起雪崩 - 分区拆分 - range 分片:把热区间再拆细 - 配合动态 split - 异步化/预计算 - 把热点聚合改成写日志,异步聚合出结果 - 副本复制 - Leader-Follower(主从/Primary-Replica) - 写入路径:Client->Leader 写入->复制到 followers->达到确认策略后返回 - 关键问题 - 同步复制 vs 异步复制 - 同步:延迟高但丢数据风险低 - 异步:延迟低但主挂可能丢最后一段数据 - 读策略 - 读 leader:更一致 - 读 follower:延迟小/抗压,但可能读旧 - 适用:大多数传统分布式 DB/缓存系统 - Multi-Leader(多主) - 多个 Leader 都可写,各自复制 - 冲突不可避免,需要冲突解决(LWW/向量时钟/CRDT/业务合并) - 适用:跨地域多活、离线写入,复杂度高 - Leaderless(无主,Dynamo 风格) - 任意节点可接写,写到 N 个副本 - 用 Quorum(N/R/W)控制一致性 - 配套:读修复、hinted handoff、反熵 - 适用:高可用、可水平扩展、允许最终一致的系统 - 一致写入确认:Quorum - 定义 - N:副本数 - W:写成功需要确认的副本数 - R:读成功需要读取的副本数 - 关键结论 - 若 R+W>N,读写集合必然有交集->通常能读到最新写(接近强一致,但还要考虑版本选择/时钟/并发写) - 若 R+W<=N,可能读不到最新写(更偏最终一致/读旧值概率更高) - 举例 - W=2,R=2->R+W=4>3:读写交集至少一个副本,读更可能拿到最新 - W=1,R=1->2<=3:读很可能读到旧副本 - 共识与选主 - 共识 - 只有一个 Leader(避免脑裂乱写) - 日志顺序一致(写入顺序全局一致) - 多数派提交(保证故障后不回滚) - Raft 最小正确集 - Leader 选主 - 节点有 term(任期),超时发起选举 - 多数派投票选出 Leader - 心跳维持 leader 身份 - 日志复制 - 客户端写入先到 leader - leader 追加日志并复制给 follower - follower 按 index/term 对齐(不一致就回滚到匹配点再补) - 提交规则 - leader 只有在日志被多数派复制后才能提交 - 提交后对外可见 Cassandra/HBase - Cassandra/HBase - LSM-Tree + WAL - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/22/20260222101553862.png,604,382) - LSM-Tree 要解决什么问题 - 传统 OLTP 存储引擎常见是 B+Tree - B+Tree 更新/插入会改动树上的多个节点页 - 页可能在磁盘不同位置->随机写很多 - 随机在 HDD 上很慢,在 SSD 上也不便宜 - LSM 的核心目标 - 把随机写变成顺序写/批量写 - 把整理数据到成本挪到后台异步做(Compaction) - LSM 通过先写日志+写内存+顺序刷盘成不可变文件,再后台合并文件,大幅提高写吞吐,但是读取会更复杂,且 compaction 会带来放大和抖动 - 写入流程 - 先写 WAL(顺序追加) - 把这条写操作追加到 WAL 文件末尾 - 顺序写,非常快 - 目的:宕机可恢复 - 写入 Memtable(内存有序结构) - MemTable 一般用 SkipList/红黑树等保证有序 - 写入是内存操作,快 - MemTable 满了就 Freeze(变成 Immutable) - 当前 Memetable 变成不可变,新写进入新的 MemTable - immutable 的存在时为了让刷盘不阻塞写入 - 刷盘生成 SSTable(顺序写文件) - 后台线程把 immutbale MemTable 以排序后的顺序写成 SSTable,不可变有序文件 - SSTable 通常附带 - 稀疏索引 - Bloom Filter - 数据块 - 读流程 - 步骤 - 先查 MemTable - 再查 Immutable MemTable - 再查磁盘上的多个 SSTable - 找到 Key 后还要做版本合并 - 同一个 Key 可能在很多层都有 - SSTable 不可变,更新不是原地修改,而是写一个新版本 - 旧版本会在后台 compaction 时被清理 - 读放大:SSTable 越多,读需要查的地方越多 - Bloom Filter 的作用 - 对于某个 SSTable,Bloom Filter 判断:这个 key 不可能在里面->直接跳过,不读磁盘 - 如果 Bloom 说在,才去查索引/数据块 - Delete 怎么做 - LSM 不会立刻物理删除数据,而是写入一个 Tombstone(删除标记) - Delete(key) 本质是写入 key->tombstone - 读到 tombstone 说明该 key 已经被删除 - 真正清理旧值与 tombstone,依靠 Compaction 完成 - Compaction - 目标 - 合并多个 SSTable - 丢弃被覆盖的旧版本 - 清理 tombstone - 降低读放大、回收空间 - Compaction 为什么会导致 p99 抖动 - 需要大量读旧文件 + 写新文件 - CPU 做合并、校验、压缩 - 可能把 SSD/HDD 打满,影响在线请求 - compaction 策略 - Sized-Tiered Compaction - 当 L0/L1 有若干大小相近的 SSTable,就合并成更大的 - 优点:写放大相对小,吞吐高 - 缺点:读放大可能更大 - Leveled Compaction - 各层有大小上限,保证 L1+ 之间范围重叠更可控 - 优点:读放大更小 - 缺点:写放大更高 数据建模与查询建模 - 数据建模与查询建模 - NoSQL 建模的通用原则 - Query-driven modeling(按查询建模) - RDB 常按照范式建模,再用 SQL 去适配查询 - 在 NoSQL 里更像:先确定查询,再设计主键/分区/排序/冗余表 - Denormalization - 目的:消灭 join,降低在线查询复杂度 - 代价:写入多处更新、需要处理一致性补偿 - 同一份实体多份查询视图表 - 把维度字段复制进事实表 - 预计算/物化视图 - 把聚合从读时搬到写时 - 写路径:事件 -> 聚合状态更新 - 读路径:直接读聚合结果 - 适合:排行榜、计数器、报表、指标面板 - 幂等与版本化 - NoSQL + 分布式写入经常至少一次交付,天然要支持幂等 - 热点治理 - 避免分区键过于集中 - 避免自增 RowKey - 避免单 key 计数器

二月 21, 2026