想象一座没有“总部”的城市咖啡馆网络:每家店独立经营,没有主店与分店之分;客人走进任意一家都能点单,订单按一套数学规则自动分派给某几家店;店主们靠口口相传了解“谁开着、谁歇业”;每笔订单都会在便签、白板和账本之间有序流转。这套体系不需要关店维护,想扩张就在地图上多开一家店——这就是 Apache Cassandra 的设计哲学。 Cassandra 是 Apache 基金会的分布式宽列存储数据库,被全球互联网公司用作海量数据的高可用存储底座。本文将用“无总部咖啡馆网络”这个比喻,拆解它的去中心化架构、一致性哈希、Gossip 协议、LSM-Tree 写入路径、压缩合并等核心机制。你不需要任何前置知识,跟着故事走,就能对 Cassandra 建立完整直觉。

岗位关联

对 AI 从业者来说,理解 Cassandra 架构能帮你在海量数据场景中正确选型与排障——无论是为推荐系统构建特征库,还是为 AI 训练搭建数据管道,你都能判断它是否合适、瓶颈在哪、如何扩展。

目录
  1. 可视化演示
  2. 去中心化架构
  3. 一致性哈希
  4. Gossip协议
  5. 数据复制一致性
  6. 写路径与LSM树
  7. 读路径机制
  8. 压缩与清理
  9. 高可用扩缩容
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示Cassandra架构的核心机制,通过架构/数据/状态/性能四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Cassandra架构:去中心化 去中心化架构与一致性哈希 Gossip协议与节点发现 核心机制可视化 Cassandra架构整体架构与关键组件

去中心化架构

设想一个遍布全城的咖啡馆网络,但没有任何“总部大楼”。每家店主独立进货、独立招聘,没有老板发号施令;服务规则靠大家约定俗成。客人走进任何一家店都能点单,因为每一家店都掌握完整的服务章程。如果某一家停业,其他店照常运转,客人们几乎感觉不到变化。

Cassandra 的架构与这个“无总部咖啡馆网络”如出一辙。集群中每个节点(node)地位完全平等,没有主从之分——这就是去中心化(decentralized)。客户端可以连接任意节点发起读写,此时这个节点自动扮演“协调者”(coordinator),负责把请求路由到正确的数据节点,就像客人找任何一位店员点单都可以,店员自己会去后厨协调。

逻辑上,所有节点构成一个“令牌环”(token ring),每个节点负责环上的一段连续区间。存数据时,先计算分区键的哈希值,找到负责该区间的节点,再落地写入。因为不存在主节点,任何一个节点宕机,其他节点照常服务,从设计上就消除了“单点故障”。这正是分布式存储最看重的基础能力。

回看咖啡馆比喻:单店再精致也可能临时关门,但“无总部网络 + 订单分派到多家店”的设计,让整个网络不会因为任何一家店的意外而停摆。去中心化架构就是这样把“单点故障”从系统基因里抹掉了。

Acluster=1−(1−a)NA_{\text{cluster}} = 1 - (1 - a)^N
这是一个简化可用性模型:假设单节点可用率为 a,副本数为 N,则集群整体可用率 ≈ 1 减去所有副本同时失效的概率。
  1. 单点可用率a 表示一台机器一年中正常工作的比例,比如 99%。
    a=0.99a = 0.99
  2. 单点不可用率1 - a 是这台机器可能在某一时刻宕机的概率。
    1−a1 - a
  3. 所有副本同时不可用N 个副本同时失效的概率是 (1-a)^N,副本数越多这个值越小。
    (1−a)N(1 - a)^N
  4. 集群整体可用率1 减去这个极小概率,就是整个集群仍能对外服务的概率。
    1−(1−a)N1 - (1-a)^N

一致性哈希

无总部不代表无秩序。“订单该给哪家店”必须有一套自动规则,不能靠人肉分配。咖啡馆网络的约定是:每位店主在入职时把自己的店名哈希成一个数字,对应到地图环上的一个坐标;同样,每张订单的订单号也哈希成环上的坐标。订单从自己的坐标出发,顺时针沿着环走,遇到的第一家店就是它的“归属店”。这套规则就是一致性哈希(consistent hashing)。

