NoSQL 真的比 SQL 快?底层拆解后我沉默了

B树 vs LSM:磁盘的物理极限

数据库之所以慢,根源在于磁盘。机械硬盘的寻道时间,大约10毫秒。10毫秒,CPU已经飞过几百万个时钟周期。所以,读写性能的终极瓶颈,就在于磁盘访问次数

SQL 数据库,惯用 B树。B树像一本精心编排的字典,所有数据按顺序排列,更新就地发生。找一条记录,一路二分下去,通常 3-4 次磁盘 IO 搞定。看起来很完美?但写入,哪怕只改一个字节,也可能引发页分裂,把相邻数据挪来挪去。这就是写放大。而且,为了保证ACID,还得写两次——先写日志,再写数据。说实话,这很笨重。

NoSQL 里的 LSM 树,思路截然不同。它把写入攒在内存,像个日志本,无脑追加。满了,就一口气刷到磁盘,形成一个个有序的SSTable 文件。这过程,顺序写,快到爆炸。因为你完全没有寻道,机械臂不用跳来跳去。但代价呢?

读出数据时,它得从最新的 SSTable 开始找,一层层回溯。越旧的数据,找得越慢。这就是读放大。为了控制读放大,LSM 必须定期进行合并(Compaction),把多个文件合并成一个,丢弃旧版本。这个过程,像垃圾回收,占用CPU和IO。所以你看到,在一个写多读少的场景里,LSM 优势巨大;但如果读多写少,比如账务系统,B树可能反而更好。别被“NoSQL一定快”这种鬼话骗了。我之前就栽过跟头,把一个报表系统从 MySQL 迁到 Cassandra,结果读取一塌糊涂,因为埋在一大堆 SSTable 里翻垃圾。

这里面的权衡,充满工程美学:用写放大换读放大,又用 Compaction 去控制读放大,像走钢丝。本质上,是在顺序性和随机性之间做调度。机械硬盘时代,这种设计是革命。到了 NVMe SSD,寻道时间几乎消失,随机读写能力飙升,LSM 的顺序写优势,其实在弱化。但 LSM 还有另一个杀手锏——数据压缩。因为 SSTable 是只读的,可以大胆用字典编码、前缀压缩,把数据压得很小。这在云上,直接换算成存储成本。B树这种原地更新的结构,压缩就难搞,碎片化严重。所以你看,每个设计都是特定时代的产物。

NoSQL LSM树与B树结构对比示意图
NoSQL LSM树与B树结构对比示意图

压测数据不说谎:1000万行写入的撕扯

光说理论没用。我们曾经做过一次内部基准测试,那场面,有点残酷。配置:三节点集群,单节点 16 vCPU,32GB内存,500GB NVMe SSD。数据集:1000万条用户行为日志,每条大约 1KB,200个字段,但实际只填充30个左右,稀疏表结构。

对战的选手:MySQL 8.0 (InnoDB),单表无分区;Apache Cassandra 4.1,使用 LZ4 压缩;还有 MongoDB 6.0,WiredTiger 引擎。我们用 32 个并发客户端,持续写入。结果:

  • MySQL:一开始冲到 1.2 万行/秒,然后随着 B树不断分裂,性能开始抽搐,降到 8000 左右,偶尔跌到 2000(触发 Checkpoint 和日志刷新)。P99 延迟达到 300ms。磁盘用量最后 15GB。
  • Cassandra:稳稳的 3.5 万行/秒,几乎没有波动。P99 延迟 8ms。为什么?因为写入就是追加日志 + 写 Memtable,完全顺序化。磁盘用量 9GB,得益于压缩。但 Compaction 期间,读性能会周期性波谷,我们没测读,所以不明显。
  • MongoDB:约 2.5 万行/秒,P99 12ms,也稳定。WiredTiger 本质上也是个类 LSM 结构,有 checkpoint 写入,所以不够极致。

你猜怎么着?把并发提到 128,MySQL 直接开始雪崩,锁等待堆积。Cassandra 反而接近线性提升,到 12 万行/秒。MongoDB 在 6 万后放缓。这就是无分享架构的威力:每个节点独立处理请求,没有中心化瓶颈。但读操作的话,完全反过来。我们随后执行了简单的主键查询,MySQL 可以接近 10 万 QPS,Cassandra 只有 4 万,因为它需要查多个 SSTable 甚至 Bloom filter 算半天。所以,场景决定一切。别上来就吹哪个厉害。

数据库写入性能压测对比图表 MySQL vs Cassandra vs MongoDB
数据库写入性能压测对比图表 MySQL vs Cassandra vs MongoDB

三个大坑和脱困指南

三个大坑和脱困指南
三个大坑和脱困指南

落地 NoSQL,我不信一帆风顺。以下是我亲手踩过的坑,带着血和泪。

坑一:数据模型让你怀疑人生

很多团队从关系型转过去,第一反应是把表平搬。但 NoSQL 的模型通常是面向查询的。比如 Cassandra,你需要按查询设计主键,先确定 Partition Key,再 Clustering Key。一旦设计错了,数据分布不均(热点),查询要走全表扫描,等于自毁。我见过一个系统,把 user_id 作为 Partition Key,因为 a 用户特别多,导致一个节点被打爆,其他节点闲死。解决方案?引入时间戳做分片,或者使用复合分区键,把热点分散。但你得事先估计数据倾斜,没估计到就是灾难。建模没有银弹,需要大量推演和原型测试。后悔药不好吃。

坑二:一致性的迷思

最终一致性,说起来轻巧。当你真的遇到“我更新了为啥读出来还是旧值”时,产品经理的眼神能杀人。Dynamo 体系里,用向量时钟(Vector Clock)或最后写入胜(Last Write Wins)解决冲突。但 LWW 真粗暴,基于时间戳,如果时钟不同步,老数据覆盖新数据,哭都来不及。我们在 Kafka + MongoDB 的流式处理中,就遭遇过:消息乱序到达,旧版本覆盖了新版本。后来强制用业务时间戳,并在读取时指定 readConcern: ‘majority’,还要手动处理冲突合并。实现起来比 SQL 事务复杂十倍。你得问自己:真的需要那么高的可用性,而愿意牺牲即时一致性吗?很多时候,业务经不起这种折腾。

坑三:运维的黑洞

NoSQL 往往把复杂性从查询层移到了运维层。扩缩容,Cassandra 要重新平衡分区(Streaming),MongoDB 的分片平衡会引发 IO 风暴。我们预生产环境扩容 3 到 5 节点,数据重分布跑了 36 小时,期间集群性能骤降。为什么?因为默认的并发迁移太保守,又或者太激进,你得精细调整 throttle。还有备份恢复,不是简单 dump,要考虑片键一致性,尤其跨分片事务(MongoDB 4.2+),备份时间点必须一致。教训是:把自动运维脚本放在手边,反复演练故障恢复,特别是网络分区(Split Brain)时的处理。别指望开箱即用。

这些经验,都是真金白银换的。NoSQL 不是万能药,它是一组特定约束下的极优解。用好了,酣畅淋漓;用砸了,就是一副枷锁。技术选型,说到底,是在理解物理极限后,对业务的一种冷酷审判。

免责声明:市场有风险,选择需谨慎!此文仅供参考,不作买卖依据。如有侵权请联系删除。
文章名称:NoSQL 真的比 SQL 快?底层拆解后我沉默了
文章链接:https://m.lfdjt.com/info_23_7725.html