(专业系统化+生动类比+全流程实操+正反案例对比)

文档版本:V1.0 | 适用版本:Apache Cassandra 4.x | 面向人群:从0入门的初学者、需生产落地的开发/运维工程师


开篇:先搞懂「Cassandra 到底是什么?」

一句话生动定义

Cassandra 是一个「能扛住亿级流量、永远不宕机、随便加机器就能扩容的分布式NoSQL数据库」,是抖音、美团、Netflix、苹果等大厂用来存海量用户行为、订单、消息数据的核心数据库。

如果用你已经熟悉的快递站类比贯穿全文:

  • MySQL = 小区楼下的夫妻店超市,东西全,但人多了就挤不动,老板不在就关门

  • Redis = 家门口的快递自提柜,拿东西超快,但只能放小件

  • Cassandra = 全国连锁的大型智能快递站,有几百上千个快递柜,没有唯一的站长,任何一个柜子坏了都不影响全站,快递多了就加柜子,永远能接件、永远能取件。

专业严谨定义

Apache Cassandra 是一款去中心化、高可用、线性可扩展、面向列的分布式NoSQL数据库,专为海量结构化/半结构化数据的高并发写入、低延迟查询场景设计,无单点故障,支持跨数据中心部署,是目前互联网行业海量时序数据、用户行为数据的首选存储方案之一。

它的诞生背景

2007年,Facebook为了解决其收件箱系统的海量消息存储问题,结合了Google BigTable的数据模型和Amazon Dynamo的分布式架构,开发了Cassandra,2008年开源捐赠给Apache软件基金会,目前已成为Apache顶级项目,是分布式数据库领域的标杆产品。


第一章:Cassandra 核心特性:为什么大厂都用它?

每个特性都用「快递站类比+专业解释+适用场景」三维讲解,彻底搞懂它的核心价值。

核心特性

快递站生动类比

专业解释

适用场景

去中心化无主架构

快递站没有唯一的「站长」,每个快递柜都是平等的,任何一个柜子都能收件、取件,不会因为某个柜子坏了、站长不在,全站就停业

集群中所有节点都是对等的,无主从之分,不存在单点故障。客户端可以连接任何一个节点执行读写操作,单个节点宕机完全不影响集群可用性

金融支付、外卖订单、即时消息等不允许停机的核心业务

线性可扩展

快递站快递太多了?直接加新的快递柜就行,加10个柜子,收件能力就翻10倍,不用改造原有站点,零停机扩容

集群扩容无需停机,新增节点会自动同步数据、分担流量,集群性能随节点数量线性增长。从3个节点扩容到300个节点,操作成本几乎不变

电商大促、直播带货等流量突增的场景,需要快速扩容扛流量

极致高性能写入

快递员送件,不用当场把快递摆到指定格子里,只要先在「收件登记本」上记一笔,就可以告诉用户「收件成功」,后续空闲了再整理快递,收件速度永远拉满

采用「日志先行+内存写+顺序落盘」的写入机制,无随机IO,写入性能拉满,单集群可支持每秒百万级的写入请求,写入性能远高于传统关系型数据库

物联网设备上报、用户行为埋点、日志采集等高并发写入的海量数据场景

强韧性高可用

同一个快递,会复制3份放到不同的快递柜里,哪怕2个柜子坏了,还有1个柜子能取到快递;甚至北京的站点坏了,上海的站点还能正常提供服务

支持多副本机制(默认3副本),可配置跨机架、跨数据中心副本,副本同步策略灵活可调,支持最终一致性/可调一致性,满足不同业务的可用性需求

跨地域部署的业务、容灾要求极高的金融级业务

灵活的查询模型

提前给快递定好「取件码规则」(分区键),用户报取件码,1秒内就能找到快递;支持按取件码前缀、时间范围精准筛选

基于分区键+聚类键的查询模型,支持精准匹配、范围查询、排序、分页,可满足绝大多数业务查询需求,同时严格限制低效的全表扫描

用户订单查询、设备时序数据查询、消息记录查询等固定模式的查询场景


第二章:Cassandra 核心设计原理:为什么它永远不宕机、写入这么快?

这一章是Cassandra的「内功心法」,搞懂底层原理,你就不会再用错,所有最佳实践都源于底层设计。我们依然用快递站类比,把复杂的分布式原理讲透。

2.1 去中心化架构:无主设计,彻底告别单点故障

传统数据库的痛点(主从架构)

MySQL的主从架构,就像快递站只有一个「站长柜」,所有快递必须先经过站长柜,再同步到其他柜子。站长柜坏了,全站就瘫痪了,必须先恢复站长柜才能正常营业——这就是单点故障

Cassandra的无主架构

Cassandra集群里的所有节点完全平等,没有主从之分:

  1. 客户端可以连接任何一个节点(这个节点会成为本次请求的「协调节点」)

  2. 协调节点会根据数据的分区键,算出数据该存在哪些节点上,直接转发请求

  3. 任何一个节点宕机,协调节点会自动把请求转发给其他副本节点,业务完全无感知

生动总结:就像快递站的任何一个柜子都能收件、取件,哪怕一半柜子坏了,剩下的柜子依然能正常营业,永远不会关门。

2.2 一致性哈希:数据怎么均匀分布到所有节点?

这就是我们上一章讲的「魔法计算器」,也是分区键的底层核心原理,这里超级细腻地拆解全过程。