在 Cassandra 中,这个“地图环”的取值空间是 0 到 2^127-1 的超大数字环。每个节点通过自己的 ID 算出环上的位置;每条数据通过分区键(partition key)算出环上的位置。数据顺时针遇到的第一个节点,就是它的 owner(归属节点)。如果 owner 宕机,数据由下一个节点继续负责,不会出现“不知道该找谁”的窘境。

虚拟节点(vnode)是工程上的重要优化:每家店可以把自己的地盘拆成多个不连续的小区间,而不是只占一个点。这样环被切得更碎,数据分布更均匀,能有效避免“某家店恰好处在热门区间而忙死”的热点问题。节点增删时,也只需要迁移相邻小区间的数据,影响面非常小。

回到咖啡馆:新店开张,不用重新培训所有人、不用把所有订单重新分一遍——只要在环上“插队”占一段地盘,把相邻分店的一部分订单接过来即可。一致性哈希让“加店”和“关店”都变成局部操作,这正是水平扩展能日常化的关键。

poskey=h(partition_key) mod 2127\text{pos}_{\text{key}} = h(\text{partition\_key}) \bmod 2^{127}
分区键经过哈希函数映射到环的地址空间 [0, 2^127),这就是数据在环上的“坐标”。
  1. 哈希函数 h任意字符串(订单号、用户 ID)经过哈希运算变成一个很大的整数。
    h(partition_key)h(\text{partition\_key})
  2. 模 2^127把大整数压缩到环的地址范围 [0, 2^127)。
     mod   2127\bmod \; 2^{127}
  3. 节点同样定位每个节点 ID 用相同公式算出自己在环上的位置。
    posnode=h(node_id) mod 2127\text{pos}_{\text{node}} = h(\text{node\_id}) \bmod 2^{127}
  4. 顺时针寻找 owner从数据坐标出发,找第一个 token 不小于自己的节点。
    owner=min⁡{ni∣posni≥poskey}\text{owner} = \min\{n_i \mid \text{pos}_{n_i} \ge \text{pos}_{\text{key}}\}

Gossip协议

无中心的网络里,店主们怎么知道“谁还在营业、谁出去旅游了”?靠的不是公告栏(没有中心),而是“小圈子口口相传”。每隔一秒,每个店主在小群里发一句“我这里一切正常”,顺便转发自己听到的别人的消息。消息一传十、十传百,很快全网络都知道每家的最新状态。这就是 Gossip 协议(八卦协议)的直觉——像八卦一样,消息在人群里快速扩散。

Cassandra 中每个节点每秒向随机选择的节点发送心跳消息,消息里带着自己知道的其他节点状态,并附一个版本号。收到消息的节点如果发现版本号更新,就更新自己的记录,并继续向其他节点传播。最终,所有节点在几秒内都能收敛到一致的集群状态视图。这个过程只传输元数据(谁活着、谁死了、谁的数据范围是什么),不搬运真正的用户数据。

新节点加入时,通过种子节点(seed node)进入集群。种子节点就像“引路人”,帮新店长加入聊天圈、认识第一批邻居。故障检测则依赖心跳超时:如果超过一段时间收不到某个节点的心跳,就用统计方法计算一个“存疑指数” phi,phi 越大说明该节点越可能真的宕机,而不是网络偶发抖动。

咖啡馆里,店主们聊的是“谁开门谁休息”这类状态信息,而不是在群里直接搬运整本菜单。Gossip 协议同样只传递“状态八卦”,真正的大块数据走的是哈希环决定的专属通道。这样既保证了全集群信息流通,又不让通信流量压垮网络。

