NoSQL:在高并发场景下,数据库和NoSQL如何做到互补?

在存储服务的优化中,我们通常从两个方面入手:

第一,提升读写性能,特别是读性能。大部分产品都以“读多写少”为主,比如你每天离不开的微信朋友圈、微博、淘宝等,这些系统的查询 QPS 远远高于写入 QPS。

第二,增强存储的扩展能力,满足海量数据的存储需求。我们之前学习过的“读写分离”和“分库分表”技术,就是为了解决传统关系型数据库在这两个方面的不足。但即使采用这些方法,有些问题依然难以解决。

比如,在一个微博项目中,关系型数据量已经达到千亿级别。即使我们将数据拆分到 1024 个库表,每个表的容量仍有上亿,并且数据量还在快速增长。这种情况下,无论怎么继续分库分表,扩展能力的瓶颈还是难以突破。

传统数据库在扩展性方面的劣势,使其很难从根本上解决这个问题。这时,我们可以引入 NoSQL 数据库来弥补短板。

NoSQL 数据库天生具备分布式能力,能够提供更好的读写性能,同时也在扩展性上表现出色,非常适合处理这种高并发和海量数据的场景。接下来,我们以垂直电商系统为例,学习如何结合使用 NoSQL 数据库和关系型数据库,构建高性能、高扩展的存储架构。

NoSQL,No SQL?

NoSQL 数据库,顾名思义,是一种不同于传统关系型数据库的数据库系统统称。它不使用 SQL 作为查询语言,而是通过提供出色的横向扩展能力和高效的读写性能,非常适合应对互联网项目中的高并发和大数据量需求。正因如此,一些大厂如小米、微博、陌陌等,往往选择 NoSQL 数据库作为高并发大容量场景下的存储解决方案。

经过十多年的发展,NoSQL 数据库衍生出多种类型,以下是几个常见的例子:

  • 键值存储(KV 存储):典型代表有 Redis 和 LevelDB。这类数据库的优势在于极高的读写性能,特别适用于对性能要求极高的场景。
  • 列式存储数据库:如 HBase 和 Cassandra。这类数据库以列而非行为单位存储数据,特别适合离线数据统计等场景。
  • 文档型数据库:例如 MongoDB 和 CouchDB。这类数据库的特点是模式自由(Schema Free),可以灵活扩展字段。对于像电商系统这样的场景,不同商品种类字段各异,使用文档型数据库可以避免频繁调整表结构,极大简化开发和维护工作。

NoSQL 数据库的多样性,使其能够根据具体需求选择合适的类型,满足不同场景下的性能和扩展性要求。

使用 NoSQL 提升写入性能

在传统的数据库系统中,大多数使用机械磁盘作为存储介质,而对机械磁盘的访问方式主要有两种:随机 IO 和顺序 IO。

随机 IO 的特点

随机 IO 需要花费时间进行磁盘寻道,这是一种代价昂贵的操作。与顺序 IO 相比,随机 IO 的读写效率低了两到三个数量级。因此,提升写入性能的一个关键点是尽量减少随机 IO 的发生。

以 MySQL 的 InnoDB 存储引擎为例

InnoDB 在执行写操作时,更新 binlogredologundolog 是顺序 IO,而更新 datafile索引文件 则是随机 IO。为了尽量减少随机 IO 的影响,关系型数据库引擎进行了许多优化措施,例如:写入时先将数据写入内存,然后批量刷新到磁盘。然而,即使如此,随机 IO 仍然不可避免。

随机 IO 在索引中的表现

在 InnoDB 引擎中,索引以 B+ 树的形式组织,而 MySQL 的主键采用聚簇索引(即数据与索引存储在一起)。由于数据与索引合并存储,在插入或更新数据时,需要找到特定位置进行写入,这会导致随机 IO。

此外,当发生页分裂时,不仅会产生随机 IO,还需要移动大量数据。这种操作对写入性能的影响非常显著,是随机 IO 的一个典型瓶颈。