第一步:什么是哈希环?

我们把0~2³²的数字,首尾相连围成一个圆环(哈希环),集群里的每个节点,都会根据自己的IP/主机名算出一个哈希值,对应到哈希环上的一个位置。

就像我们把快递站的10个快递柜,按编号均匀摆放在一个圆形的跑道上,每个柜子占一个固定的位置。

第二步:数据怎么定位节点?

当你写入一行数据时:

  1. Cassandra 会对这行数据的分区键做哈希计算,得到一个0~2³²之间的数字

  2. 拿着这个数字,在哈希环上顺时针找第一个遇到的节点,这个节点就是数据的「主副本节点」

  3. 按照你配置的副本数,继续顺时针找后续的节点,作为副本节点

快递站类比

  • 快递单号(分区键)放进魔法计算器,算出一个跑道上的位置

  • 顺时针第一个遇到的快递柜,就是放这个快递的主柜子

  • 再往后找2个柜子,放快递的备份,一共3个副本

第三步:虚拟节点:解决数据倾斜的终极方案

如果只用物理节点对应哈希环上的一个位置,会出现两个问题:

  1. 节点少的时候,数据分布不均匀,有的柜子快递堆成山,有的柜子空着

  2. 新增/下线节点时,需要迁移的数据量太大

Cassandra 用虚拟节点(Vnode) 解决了这个问题:

  • 每个物理节点,不再对应哈希环上的一个位置,而是对应几百个虚拟节点,均匀散列在哈希环上

  • 数据会均匀分布到所有虚拟节点上,最终均匀落到每个物理节点

  • 新增/下线节点时,只会影响少量虚拟节点的数据迁移,对集群影响极小

生动总结:就像把一个大快递柜,拆成了256个小格子,均匀散落在圆形跑道上,快递会均匀分到每个小格子里,不会出现某个大柜子堆爆的情况。

2.3 副本机制:数据丢不了、服务停不了的核心

副本就是「数据的备份」,Cassandra的副本机制,是它高可用的核心保障。

核心概念

  • 副本数(Replication Factor):一份数据要存几个备份,生产环境建议3副本,单机测试可以设1副本

  • 副本策略:副本怎么分布到节点上,分为两种:

    • SimpleStrategy:简单策略,只在同一个数据中心里,按哈希环顺时针分布副本,仅单机/测试环境用

    • NetworkTopologyStrategy:网络拓扑策略,可按数据中心配置副本数,副本会分散到不同机架、不同数据中心,生产环境必须用这个

生产级副本配置示例

-- 生产环境键空间配置:北京数据中心3副本,上海数据中心2副本
CREATE KEYSPACE IF NOT EXISTS order_db
WITH replication = {
    'class': 'NetworkTopologyStrategy',
    'BJ-DC': 3,  -- 北京机房3副本,分散到3个不同机架
    'SH-DC': 2   -- 上海机房2副本,做异地容灾
};

快递站类比:北京的快递站,同一个快递放3个不同的快递柜,还同步2份到上海的快递站,哪怕北京的快递站全炸了,上海的站点还能取到快递。

2.4 读写流程:为什么Cassandra写入比读取快10倍?

这是Cassandra最核心的性能设计,也是很多初学者搞不懂的地方,我们用「快递收件流程」类比,一步一步讲透。

2.4.1 写入流程:永远的O(1)操作,无随机IO

快递员送快递的流程,对应Cassandra的写入流程,全程无等待、无随机IO,速度拉满:

步骤

快递站类比

Cassandra专业流程

1

快递员把快递送到前台,前台先在「收件登记本」上记一笔:「3月27日,收到小明的快递,单号XXX」

客户端写入请求到达协调节点,先写WAL(预写日志),顺序写入磁盘,哪怕机器断电,数据也不会丢

2

登记完,立刻告诉快递员「收件成功」,快递员可以走了

WAL写成功后,立刻给客户端返回「写入成功」,无需等待落盘

3

前台空闲的时候,把登记本里的快递,放到「临时分拣架」上,按单号排序整理

同时,把数据写入MemTable(内存表),内存里按分区键+聚类键排序

4

分拣架满了,就把一整架快递,打包放到「固定货架」上,永久存放,不再修改

当MemTable达到阈值(默认128MB),会异步刷盘到SSTable(有序字符串表),顺序写入磁盘,无随机IO

5

定期把多个旧的货架打包合并,清理掉过期、删除的快递,节省空间

后台定期执行Compaction(合并),把多个小的SSTable合并成大的SSTable,清理墓碑(删除标记)、过期数据

核心结论:Cassandra的写入,只需要一次顺序磁盘写(WAL)+ 一次内存写,就可以返回成功,全程无随机IO,所以写入性能极致,哪怕每秒百万级写入,也能轻松扛住。

2.4.2 读取流程:怎么快速找到数据?

用户取快递的流程,对应Cassandra的读取流程,核心是「精准定位分区,合并多版本数据」:

  1. 客户端发送查询请求,必须带分区键,协调节点根据分区键算出数据所在的节点

  2. 协调节点根据配置的一致性级别,向对应节点发起读取请求

  3. 目标节点收到请求后,先查MemTable(内存),再查SSTable(磁盘),合并所有版本的数据,返回最新的结果

  4. 协调节点收到数据后,校验一致性,返回给客户端

为什么读取比写入慢?