ϕ=Tnow−Tlast心跳间隔均值\phi = \frac{T_{\text{now}} - T_{\text{last}}}{\text{心跳间隔均值}}
这是 Phi Accrual 故障检测的简化形式:把“沉默时长”除以历史平均心跳间隔,得到一个可比较的指数。
  1. 当前时刻T_now 是现在的时间戳。
    TnowT_{\text{now}}
  2. 上次心跳时刻T_last 是该节点最后一次广播心跳的时间。
    TlastT_{\text{last}}
  3. 沉默时长归一化两者相减得到“多久没说话了”,再除以历史平均间隔,消除不同网络环境的差异。
    Tnow−Tlast心跳间隔均值\frac{T_{\text{now}} - T_{\text{last}}}{\text{心跳间隔均值}}
  4. 判定阈值phi 超过阈值(如 8)才判定节点不可用,避免网络抖动造成误杀。
    ϕ>8  ⟹  节点不可用\phi > 8 \implies \text{节点不可用}

数据复制一致性

单靠哈希环分配,数据还缺一道保险:如果归属店突然关门,订单就丢了。于是咖啡馆网络规定:每笔订单抄送多份,抄几份由“副本因子”(Replication Factor,RF)决定。RF=3 意味着每个订单同时抄给 3 家分店保管,任何一家关门,另外两家依然能提供完整订单。

写入时,协调者把数据同时发给所有副本节点,等足够多的副本确认后才向客户端报告“写入成功”。这个“足够多”就是一致性级别(consistency level)。最常用的 QUORUM 级别要求“超过半数副本确认”;ONE 级别只要求一个副本确认,延迟更低但一致性更弱;ALL 级别要求所有副本确认,最安全但最慢。用户可以按业务需求在读写两端分别选择。

还有一个巧妙机制叫 Hinted Handoff(提示移交):如果某个副本节点刚好离线,协调者会把数据暂时保存在自己这里,附上一张“小纸条”指向目标节点。等目标节点恢复后,协调者再补发数据。就像吧台帮请假同事记了一笔账,等同事回来照单补录,让系统不因短暂故障而阻塞写入。

想想咖啡店的抄送制度:多数店对得上账,就确认订单有效,不必等所有店都回话。这种“折中”正是分布式系统的艺术——在延迟、可用性和一致性之间,用可调参数找到最适合业务的平衡点。

QUORUM=⌊RF2⌋+1\text{QUORUM} = \left\lfloor \frac{\text{RF}}{2} \right\rfloor + 1
QUORUM 即“过半数”:副本因子为 RF 时,需要至少 floor(RF/2)+1 个副本确认。
  1. 副本因子一份数据复制到 RF 个节点,比如 RF=3。
    RF=3\text{RF} = 3
  2. 半数向下取整3 ÷ 2 = 1.5,向下取整为 1。
    ⌊32⌋=1\left\lfloor \frac{3}{2} \right\rfloor = 1
  3. 超过半数再加 1,得到 QUORUM = 2,即至少 2 个副本确认。
    ⌊32⌋+1=2\left\lfloor \frac{3}{2} \right\rfloor + 1 = 2

写路径与LSM树

顾客点单时,服务员先用最快速度把订单记在便签(Commit Log)上,防止丢单;这步是顺序追加写入磁盘,速度极快。接着把订单内容写到吧台的白板(Memtable)上——白板在内存里,读写都快。当白板写满,后台线程就把白板内容誊写成一个新的正式账本 SSTable,换一块新白板继续接单。整个过程不用改旧账本,因为新订单永远先进新白板。

这套“便签 → 白板 → 账本”的流程,就是 LSM-Tree(Log-Structured Merge-Tree,日志结构合并树)的思想:写入全部走顺序追加,先在内存聚合,再批量落盘。相比传统 B+ 树每次写入都要随机修改磁盘页面,LSM-Tree 把随机写转化成了顺序写,写入吞吐量能高出数倍。这也是 Cassandra 被誉为“写性能怪兽”的根本原因。

SSTable 是一个不可变的(immutable)数据文件:一旦誊写完成就永远不再修改。这意味着没有“原地更新”的操作,所有更新都是产生新版本数据。旧的账本虽然不再被引用,但物理上还占着磁盘,需要后续的合并压缩(compaction)来收拾——这是第 7 节的主题。不可变性也带来一个好处:SSTable 可以被安全地缓存、压缩和并行读取,无需加锁。