通过这些分析,我们可以更清楚地了解随机 IO 对性能的影响以及数据库为应对这一问题所做的优化。

NoSQL 数据库是怎么解决这个问题的呢?

解决随机 IO 的问题有多种方法,这里我们重点讲解一种最常见的方案,即许多 NoSQL 数据库都采用的 基于 LSM 树(Log-Structured Merge Tree)的存储引擎。这种算法被广泛使用,例如 HBase、Cassandra 和 LevelDB 都以它为核心存储引擎。

LSM 树的设计理念

LSM 树通过牺牲部分读性能,来换取更高的写入性能。其基本思想如下:

  1. 数据首先写入到内存中的一个名为 MemTable 的结构,MemTable 内的数据按照写入的 Key 排序。
  2. 为了防止因机器掉电或重启导致数据丢失,写入的数据会同时记录在磁盘上的 Write Ahead Log 中,作为备份。
  3. 当 MemTable 中的数据达到一定规模时,系统会将其刷新到磁盘上,生成一个新的文件,称为 SSTable(Sorted String Table)

    SSTable 的合并

随着 SSTable 文件数量的增加,系统会定期将这些文件合并,以减少文件数量。由于 SSTable 本身是有序的,合并操作非常高效。

读数据的流程

在 LSM 树中查找数据时,系统会先在 MemTable 中查找,如果未找到,再依次查询 SSTable 文件。虽然 LSM 树存储的数据是有序的,因此查找效率较高,但由于数据分散在多个 SSTable 中,读取性能通常略低于基于 B+ 树的索引。

通过这种设计,LSM 树大幅降低了随机 IO 的开销,成为 NoSQL 数据库提升写入性能的一种核心解决方案。

和 LSM 树类似的算法有很多,比如说 TokuDB 使用的名为 Fractal tree 的索引结构,它们的核心思想就是将随机 IO 变成顺序的 IO,从而提升写入的性能。在后面的缓存篇中,我也将给你着重介绍我们是如何使用 KV 型 NoSQL 存储来提升读性能的。所以你看,NoSQL 数据库补充关系型数据库的第一种方式就是提升读写性能。

场景补充

除了提升性能之外,NoSQL 数据库在某些场景下还可以作为传统关系型数据库的有力补充。我们通过一个具体的例子来说明。

假设有一天,CEO 找到你,说他正在为垂直电商项目规划一个搜索功能,需要实现商品名称的模糊搜索,并希望你尽快提供解决方案。

一开始,你可能会觉得这很简单,只需要在数据库中执行一条类似 SELECT * FROM product WHERE name LIKE '%***%' 的查询语句即可。然而,在实际操作中却发现了问题。

通过测试你会发现,并不是所有的模糊查询都能利用索引。例如,语句 SELECT * FROM product WHERE name LIKE '%电冰箱' 无法使用 name 字段上的索引,而是会触发全表扫描,这种查询性能在高数据量下是难以接受的。

相对而言,像 SELECT * FROM product WHERE name LIKE '索尼%' 这样的查询可以利用索引,从而显著提升查询效率。这是因为在前缀匹配的情况下,数据库能够有效地利用索引,而后缀或全模糊匹配时,索引则无能为力,导致性能瓶颈的出现。

面对这样的场景,我们需要考虑其他解决方案,而 NoSQL 数据库往往可以提供有效的补充。

于是你在谷歌上搜索了一下解决方案,发现大家都在使用开源组件 Elasticsearch 来支持搜索的请求,它本身是基于“倒排索引”来实现的,那么什么是倒排索引呢?倒排索引是指将记录中的某些列做分词,然后形成的分词与记录 ID 之间的映射关系。比如说,你的垂直电商项目里面有以下记录:

那么,我们将商品名称做简单的分词,然后建立起分词和商品 ID 的对应关系,就像下面展示的这样:

这样,如果用户搜索电冰箱,就可以给他展示商品 ID 为 1 和 3 的两件商品了。

而 Elasticsearch 作为一种常见的 NoSQL 数据库,就以倒排索引作为核心技术原理,为你提供了分布式的全文搜索服务,这在传统的关系型数据库中使用 SQL 语句是很难实现的。所以你看,NoSQL 可以在某些业务场景下代替传统数据库提供数据存储服务。

提升扩展性

在扩展性方面,许多 NoSQL 数据库也具有天然的优势。还是以你的垂直电商系统为例。

假设你为电商系统新增了一个评论功能。起初,你的评估较为乐观,认为评论的增长不会太快,因此为评论系统设计了 8 个库,每个库拆分为 16 张表。然而,功能上线后,评论量却以异常迅猛的速度增长,超出了预期。

为了应对这种情况,你不得不继续拆分更多的库和表,并将已有数据迁移到新的库表中。这个过程不仅繁琐、耗时,还容易出错,对系统的稳定性造成影响。

在这种情况下,你开始思考,是否可以使用 NoSQL 数据库来彻底解决扩展性问题。经过调研发现,许多 NoSQL 数据库在设计之初就充分考虑了分布式和海量数据存储的需求。例如,MongoDB 就具备以下三个与扩展性相关的核心特性:

  1. 分片(Sharding):通过水平扩展轻松应对数据量增长。
  2. 动态 Schema:灵活的数据模型支持频繁变更。
  3. 自动负载均衡:保证数据分布均匀,提升性能和可靠性。

这些特性使 NoSQL 数据库能够从容应对存储需求的快速增长,为系统扩展提供了更高效的解决方案。

首先是 Replica,即副本集,可以理解为主从分离机制。它通过将数据复制多份来确保主节点故障时数据不会丢失,同时支持读写分离。Replica 中的主节点负责处理写请求,并将数据变更记录到 oplog(类似于 binlog)中;从节点通过读取 oplog 来同步数据,以保持与主节点的一致性。

如果主节点发生故障,MongoDB 会自动从从节点中选出一个新的主节点,继续处理写请求,从而保证服务的高可用性。

其次是 Shard,也就是分片机制,可以理解为分库分表的实现。Shard 通过一定规则将数据划分为多份,并分布存储到不同的机器上。MongoDB 的分片功能通常由以下三个角色协作完成:

  1. Shard Server:存储实际数据的节点,每个 Shard Server 是一个独立的 Mongod 进程。
  2. Config Server:负责存储元数据的信息,例如每个分片存储了哪些数据。这通常是一组 Mongod 进程协同工作。
  3. Route Server:不存储实际数据,仅作为路由器。它通过读取 Config Server 的元数据,将客户端的请求路由到正确的 Shard Server。

通过 Replica 提高可用性和读写性能,以及 Shard 实现数据的分布式存储,MongoDB 能够高效地支持大规模数据存储和访问需求。

第三个特性是 负载均衡。当 MongoDB 检测到各个 Shard 之间的数据分布不均时,会启动 Balancer 进程,对数据进行重新分配,以尽可能均衡各 Shard Server 的数据存储量。

此外,当 Shard Server 的存储空间不足,需要增加新的 Shard Server 时,系统会自动将部分数据迁移到新增的节点上。这种自动化的迁移过程不仅降低了手动迁移和验证数据的成本,还确保了系统的稳定性和扩展效率。

你可以看到,NoSQL 数据库中内置的扩展性方面的特性可以让我们不再需要对数据库做分库分表和主从分离,也是对传统数据库一个良好的补充。你可能会觉得,NoSQL 已经成熟到可以代替关系型数据库了,但是就目前来看,NoSQL 只能作为传统关系型数据库的补充而存在,弥补关系型数据库在性能、扩展性和某些场景下的不足,所以你在使用或者选择时要结合自身的场景灵活地运用。

6