读取需要查内存+多个SSTable,还要合并数据,甚至可能需要跨节点校验一致性,所以读取开销比写入大。这也是为什么Cassandra的设计理念是「查询驱动表设计」,必须用分区键精准定位数据,避免跨分区查询。


第三章:Cassandra 核心术语扫盲:从0到1搞懂所有概念

这一章把所有Cassandra的核心术语,用「快递站类比+专业定义+示例」三维讲解,彻底告别术语恐惧,所有术语前后统一,和之前的内容完全呼应。

术语

快递站类比

专业定义

示例

集群(Cluster)

整个连锁快递站体系

由多个Cassandra节点组成的分布式系统,是最大的管理单元

由北京、上海共10个节点组成的订单集群

节点(Node)

一个快递柜

运行Cassandra进程的一台服务器,是集群的最小单元

一台8核32G的云服务器,运行Cassandra 4.0

数据中心(DC)

同一个城市的所有快递站点

一组逻辑/物理上在一起的节点,通常对应一个机房/可用区,用于容灾隔离

北京阿里云可用区A的所有节点,命名为BJ-DC

机架(Rack)

站点里的一排快递柜

一组物理上在一起的节点,通常对应一个物理机架,副本会分散到不同机架,避免机架断电导致数据丢失

北京机房的1号机架,包含3个节点

键空间(Keyspace)

快递站里的一个业务专区(比如外卖订单专区、生鲜专区)

Cassandra的最高级数据容器,相当于MySQL的「数据库」,副本策略、副本数都在键空间级别配置

外卖订单键空间 order_db

表(Table)

专区里的一组货架

键空间里的数据表,相当于MySQL的「表」,由行、列组成,必须定义主键,数据按分区键组织

订单表 user_orders

行(Row)

一个快递

表里的一条数据,是Cassandra的最小数据单元,由主键唯一标识

小明的20260327号订单

列(Column)

快递上的标签(收件人、手机号、地址)

行里的一个字段,有名字、数据类型,相当于MySQL的「字段」

订单号、用户ID、下单时间、商品名称

主键(Primary Key)

快递的取件码

表的唯一标识,必须定义,分为两部分:分区键(必须)+ 聚类键(可选)

PRIMARY KEY ((user_id, order_month), order_time)

分区键(Partition Key)

取件码的前半段,决定快递放哪个柜子

主键的第一部分,用于哈希计算,决定数据存在哪个节点、哪个分区,所有查询必须带分区键

上面例子里的 (user_id, order_month)

聚类键(Clustering Key)

取件码的后半段,决定快递在格子里怎么排序

主键的第二部分,用于决定同一个分区里,数据的排序顺序,支持范围查询

上面例子里的 order_time,按下单时间降序排序

分区(Partition)

快递柜里的一个格子

具有相同分区键值的所有行,组成一个分区,是Cassandra数据存储和读取的最小单元

同一个用户、同一个月的所有订单,存在同一个分区里

副本(Replica)

快递的备份

同一份数据的多个拷贝,分布在不同节点上,用于高可用和容灾

一份订单数据,在北京机房存3份,上海机房存2份

SSTable

快递站的固定货架

磁盘上的有序数据文件,数据最终的持久化存储,只读不修改

节点磁盘上的 nb-1-big-Data.db 文件

MemTable

快递站的临时分拣架

内存里的有序数据结构,写入的新数据先存在这里,满了再刷盘到SSTable

节点内存里的待刷盘数据

WAL(预写日志)

快递站的收件登记本

磁盘上的顺序写入日志,写入数据前先写WAL,防止断电丢数据

节点磁盘上的 wal.log 文件


第四章:核心中的核心:分区键 深度全解

这一章是整个文档的重中之重,结合之前的内容,超级详细、细腻地拆解分区键的所有知识点,从原理到实操,从避坑到最佳实践,彻底搞懂分区键。

4.1 什么是分区键?再一次讲透本质

一句话本质

分区键 = 数据的「地址编码」,是Cassandra决定数据存在哪个节点、哪个分区的唯一依据,也是你能快速查到数据的唯一钥匙。

没有分区键,Cassandra就不知道数据放哪个柜子,只能翻遍整个快递站的所有柜子(全表扫描),这在Cassandra里是绝对禁止的,会直接拖垮整个集群。

分区键在主键里的位置

Cassandra的主键定义,有固定的格式,分区键永远是主键的第一部分

-- 格式1:简单主键 = 简单分区键(无聚类键)
PRIMARY KEY (分区键)

-- 格式2:复合主键 = 简单分区键 + 聚类键
PRIMARY KEY (分区键, 聚类键1, 聚类键2...)

-- 格式3:复合主键 = 复合分区键 + 聚类键
PRIMARY KEY ((分区键1, 分区键2...), 聚类键1, 聚类键2...)

⚠️ 关键规则:

  • 复合分区键必须用双括号括起来,否则Cassandra会把第一个字段当成分区键,后面的当成聚类键

  • 查询时,必须带全分区键的所有字段,少一个都不行

  • 聚类键是可选的,用于分区内排序和范围查询

4.2 分区键的完整工作流程

我们用「用户订单表」的例子,一步一步拆解分区键的工作全流程,细腻到每一个环节。

示例表定义