再回到咖啡馆:便签防丢失,白板提速度,账本做沉淀,三层各司其职。订单从来不会被“就地修改”,只会被更新版本覆盖逻辑状态。这套流水线的代价是磁盘写得多一点,但换来的是简单、高效、并发友好的写入模型。

写放大=SdiskSapp\text{写放大} = \frac{S_{\text{disk}}}{S_{\text{app}}}
写放大衡量“应用写 1 字节,磁盘实际写了几字节”。LSM 的多次合并会让这个值大于 1。
  1. 应用写入量S_app 是客户端程序提交的数据字节数。
    SappS_{\text{app}}
  2. 磁盘写入量S_disk 包括 Commit Log、SSTable 落盘、后台压缩合并等全部写盘流量。
    SdiskS_{\text{disk}}
  3. 比值含义理想值是 1,但 LSM-Tree 因为多次合并,实际通常在 10~30 之间。
    SdiskSapp\frac{S_{\text{disk}}}{S_{\text{app}}}

读路径机制

顾客来取订单,服务员怎么快速找到它?先看吧台白板(Memtable),那里是最新的订单;如果白板没有,再去翻正式账本(SSTable)。但账本越积越多,一本本翻太慢了。于是每本账本封面都贴了一个“快速索引”——Bloom Filter(布隆过滤器),它能直接告诉你“这本账本里肯定没有这个订单”或“可能有”。大多数账本在第一步就被跳过,只有少数账本需要真正翻开查找。

读请求同样由协调者发起:它会向拥有该数据的所有副本节点并发发出请求,各副本返回自己保存的版本。协调者比较时间戳,把最新版本返回给客户端。如果发现某些副本的版本陈旧,还可以触发“读修复”(read repair),用最新版本纠正过期副本——相当于取餐时顺便帮店主把抄错的账本改过来。

删除操作在 Cassandra 中很有意思:数据不会立即被抹掉,而是写入一个“删除标记”(tombstone,墓碑)。读取时看到墓碑,就视该数据为不存在。墓碑本身仍然占用磁盘空间,要等到压缩阶段才会被真正清除。这也是 Cassandra 中“DELETE 后磁盘不释放”现象的来源。

回想取餐场景:白板最先被看,账本靠目录索引快速跳过,多本账本之间比版本号取最新,被取消的订单靠标记过滤。读路径的所有设计都围绕一个目标——用最小的成本,找到最新版本的答案。

P误判≈(1−e−knm)kP_{\text{误判}} \approx \left(1 - e^{-\frac{k n}{m}}\right)^k
Bloom Filter 的误判率公式:用 m 位的位数组、k 个哈希函数存储 n 个元素时,查询的假阳性概率。
  1. 位数组长度m 是 Bloom Filter 底层位数组的比特数。
    mm
  2. 元素个数n 是已经插入过滤器的 key 数量。
    nn
  3. 哈希函数个数k 表示每个 key 会被 k 个哈希函数映射到位数组的 k 个位置。
    kk
  4. 误判概率查询时这 k 个位置恰好都被其他元素置 1 的概率,就是假阳性率。
    (1−e−knm)k\left(1 - e^{-\frac{k n}{m}}\right)^k

压缩与清理

账本日积月累,吧台后面的架子越来越挤,找一本账本越来越慢。于是每周打烊后,店员做一次“账本合订”:把许多小账本合并成一册大账本,合并过程中顺手把标着“已取消”的订单页撕掉。这个过程就是 compaction(压缩合并)。

Cassandra 提供了多种压缩策略。最常见的是 STCS(SizeTiered Compaction Strategy,大小分层压缩):把大小相近的 SSTable 归为一组,合并成更大的文件。它的优点是实现简单、写放大相对低,适合写多读少的场景。另一种是 LCS(Leveled Compaction Strategy,分层压缩):把 SSTable 按层级组织,每层内 key 范围不重叠,读取时最多查少数几个文件,读性能更好,但后台合并更频繁、写放大更高。选择哪种策略,本质是在“读放大”和“写放大”之间做取舍。

压缩在后台自动运行,会占用一部分磁盘 IO 和 CPU。如果压缩跟不上写入速度,SSTable 数量会无限增长,读性能就会劣化。因此存储工程师的日常运维中,监控 SSTable 数量、压缩停顿时间和磁盘吞吐是必做功课。

账本不整理,终将堆满库房;数据不压缩,磁盘终将被墓碑和冗余文件占满。压缩虽然有后台开销,但它是 LSM-Tree 模型保持读性能、回收磁盘空间的“清道夫”,必不可少。

Smerged=∑i=1kSiS_{\text{merged}} = \sum_{i=1}^{k} S_i
合并后的 SSTable 大小约等于参与合并的所有文件大小之和(逻辑上会有去重,但物理大小接近总和)。
  1. 参与合并集合S_1 到 S_k 是这次压缩选中的 k 个小文件。
    {S1,S2,…,Sk}\{S_1, S_2, \dots, S_k\}
  2. 合并结果S_merged 是新产生的大 SSTable 的大小。
    SmergedS_{\text{merged}}
  3. 查询成本变化合并前读一条数据最坏要查 k 个文件,合并后只需要查 1 个。
    O(k)→O(1)O(k) \to O(1)

高可用扩缩容

生意火了,想开新店。流程并不复杂:新店长先通过种子节点进入“聊天圈”(Gossip),在哈希环上选一个位置宣布自己的地盘,然后相邻的老店把属于这段区间的数据迁移过来。整个过程中所有老店照常营业,客人完全无感知——这就是 Cassandra 引以为傲的在线水平扩展。

缩容同理:要关掉一家店,先把它的数据迁移给相邻节点,再让它退出环。由于一致性哈希的特性,数据迁移只影响环上的相邻区间,比例大约是新节点数分之一。相比许多数据库“扩容要停机重建”的重流程,Cassandra 把扩缩容变成了日常运维操作,这也是它能支撑千万级 QPS 规模的原因。

更进一步,Cassandra 支持多数据中心部署:可以在北京、上海、深圳各建一个“分店群”,每个数据中心内部用一致性哈希和 Gossip 管理,数据中心之间通过跨 DC 复制同步数据。即使整个机房断电,另一个数据中心的副本依然可以继续提供读取服务,真正做到“可用性优先”。

咖啡馆网络开新店,不需要关掉整条街重新规划;Cassandra 加节点,也不需要停服重建索引。这套“插队占位 + 邻店让渡”的机制,让水平扩展从“年度大项目”变成了“随手加一台机器”的日常操作。

迁移比例≈1N+1\text{迁移比例} \approx \frac{1}{N+1}
加入一个新节点后,环上大约有 1/(N+1) 的数据需要从旧 owner 迁移到新节点。
  1. 原有节点数N 是当前集群的节点总数。
    NN
  2. 加入新节点后总数N+1,环被多切一段,每个节点平均区间变小。
    N+1N+1
  3. 影响范围只有新节点抢占的那段区间对应的旧 owner 需要迁出数据,比例约 1/(N+1)。
    1N+1\frac{1}{N+1}

AI 应用场景

AI 训练日志采集管道

阶段:数据接入与预处理

解决的问题:训练推荐模型需要海量用户行为日志(点击、滑动、购买),每天数十亿条。传统关系型数据库随机写入很快成为瓶颈,且扩展困难。Cassandra 的 LSM-Tree 顺序写模型能承担每秒百万级写入,同时天然支持按时间和用户 ID 分区,为下游 AI 训练提供稳定数据源。

具体案例:一家游戏公司的埋点系统,用 Cassandra 存储玩家行为事件,按 (日期, 玩家ID) 作为分区键。数据先进入 Kafka 清洗,再由流处理程序批量写入 Cassandra。训练团队既可以用 SELECT ... WHERE player_id=? 查询单个用户的完整行为序列,也可以按日期范围扫描做批量特征统计。

物联网设备状态存储

阶段:实时数据存储与特征提取