CREATE TABLE IF NOT EXISTS order_db.user_orders (
    user_id UUID,
    order_month TEXT,
    order_time TIMESTAMP,
    order_id UUID,
    food_name TEXT,
    price DECIMAL,
    PRIMARY KEY ((user_id, order_month), order_time DESC)
);
  • 复合分区键:(user_id, order_month)(用户ID+订单月份)

  • 聚类键:order_time DESC(按下单时间降序排序)

写入时的分区键工作流程

  1. 你执行插入语句,写入小明2026年3月的订单:

    INSERT INTO order_db.user_orders (user_id, order_month, order_time, order_id, food_name, price)
    VALUES (
        '550e8400-e29b-41d4-a716-446655440000',
        '2026-03',
        '2026-03-27 12:00:00',
        uuid(),
        '香辣鸡腿堡',
        25.0
    );
  2. Cassandra 提取分区键的两个字段值:user_id=xxx + order_month=2026-03,拼接后做Murmur3哈希计算(Cassandra默认的哈希算法),得到一个哈希值:123456789

  3. 根据哈希值,在哈希环上顺时针找到第一个节点,作为主副本节点,再按副本数找到副本节点

  4. 把这行数据,写入对应节点的WAL和MemTable,最终落到SSTable里

  5. 同一个用户、同一个月的所有订单,都会算出同一个哈希值,永远存在同一个分区里

查询时的分区键工作流程

  1. 你执行查询语句,查小明2026年3月的所有订单:

    SELECT * FROM order_db.user_orders
    WHERE user_id = '550e8400-e29b-41d4-a716-446655440000'
      AND order_month = '2026-03';
  2. Cassandra 提取分区键值,做哈希计算,得到和写入时一样的哈希值123456789

  3. 直接定位到数据所在的节点和分区,不用查其他任何节点

  4. 在分区内,按聚类键order_time DESC的排序,直接返回数据,全程O(1)操作,毫秒级返回

错误查询的后果

如果你执行不带分区键的查询:

SELECT * FROM order_db.user_orders WHERE price > 20;

Cassandra 不知道数据在哪个节点,只能向集群里的所有节点发送查询请求,每个节点都要扫描自己的全量数据,再把结果汇总返回——这就是全表扫描,会耗尽整个集群的CPU和IO资源,生产环境会直接导致集群雪崩。

4.3 分区键的两种类型:适用场景+代码示例

类型1:简单分区键(Single Partition Key)

只有一个字段作为分区键,结构最简单。

代码示例
-- 简单分区键:user_id,适合用户基础信息表,一个用户只有一行数据
CREATE TABLE IF NOT EXISTS order_db.user_info (
    user_id UUID PRIMARY KEY,  -- 简单分区键
    username TEXT,
    phone TEXT,
    address TEXT,
    create_time TIMESTAMP
);
适用场景
  • 单分区内的数据量很小(比如一行/几行数据)

  • 查询场景固定,永远带这个分区键字段

  • 数据不会随时间持续增长,不会出现大分区

优点
  • 结构简单,查询灵活,只要带分区键就能查

  • 不会出现查询时漏写分区键字段的问题

缺点
  • 无法拆分大分区,如果分区内数据持续增长,会出现大分区问题


类型2:复合分区键(Composite Partition Key)

用多个字段拼接成一个分区键,必须用双括号括起来,是生产环境最常用的类型。

代码示例
-- 复合分区键:(user_id, order_month),适合订单表,按用户+月份拆分分区
CREATE TABLE IF NOT EXISTS order_db.user_orders (
    user_id UUID,
    order_month TEXT,
    order_time TIMESTAMP,
    order_id UUID,
    food_name TEXT,
    price DECIMAL,
    PRIMARY KEY ((user_id, order_month), order_time DESC)
);
适用场景
  • 数据会随时间持续增长(比如订单、日志、时序数据)

  • 单用户/单设备的数据量很大,需要拆分到多个分区

  • 查询场景固定,会同时带分区键的所有字段(比如查用户某个月的订单)

优点
  • 可以把大分区分成多个小分区,避免单分区过大

  • 数据分布更均匀,不会出现数据倾斜

  • 查询依然是单分区查询,性能极高

缺点
  • 查询时必须带全分区键的所有字段,少一个都不行

  • 无法跨分区做范围查询(比如查用户2026年1-3月的订单,需要查3个分区)

4.4 分区的黄金规则:单个分区不能超过100MB

这是Cassandra官方的硬性建议,也是生产环境最容易踩的坑,必须牢牢记住。

为什么不能超过100MB?

  1. 查询性能暴跌:分区太大,Cassandra扫描分区内数据的时间会变长,原本毫秒级的查询,会变成几百毫秒甚至几秒

  2. 节点恢复困难:节点宕机后,数据同步需要传输大分区,耗时极长,容易导致集群不一致

  3. Compaction压力巨大:大分区的SSTable合并,会占用大量CPU和IO,影响集群稳定性

  4. 内存溢出风险:读取大分区时,会把大量数据加载到内存,容易导致OOM(内存溢出)

怎么计算分区大小?

举个例子:

  • 一行订单数据,平均大小是200字节

  • 一个用户一个月有1000条订单,单分区大小=200字节 * 1000 = 200KB(远小于100MB,安全)

  • 一个用户一年有10万条订单,单分区大小=200字节 * 10万 = 20MB(依然安全)

  • 一个用户一天有1万条订单,用用户ID做分区键,一年就有365万条,单分区大小=730MB(严重超标,必须拆分)