解决的问题:智能工厂中每台设备每秒上报状态,百万设备意味着每秒百万级写入。设备状态天然按设备 ID 聚合,Cassandra 的宽列模型正好把一个设备的所有传感器数据放在同一行(或邻近列),读取连续高效;AI 异常检测模型可以低延迟获取设备状态序列。

具体案例:某新能源电池工厂在每条产线上部署传感器,采集温度、电压、电流等指标。Cassandra 以设备 ID 为分区键,每分钟一个时间桶,AI 模型实时读取最近 N 分钟数据,预测设备故障概率,提前触发维护工单。

推荐系统在线特征库

阶段:在线推理特征读取

解决的问题:推荐系统在线推理需要在毫秒级内读取用户最新行为特征。传统缓存系统(如 Redis)内存成本高,且扩展受内存上限约束。Cassandra 高可用、可水平扩展的特性,使其成为特征存储的流行选择:用户特征按用户 ID 分区,热点用户天然落在同一节点区间,读延迟稳定。

具体案例:某短视频推荐平台把用户最近 100 条交互行为存放在 Cassandra 中,线上推理服务每次请求读取特征向量。大促高峰期通过临时加节点水平扩展,不需要停机,整个集群在分钟级完成扩容。

Python 演示

python60 行
import hashlibfrom collections import defaultdict  class SimpleCassandraRing:    """极简实现:用一致性哈希环模拟Cassandra数据分布与副本复制"""     def __init__(self, nodes=None, rf=3):        self.ring = []          # [(token, node_id), ...],按token升序        self.rf = rf        self.node_data_map = defaultdict(dict)        for node in nodes or []:            self.add_node(node)     def _hash(self, key):        return int(hashlib.md5(key.encode()).hexdigest(), 16)     def add_node(self, node_id):        token = self._hash(node_id)        self.ring.append((token, node_id))        self.ring.sort()     def remove_node(self, node_id):        self.ring = [(t, n) for t, n in self.ring if n != node_id]     def find_owner(self, key):        pos = self._hash(key)        for token, node_id in self.ring:            if token >= pos:                return node_id        return self.ring[0][1] if self.ring else None     def write(self, key, value):        owner = self.find_owner(key)        idx = next(i for i, (t, n) in enumerate(self.ring) if n == owner)        for k in range(self.rf):            node = self.ring[(idx + k) % len(self.ring)][1]            self.node_data_map[node][key] = value     def read(self, key):        owner = self.find_owner(key)        idx = next(i for i, (t, n) in enumerate(self.ring) if n == owner)        for k in range(self.rf):            node = self.ring[(idx + k) % len(self.ring)][1]            if key in self.node_data_map[node]:                return self.node_data_map[node][key]        return None     def status(self):        return {n: len(self.node_data_map[n]) for n, _ in self.ring}  if __name__ == "__main__":    cluster = SimpleCassandraRing(["node-a", "node-b", "node-c", "node-d"], rf=3)    cluster.write("user:1001", {"name": "Alice", "level": 5})    cluster.write("user:1002", {"name": "Bob", "level": 3})    print("读取 user:1001 ->", cluster.read("user:1001"))    print("当前各节点数据量:", cluster.status())    cluster.add_node("node-e")    print("水平扩展后各节点数据量:", cluster.status())
目的:用 100 行 Python 模拟 Cassandra 的一致性哈希环、副本复制和节点水平扩展,让你直观看到数据如何被自动分配到不同“分店”。
  1. self.ring.append((token, node_id))把节点的哈希 token 放入环数组并排序,保证“顺时针查找”从低到高有序
  2. if token >= pos: return node_id从数据哈希位置出发,顺时针找第一个 token 不小于它的节点;若走完一圈都没有,就回到第一个节点
  3. for k in range(self.rf)指定副本因子 rf,让数据同时复制到 owner 以及后面 rf-1 个节点
  4. self.node_data_map[node][key] = value把数据写入节点对应的字典,模拟每个节点的本地存储
  5. cluster.add_node("node-e")演示水平扩展:新增节点后,环自动重新排序,新数据会分布到新节点上

常见误区

误区很多初学者以为 Cassandra 和 MySQL 一样,写入成功后立刻就能读到最新数据,把它当作强一致性数据库来设计业务。

正解Cassandra 提供的是“可调一致性”(tunable consistency)。默认常用级别(如 ONE、QUORUM)下,不同副本之间可能存在短暂延迟,属于最终一致模型。只有选择 LINEARIZABLE 等高级别时才近似强一致,但代价是性能下降。就像咖啡馆网络:多数店对得上账就告诉顾客“订单已确认”,但个别分店可能还没抄完账本。

为何常见:Cassandra 的官方文档常强调“high availability“和”no single point of failure”,新手容易把高可用等同于强一致,忽略了二者是分布式系统中互相权衡的两端。

误区听到“宽列存储”就以为是列式数据库,觉得它和 ClickHouse、BigQuery 一样适合大规模聚合分析。

正解Cassandra 是“宽列存储”(wide-column),数据仍按行组织,一行可以有海量列,但物理存储布局是行优先的;它擅长按行键(分区键)做点查和范围查,不擅长跨行聚合和复杂 JOIN。想用一条 SQL 做数亿行分组统计,Cassandra 会让你失望。它就像咖啡馆的订单账本:按“哪桌点了什么”记录,而不是按“所有订单的咖啡种类”汇总记录。

为何常见:术语中的“列”字极具迷惑性,加上 NoSQL 概念普及不够系统,很容易和列式 OLAP 引擎混为一谈。

误区执行 DELETE 删除大量数据后,发现磁盘空间并没有明显减少,于是怀疑是 bug。

正解Cassandra 的删除只是写入一条 tombstone(墓碑)标记,数据本身仍然存在磁盘上,读取时会被过滤掉;物理空间要等到 compaction 阶段才会真正回收。如果长时间不触发压缩,墓碑会占用大量空间,甚至拖慢读性能。这是 LSM-Tree 存储模型的固有特性,不是故障。

为何常见:传统关系型数据库的 DELETE 会立即释放页空间,用户把这份直觉迁移到 Cassandra 上,自然产生误解。

误区以为只要节点足够多,真实数据的哈希值自然会均匀落在每个节点上,虚拟节点只是锦上添花。

正解哈希函数的输出在统计上是均匀的,但真实世界的 key 集合是有限的、有偏的,样本量不大时分布可能严重倾斜。没有 vnode,节点少时几台机器可能扛着绝大多数数据。虚拟节点把环切分成数千个微小区间,让每台物理节点承担许多分散的小段,数据分布才会接近均匀,热点概率大幅下降。

为何常见:教科书上“哈希随机分布”的理想假设,让初学者忽略了工程中样本偏斜与节点粒度对均衡性的实际影响。

面试题

Q1. Cassandra 的整体架构属于以下哪种模式?

单选题 · easy

  • A.主从复制模式
  • B.无中心对等模式← 正确答案
  • C.共享存储模式
  • D.单机单库模式

答案:B。Cassandra 是去中心化的对等架构,所有节点地位平等,客户端可连接任意节点读写。A 是 MySQL 主从复制;C 是 Oracle RAC 等共享存储方案;D 是单机数据库,都不符合 Cassandra 的设计。

Q2. Cassandra 决定一条数据存储在哪个节点的核心策略是什么?

单选题 · easy

  • A.轮流分配请求
  • B.一致性哈希分配← 正确答案
  • C.主节点指派存储
  • D.随机选择存储节点

答案:B。Cassandra 使用一致性哈希,把数据 key 和节点 ID 都哈希到环上,数据顺时针归到第一个遇到的节点。A/B/C/D 都不是 Cassandra 的数据分布机制,其中“主节点指派”完全不成立,因为 Cassandra 没有主节点。

Q3. Cassandra 属于以下哪类数据库?

单选题 · easy

  • A.关系型事务数据库
  • B.宽列族存储数据库← 正确答案
  • C.图数据库存储系统
  • D.内存键值缓存系统

答案:B。Cassandra 是宽列(列族)存储数据库,按行键组织数据,每行可包含海量列。它不是关系型数据库(无 JOIN、无 ACID 事务),不是图数据库,也不是纯内存缓存(数据持久化在磁盘)。