怎么拆分大分区?

核心思路:给分区键增加时间维度,按时间粒度拆分

数据增长速度

拆分粒度

复合分区键示例

每天几百条

按年拆分

(user_id, year)

每天几千条

按月拆分

(user_id, year_month)

每天几万条

按日拆分

(user_id, year_month_day)

每秒几十条

按小时拆分

(device_id, year_month_day_hour)


第五章:CQL 实操全指南:从环境搭建到增删改查

这一章是保姆级实操,从环境搭建开始,一步步带你写CQL,所有代码都可以直接复制运行,同时标注正确和错误的用法。

5.1 环境搭建(5分钟搞定)

1. 下载安装

2. 启动Cassandra

  • Windows:双击 bin/cassandra.bat

  • Linux/Mac:在终端执行 bin/cassandra -f(-f表示前台运行)

  • 看到 Startup complete 就说明启动成功了

3. 打开CQL命令行

  • 执行 bin/cqlsh,就能进入Cassandra的命令行界面,开始写CQL了

5.2 核心CQL操作:从建库到增删改查

操作1:创建键空间(Keyspace)

键空间相当于MySQL的数据库,必须先建键空间,才能建表。

测试环境建库
-- 单机测试用,简单策略,1副本
CREATE KEYSPACE IF NOT EXISTS test_db
WITH replication = {
    'class': 'SimpleStrategy',
    'replication_factor': 1
}
AND DURABLE_WRITES = true; -- 开启持久化,默认true,关闭会丢数据

-- 切换到这个键空间
USE test_db;
生产环境建库
-- 生产环境用,网络拓扑策略,多数据中心多副本
CREATE KEYSPACE IF NOT EXISTS order_db
WITH replication = {
    'class': 'NetworkTopologyStrategy',
    'BJ-DC': 3,
    'SH-DC': 2
}
AND DURABLE_WRITES = true;

操作2:创建表(Table)

建表的核心是设计主键、分区键、聚类键,必须遵循「查询驱动设计」,先想清楚查询场景,再建表。

示例1:用户信息表(简单分区键)
CREATE TABLE IF NOT EXISTS test_db.user_info (
    user_id UUID,
    username TEXT,
    phone TEXT,
    address TEXT,
    create_time TIMESTAMP,
    PRIMARY KEY (user_id)  -- 简单分区键
)
WITH comment = '用户基础信息表'
AND gc_grace_seconds = 864000; -- 墓碑保留时间,默认10天
示例2:用户订单表(复合分区键+聚类键)
CREATE TABLE IF NOT EXISTS test_db.user_orders (
    user_id UUID,
    order_month TEXT,
    order_time TIMESTAMP,
    order_id UUID,
    food_name TEXT,
    price DECIMAL,
    pay_status INT,
    PRIMARY KEY ((user_id, order_month), order_time DESC, order_id)  -- 复合分区键+多聚类键
)
WITH comment = '用户订单表'
AND CLUSTERING ORDER BY (order_time DESC, order_id ASC); -- 聚类键的排序规则

⚠️ 关键说明:

  • 聚类键的排序规则,在建表时就定死了,查询时不能修改

  • 多个聚类键,排序是按顺序的,先按第一个聚类键排,再按第二个排

操作3:插入数据(INSERT)

Cassandra的插入是「UPSERT」逻辑:如果主键已存在,就更新;不存在,就插入,不会报错。

-- 插入用户信息
INSERT INTO test_db.user_info (user_id, username, phone, address, create_time)
VALUES (
    uuid(),
    '小明',
    '13800138000',
    '北京市朝阳区',
    toTimestamp(now())
);

-- 插入订单数据
INSERT INTO test_db.user_orders (user_id, order_month, order_time, order_id, food_name, price, pay_status)
VALUES (
    550e8400-e29b-41d4-a716-446655440000,
    '2026-03',
    '2026-03-27 12:00:00',
    uuid(),
    '香辣鸡腿堡',
    25.0,
    1
);

操作4:查询数据(SELECT)

查询的核心铁律:必须带分区键,禁止全表扫描,禁止滥用ALLOW FILTERING

正确查询:带全分区键
-- 查单个用户信息(带简单分区键)
SELECT * FROM test_db.user_info
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;

-- 查用户某个月的订单(带全复合分区键)
SELECT * FROM test_db.user_orders
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000
  AND order_month = '2026-03';

-- 分区内范围查询(带分区键+聚类键范围)
SELECT * FROM test_db.user_orders
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000
  AND order_month = '2026-03'
  AND order_time >= '2026-03-01 00:00:00'
  AND order_time <= '2026-03-31 23:59:59';
错误查询:绝对禁止
-- 错误1:不带分区键,全表扫描
SELECT * FROM test_db.user_orders;

-- 错误2:只带复合分区键的一部分,无法定位分区
SELECT * FROM test_db.user_orders
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;

-- 错误3:滥用ALLOW FILTERING,本质还是全表扫描
SELECT * FROM test_db.user_orders
WHERE price > 20 ALLOW FILTERING;

⚠️ 关于ALLOW FILTERING

它允许Cassandra在查询时过滤非主键字段,但本质是先全表/全分区扫描,再过滤数据,只有在「分区内数据量很小,过滤后结果极少」的场景才能用,生产环境99%的场景都禁止使用。

操作5:更新数据(UPDATE)

更新必须带全主键,只能更新非主键字段,Cassandra没有「行级锁」,更新是最终一致性的。

-- 更新用户手机号,必须带主键user_id
UPDATE test_db.user_info
SET phone = '13900139000'
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;

操作6:删除数据(DELETE)

Cassandra的删除不是真的立刻删除,而是写一个「墓碑(Tombstone)」标记,后台Compaction时才会真正清理。

-- 删除一行订单数据,必须带全主键
DELETE FROM test_db.user_orders
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000
  AND order_month = '2026-03'
  AND order_time = '2026-03-27 12:00:00'
  AND order_id = 123e4567-e89b-12d3-a456-426614174000;

-- 删除一个分区的所有数据
DELETE FROM test_db.user_orders
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000
  AND order_month = '2026-03';

第六章:Cassandra 数据模型设计黄金法则

这是Cassandra和MySQL最核心的区别,也是90%的初学者用错Cassandra的根本原因。

6.1 核心设计理念:查询驱动设计,而非实体驱动设计

MySQL的设计思路(实体驱动)

先设计实体(用户、订单、商品),按第三范式建表,通过主键、外键关联,一个表可以应对多种查询场景,用JOIN关联数据。

Cassandra的设计思路(查询驱动)

一个查询场景,对应一张表,完全反范式,数据冗余存储,表结构完全为查询服务,不做JOIN,不做跨表关联。

生动类比

  • MySQL = 图书馆,按图书分类放书,你可以按书名、作者、出版社多种方式找书,需要翻索引

  • Cassandra = 你家的书架,你会把「常看的编程书」放第一层,「睡前看的小说」放床头,「孩子的绘本」放儿童房——怎么方便拿,就怎么放,同一本书可以放多个地方,完全为你的使用场景服务。

6.2 数据模型设计5步走(生产级标准流程)

我们用「外卖订单系统」的例子,走一遍完整的设计流程。

第一步:梳理所有业务查询场景

先把所有需要的查询场景列出来,一个都不能漏,这是设计的基础:

  1. 用户端:查询「我的」某个月的所有订单

  2. 用户端:查询「我的」某个订单的详情

  3. 商家端:查询「我的店铺」某天的所有订单

  4. 运营端:查询某个城市某个时间段的订单统计

第二步:为每个查询场景设计一张表

一个查询场景对应一张表,数据冗余存储,完全为查询服务。

表1:用户订单表(对应用户查自己的月订单)
-- 查询场景:用户查自己某个月的订单,按下单时间倒序
CREATE TABLE IF NOT EXISTS order_db.user_orders_by_user_month (
    user_id UUID,
    order_month TEXT,
    order_time TIMESTAMP,
    order_id UUID,
    shop_id UUID,
    shop_name TEXT,
    food_name TEXT,
    price DECIMAL,
    pay_status INT,
    PRIMARY KEY ((user_id, order_month), order_time DESC, order_id)
);
表2:订单详情表(对应用户查单个订单详情)
-- 查询场景:按订单ID查订单详情
CREATE TABLE IF NOT EXISTS order_db.order_detail_by_id (
    order_id UUID PRIMARY KEY,
    user_id UUID,
    shop_id UUID,
    shop_name TEXT,
    food_name TEXT,
    price DECIMAL,
    pay_status INT,
    address TEXT,
    create_time TIMESTAMP
);
表3:商家订单表(对应商家查某天的订单)
-- 查询场景:商家查自己店铺某天的订单
CREATE TABLE IF NOT EXISTS order_db.shop_orders_by_shop_day (
    shop_id UUID,
    order_date DATE,
    order_time TIMESTAMP,
    order_id UUID,
    user_id UUID,
    user_name TEXT,
    food_name TEXT,
    price DECIMAL,
    pay_status INT,
    PRIMARY KEY ((shop_id, order_date), order_time DESC, order_id)
);

第三步:设计分区键,避免大分区和数据倾斜

  • 表1:用(user_id, order_month)做复合分区键,按月份拆分,避免单用户订单太多导致大分区

  • 表2:用order_id做简单分区键,每个订单一行,无大分区风险

  • 表3:用(shop_id, order_date)做复合分区键,按天拆分,避免大商家订单太多导致大分区

第四步:设计聚类键,满足排序和范围查询

  • 所有表的聚类键都用order_time DESC,满足最新订单排在最前面的业务需求

  • 增加order_id作为最后一个聚类键,保证主键唯一性,避免同一时间下单的订单主键冲突

第五步:写入时同步更新所有表

当用户下单时,需要同时写入这3张表,保证数据一致性。Cassandra支持批处理(BATCH),可以保证同一个分区的多个写入操作原子性。

-- 下单时,批量写入3张表
BEGIN BATCH
    -- 写入用户订单表
    INSERT INTO order_db.user_orders_by_user_month (user_id, order_month, order_time, order_id, shop_id, shop_name, food_name, price, pay_status)
    VALUES ('xxx', '2026-03', '2026-03-27 12:00:00', 'yyy', 'zzz', '肯德基', '香辣鸡腿堡', 25.0, 1);
    
    -- 写入订单详情表
    INSERT INTO order_db.order_detail_by_id (order_id, user_id, shop_id, shop_name, food_name, price, pay_status, address, create_time)
    VALUES ('yyy', 'xxx', 'zzz', '肯德基', '香辣鸡腿堡', 25.0, 1, '北京市朝阳区', '2026-03-27 12:00:00');
    
    -- 写入商家订单表
    INSERT INTO order_db.shop_orders_by_shop_day (shop_id, order_date, order_time, order_id, user_id, user_name, food_name, price, pay_status)
    VALUES ('zzz', '2026-03-27', '2026-03-27 12:00:00', 'yyy', 'xxx', '小明', '香辣鸡腿堡', 25.0, 1);