Q4. 当副本因子 RF=3 时,QUORUM 一致性级别要求几个副本确认写入成功?

单选题 · mid

  • A.需要 1 个副本确认
  • B.需要 2 个副本确认← 正确答案
  • C.需要 3 个副本确认
  • D.需要 0 个副本确认

答案:B。QUORUM 的计算公式是 floor(RF/2)+1,RF=3 时 floor(3/2)+1=2,因此至少 2 个副本确认即可报告写入成功。这既保证了多数派的一致性,又不需要等待所有副本,是延迟与可靠性的折中。

Q5. 下列哪些组件属于 Cassandra 的写入路径?

多选题 · mid

  • A.提交日志 Commit Log← 正确答案
  • B.内存表 Memtable← 正确答案
  • C.字符串表 SSTable← 正确答案
  • D.二进制日志 Binlog

答案:ABC。Cassandra 写路径是“Commit Log → Memtable → 刷盘为 SSTable”。Commit Log 保证崩溃恢复,Memtable 在内存中缓存,SSTable 是不可变的磁盘文件。Binlog 是 MySQL 的二进制日志,Cassandra 没有该概念。

Q6. Gossip 协议在 Cassandra 中的主要作用有哪些?

多选题 · mid

  • A.传播节点状态与元数据← 正确答案
  • B.直接服务客户端读写
  • C.发现故障节点并通知← 正确答案
  • D.承担用户数据全量复制

答案:AC。Gossip 每秒交换节点心跳与元数据,让集群快速感知节点存活状态。服务客户端读写是协调者的职责;用户数据的复制走的是哈希环协调后的写路径,不依赖 Gossip 承担数据流量。

Q7. 在 Cassandra 中执行 DELETE 删除数据,最初发生的是什么?

单选题 · mid

  • A.立即物理清除数据
  • B.写入墓碑标记删除← 正确答案
  • C.覆盖旧数据并同步
  • D.触发整表重建索引

答案:B。Cassandra 的删除是“逻辑删除”,写入一条 tombstone 墓碑记录,读取时自动过滤;物理空间要等 compaction 才会回收。A 是关系型数据库行为;C 不是 SSTable 的不可变语义;D 不存在此操作。

Q8. 某节点短暂离线,写操作被协调者暂时保存、等该节点恢复后再补发,这个机制叫什么?

单选题 · hard

  • A.读修复机制
  • B.提示移交机制← 正确答案
  • C.幂等重做机制
  • D.热数据迁移机制

答案:B。Hinted Handoff(提示移交)在副本节点短暂不可达时,把写数据暂时保存在协调者本地并记录目标节点,恢复后补发。读修复发生在读取路径上纠正过期副本;幂等重做与热迁移都不是 Cassandra 的标准术语。

Q9. 为什么 Cassandra 选用 LSM-Tree 而不是 B+ 树作为存储结构?

多选题 · hard

  • A.顺序写为主写吞吐更高← 正确答案
  • B.完全不需要读取操作
  • C.后台合并平衡读写放大← 正确答案
  • D.完全不需要内存即可运行

答案:AC。LSM-Tree 把随机写改成顺序追加写,大量写入在内存聚合后批量落盘,写吞吐远高于 B+ 树;后台 compaction 在后台合并文件,用额外的写放大换取持续稳定的写性能。LSM 仍然需要读操作,也必须依赖内存缓冲 Memtable。

Q10. 关于一致性哈希中的虚拟节点(vnode),下列说法最准确的是?

单选题 · extreme

  • A.环被切分更细分布更均匀← 正确答案
  • B.数据不再需要复制到多个节点
  • C.节点数量必须保持偶数幂
  • D.哈希函数碰撞概率完全消失

答案:A。vnode 把每个物理节点拆成多个虚拟区间,让环被切得更碎,数据分布更均匀,减少热点。vnode 不改变副本因子,节点数量也无需是偶数幂,更不会消除哈希碰撞——它只解决“样本偏斜导致负载不均”的工程问题。