APPLY BATCH;

6.3 数据模型设计的3个绝对禁止

  1. 禁止用MySQL的范式思维设计Cassandra表:不要试图做表关联、不要搞第三范式,Cassandra没有JOIN,关联查询必须在业务代码里做

  2. 禁止一张表应对多个查询场景:不要试图用一张表满足所有查询,否则必然会出现不带分区键的查询,性能极差

  3. 禁止过度冗余:虽然是反范式设计,但不要把无关的字段冗余到表里,只冗余查询需要的字段,避免数据量过大


第七章:最佳实践 VS 反面案例 全对比

这一章是生产环境避坑指南,每个点都有反面案例、问题分析、最佳实践,全是大厂踩过的坑,必须牢牢记住。

7.1 分区键设计最佳实践

对比项

反面案例(错误写法)

问题分析

最佳实践(正确写法)

分区键基数选择

gendercitystatus这种低基数字段做分区键<br/>PRIMARY KEY (city, user_id)

低基数字段的取值极少(比如性别只有3种),数据会全堆在2-3个分区里,导致严重的数据倾斜,节点负载不均,甚至宕机

user_idorder_iddevice_id这种高基数、分布均匀的字段做分区键<br/>PRIMARY KEY (user_id, city)

大分区处理

user_id做简单分区键,存用户所有订单,单用户一年10万订单,分区大小超过1GB

单分区过大,查询性能暴跌,Compaction压力巨大,节点恢复困难,甚至OOM

用复合分区键,按时间粒度拆分大分区<br/>PRIMARY KEY ((user_id, order_month), order_time)

复合分区键顺序

把低频查询字段放在前面<br/>PRIMARY KEY ((order_month, user_id), order_time),查询时只带user_id,不带order_month

复合分区键必须带全所有字段才能查询,顺序写反会导致常用查询无法命中,只能全表扫描

按查询场景设计复合分区键顺序,高频查询字段放在前面<br/>PRIMARY KEY ((user_id, order_month), order_time)

分区键查询

不带分区键查询,或者只带复合分区键的一部分

无法定位分区,导致全表/全集群扫描,耗尽集群资源,引发雪崩

所有查询必须带全分区键的所有字段,哪怕需要冗余表来满足查询场景

7.2 数据模型设计最佳实践

对比项

反面案例(错误写法)

问题分析

最佳实践(正确写法)

设计思路

用MySQL的范式思维,建用户表、订单表、商品表,靠业务代码做JOIN关联

每次查询都要查多张表,多次请求Cassandra,性能极差,代码复杂度高

采用查询驱动设计,一个查询场景对应一张表,数据冗余存储,单表就能满足查询需求

表结构设计

一张表加了几十上百个字段,不管查询用不用得上,全放进去

行数据过大,导致分区膨胀,读取时需要加载大量无用字段,浪费内存和IO

只把查询需要的字段放到表里,非必要字段不冗余,避免行过大

主键设计

主键设计不合理,无法保证唯一性,导致数据被覆盖

Cassandra的INSERT是UPSERT,主键重复会直接覆盖原有数据,导致数据丢失

主键必须能唯一标识一行数据,必要时增加order_iduuid等字段保证唯一性

排序设计

不在建表时指定聚类键排序规则,查询时用ORDER BY排序

Cassandra的ORDER BY只能按聚类键的预定义排序规则来,否则会报错,或者强制全分区扫描

建表时就通过CLUSTERING ORDER BY指定好排序规则,查询时直接按这个顺序返回

7.3 读写操作最佳实践

对比项

反面案例(错误写法)

问题分析

最佳实践(正确写法)

写入操作

大批量无界写入,一次性写入几十万条数据

会把节点的MemTable打满,频繁刷盘,Compaction压力暴增,甚至导致节点宕机

批量写入控制在100-1000条/批,单批次大小不超过5MB,避免大批次写入

写入操作

频繁更新/删除同一行数据

会产生大量墓碑和多版本数据,Compaction压力大,读取时需要合并大量版本,性能暴跌

尽量减少更新操作,Cassandra适合写一次、读多次的场景,避免频繁修改

查询操作

不带分区键,用ALLOW FILTERING做全表过滤

本质是全集群扫描,会耗尽所有节点的CPU和IO,生产环境会直接导致集群雪崩

绝对禁止不带分区键的查询,哪怕冗余表,也要保证查询带分区键

查询操作

跨分区IN查询,比如WHERE user_id IN (id1, id2, ..., id1000)

IN查询会同时向多个分区发起请求,协调节点需要汇总所有结果,延迟极高,容易超时

把IN查询拆成多个单分区查询,在业务代码里并发执行,再汇总结果,单IN查询的元素不超过10个

查询操作

不加限制的SELECT *,查询整个分区的所有数据

分区内有几十万行数据时,会一次性加载全量数据,导致内存溢出,节点宕机

必须加LIMIT限制,分页查询,单次查询不超过1000行

7.4 生产环境运维最佳实践

  1. 集群规划

    1. 生产环境节点数至少3个,副本数至少3个,分散到不同机架

    2. 跨数据中心部署,至少2个数据中心,做异地容灾

    3. 节点配置:推荐8核32G以上,SSD磁盘,Cassandra对磁盘IO要求极高

  2. 参数配置

    1. 堆内存设置:最大不超过31GB,推荐8-16GB,避免大堆GC停顿

    2. 虚拟节点数:默认256个,无需修改,保证数据均匀分布

    3. 并发读写线程数:根据CPU核心数调整,避免线程过多导致上下文切换频繁

  3. 监控告警

    1. 必须监控:节点状态、读写延迟、分区大小、Compaction队列、CPU/内存/磁盘IO

    2. 告警阈值:单分区超过100MB告警,读写延迟超过100ms告警,节点宕机立刻告警

  4. 数据清理

    1. 给有生命周期的数据设置TTL(过期时间),自动清理过期数据,避免手动删除产生大量墓碑

    2. 定期执行nodetool repair,修复集群数据不一致,生产环境至少每周一次


第八章:生产环境常见问题排查指南

问题1:数据倾斜,某个节点CPU/IO使用率远高于其他节点

排查方法

  1. 执行 nodetool tablestats 键空间名.表名,看每个分区的大小,找到超大分区

  2. 执行 nodetool ring,看每个节点的负载分布,是否不均匀

根因

  • 分区键选择错误,用了低基数字段,导致数据集中在少数分区

  • 没有拆分大分区,单分区数据量过大,集中在某个节点

解决方案

  • 重新设计分区键,用高基数字段做分区键

  • 用复合分区键拆分大分区,按时间粒度分散数据

  • 调整虚拟节点数,让数据分布更均匀

问题2:读取延迟极高,查询超时

排查方法

  1. 开启慢查询日志,找到慢查询语句,看是否带分区键

  2. 执行 nodetool tablehistograms 键空间名.表名,看读写延迟分布

  3. 检查分区大小,是否有超过100MB的大分区

根因

  • 查询不带分区键,全表扫描

  • 分区过大,扫描分区内数据耗时过长

  • SSTable数量过多,读取时需要查多个SSTable

解决方案

  • 优化查询语句,必须带分区键

  • 拆分大分区,控制单分区大小在100MB以内

  • 调整Compaction策略,减少SSTable数量

问题3:节点频繁GC停顿,甚至OOM内存溢出

排查方法

  1. 查看GC日志,看GC停顿时间和频率

  2. 查看堆内存配置,是否超过31GB

  3. 查看是否有大分区查询、无限制SELECT *查询

根因

  • 堆内存设置过大,导致Full GC停顿时间过长

  • 大分区查询,一次性加载大量数据到内存

  • 批量写入过大,导致MemTable频繁刷盘,内存占用过高

解决方案

  • 调整堆内存到8-16GB,最大不超过31GB

  • 优化查询,必须加LIMIT限制,禁止无限制查询

  • 控制批量写入大小,单批次不超过5MB


第九章:总结:Cassandra 核心心法

  1. Cassandra的核心优势:去中心化无单点故障、线性可扩展、极致写入性能、跨数据中心高可用,是海量高并发写入场景的首选

  2. 分区键是灵魂:分区键决定了数据的分布和查询性能,所有设计都要围绕分区键展开,所有查询必须带分区键

  3. 设计理念是查询驱动:一个查询场景对应一张表,反范式冗余存储,不要用MySQL的思维设计Cassandra

  4. 生产环境铁律:单分区不超过100MB,禁止全表扫描,禁止滥用ALLOW FILTERING,3副本起步,跨机架/数据中心部署

  5. Cassandra的适用场景:用户行为埋点、物联网时序数据、订单/消息记录、日志存储等海量写入、固定模式查询的场景;不适合需要频繁更新、复杂关联查询、强事务的场景。


附录

附录1:Cassandra 常用数据类型对照表

CQL数据类型

说明

对应Java类型

UUID

全局唯一ID,常用做主键

java.util.UUID

TEXT

字符串类型,无长度限制

java.lang.String

INT

32位整数

java.lang.Integer

BIGINT

64位长整数

java.lang.Long

DECIMAL

高精度小数,适合金额

java.math.BigDecimal

BOOLEAN

布尔值

java.lang.Boolean

TIMESTAMP

时间戳,精确到毫秒

java.util.Date

DATE

日期,精确到天

java.time.LocalDate

MAP

键值对集合

java.util.Map

LIST

有序列表

java.util.List

SET

无序不重复集合

java.util.Set

附录2:常用nodetool运维命令

命令

作用

nodetool status

查看集群节点状态

nodetool ring

查看哈希环和节点负载分布

nodetool tablestats

查看表的统计信息(分区大小、读写次数等)

nodetool tablehistograms

查看表的读写延迟分布

nodetool repair

修复集群数据不一致

nodetool compact

手动触发Compaction合并SSTable

nodetool decommission

下线节点,平滑迁移数据

附录3:版本选型建议

  • 生产环境优先选择 4.1.x稳定版,不推荐3.x以下的老旧版本

  • JDK版本:4.x推荐JDK11,3.x推荐JDK8

  • 不要用最新的开发版,避免未发现的bug

两块二每分钟