基础概念
以披萨为例。您得到不同切片的披萨,然后与您的朋友分享这些切片。分片也称为数据分区,其工作原理与共享 Pizza 切片的概念相同。它基本上是一种数据库架构模式,其中我们将大型数据集拆分为较小的块(逻辑分片),并将这些块存储/分布在不同的机器/数据库节点(物理分片)中。每个块/分区都称为“分片”,每个分片都具有与原始数据库相同的数据库模式。我们以每一行恰好出现在一个分片中的方式分布数据。这是提高可扩展性的好机制一个应用程序。

优点
- 解决可扩展性问题:使用单一数据库服务器架构时,任何应用程序都会 在用户开始使用该应用程序时出现性能下降。读写查询变慢,网络带宽开始饱和。在某些时候,您将用完磁盘空间。数据库分片通过跨多台机器对数据进行分区解决了所有这些问题。
- 高可用性:单服务器架构的一个问题是,如果发生中断,那么整个应用程序将不可用,这对于拥有更多用户的网站来说是不利的。分片数据库没有这种情况。如果分片架构发生中断,那么只有一些特定的分片会宕机。所有其他分片将继续运行,并且整个应用程序不会对用户不可用。
- 加快查询响应时间:当您在具有大型单体数据库且没有分片架构的应用程序中提交查询时,需要更多时间才能找到结果。它必须搜索表中的每一行,这会减慢您提供的查询的响应时间。这不会发生在分片架构中。在分片数据库中,查询肯定只经过更少的行,并且您可以在更短的时间内收到响应。
- 更多写入带宽:对于许多应用程序来说,写入是一个主要瓶颈。在没有主数据库的情况下,序列化写入分片架构允许您并行写入并增加写入吞吐量。
- 扩展:对数据库进行分片有助于水平扩展,称为向外扩展。在水平扩展中,您在网络中添加更多机器并将负载分配到这些机器上以实现更快的处理和响应。这有很多优点。您可以同时做更多的工作,并且可以处理来自用户的高请求,尤其是在写入数据时,因为系统中存在并行路径。您还可以对通过不同网络路径访问分片的 Web 服务器进行负载平衡,这些分片由不同的 CPU 处理,并使用单独的 RAM 缓存或磁盘 IO 路径来处理工作。
缺点
- 系统更加复杂: 在应用程序中实现适当的分片数据库体系结构时,需要小心。这是一项复杂的任务,如果没有正确实现,那么您可能会丢失数据或者在数据库中得到损坏的表。您还需要在多个分片管理数据,而不是从单个入口点管理和访问数据。这可能会影响您的团队的工作流程,并可能会对某些团队造成潜在的破坏。
- 重新平衡数据: 在分片数据库体系结构中,有时分片会变得不平衡(当一个分片超过其他分片时)。考虑一个例子,您有两个数据库碎片。一个碎片库的客户名称以字母 A 至 M 开头,另一个碎片库的客户名称以字母 N 至 Z 开头。如果有这么多字母 L 的用户,那么
Shard one将比Shard two拥有更多的数据。这将影响应用程序的性能(降低速度) ,并且对于很大一部分用户来说,它将停止运行。A-M 碎片将变得不平衡,它将被称为数据库热点。为了克服这个问题并重新平衡数据,您需要对数据分布进行重新分片。将数据从一个碎片移动到另一个碎片不是一个好主意,因为它需要很多停机时间。 - 昂贵的链接: 在单个数据库中,可以轻松地执行连接以实现任何功能。但是在分片架构中,你需要从不同的分片中提取数据,并且需要在多个网络服务器上执行连接。你不能提交一个单独的查询来从不同的分片中获取数据。您需要为每个碎片提交多个查询,提取数据,并通过网络连接数据。这将是一个非常昂贵和耗时的过程。增加了系统的延迟。
- 没有原生支持: 分片不受每个数据库引擎的原生支持。例如,PostgreSQL 不包含自动分片特性,因此必须手动分片。您需要遵循 自我滚动 的方法。您将很难找到有关分片的提示或文档,并在实现分片期间对问题进行故障排除。
分片体系结构
基于 HASH 分片
在这里举个例子,我们获取一个实体的值,比如客户 ID、客户电子邮件、客户的 IP 地址、邮政编码等,并使用这个值作为散列函数的输入。此过程生成一个哈希值,用于确定需要使用哪个碎片来存储数据。我们需要记住,散列函数中输入的值都应该来自同一列(碎片键) ,以确保数据以正确的顺序和一致的方式放置。基本上,shard 键就像一个主键或一个单独行的唯一标识符。
比如你有三台数据库服务器,每一个请求都会有一个应用ID,每次注册时,该ID都会自增1。为了确定应该放在哪台服务器上,我们对这些应用程序ID执行对3的模运算。然后余数用于标识存储数据的服务器。

这个方案的缺点是缺乏弹性负载平衡,这意味着如果你尝试动态增加或移除数据库服务器的话,是一件非常麻烦且昂贵的过程。例如在上面的例子中,如果你想添加第五台服务器的话,那么你就需要对现有已存在的 key 进行重新映射,修正值并重新迁移到新的服务器上。然后散列函数中的3也需要调整为对应数字。而且在数据迁移过程中,你的新旧函数可能都会失效,应用服务暂时无法对用户提供服务,直到停机迁移结束。
注意:分片 key 不应该包含一个随机变化的值,他必须是静态的常量。
基于范围或水平分片
在该方案中,我们根据每个实体中都包含的给定范围值进行分片。这么说吧,比如你的数据库存有客户的名称和邮箱信息,那么你就可以根据这些信息进行分片。其中一个分片只存储以 A-P 开头的客户,剩下的全部存储到另外的分片中。

基于范围的分片方式是最容易实现分片的方式。每个分片都包含了不同的数据集,但是他们都与原始数据库具有相同的模式。在此案中,你只需要确定数据的范围,接着你就可以把对应的数据放进去。这非常适合那种非静态的数据(例如存储在校大学生的联系信息)
该方案的缺点也比较明显,就是你无法保证数据是均匀分布的。在上面的例子中,你可能有很多很多客户的名字在 A-P 之间,这样的话,一号分片就要承担大量的负载,而二号则基本没有请求。
垂直分片
在这个方案中,我们把一个表中的所有列进行拆分,并放到不同的表中。数据完全独立于一个分区或另外一个。此外,每一个分区都保存不同的列和行,把一个实体的不同特性放到不同机器的不同分片上。以 Twitter 为例,用户有自己的资料,关注者数量,发表的推文。我们可以把他自己的资料放在一个分片上,关注者放在第二个分片上,推文放在第三个分片上。

用这种方法,你可以单独分离和处理数据的关键部分(比如用户资料),非关键部分(比如推文)。并且分别建立不同的备份和一致性模型。这是该方案的最主要的优点。
该方案的缺陷就是在这种架构下,对于任意的查询你都需要把数据从不同的分片中合并起来返回,这就是不必要的增加了系统的研发和操作的复杂性。而且,如果你的应用程序不断维护,你又增加了更多的信息项,那么你就必须进一步的切分独属于这些特征的数据库出来。
基于目录分片
在这个方法中,我们为原始数据库创建并维护一个查询服务或是一张查询表。简单来说,查询表中将数据的特定分片键和其所在的数据库信息进行映射。这样,我们就能追踪哪些数据存在了哪个数据库中。

查找表告诉我们可以在哪里找到特定数据的一组静态信息。在上面的图例中,我们使用了 delivery zone 当做分片键。首先,客户端应用通过查找服务来确定数据具体被放在了哪个分片(数据库分区)里。当查找服务返回分片后,应用再去查询或更新分片中的实际数据。
基于目录分片的方式比范围和 HASH 分片更为灵活。在范围分片里,你必须确定范围的数值。在 HASH 分片中,你必须使用一个固定的散列函数,该函数后续还很难改变。而在目录分片的方案中,你可以非常自由的给分片中的数据项们使用任何的算法。而且,也非常容易动态的增加数据分片。
但该方案最大的缺陷就是查找表的单点故障问题,如果他突然崩溃了,那就将影响任何在表中写入或是访问的操作。
基于生成密钥的分片
在这个方案中,我们可以使用特殊的密钥生成规则来写入分片键。以订单号为例,假如我们有三个库,每个库有三张表,订单号类似于:xxxx-0103-221215-0000001,xxxx是业务类别或是前缀,221215是当天日期,0000001是自增长的数字。
我们主要讨论0103这个数字,它表明该数据位于一号库中的第三张表中。当客户端应用发起写入请求时,我们可以按照算法生成基于当前分片数的订单号返回。而且,在生成分片键规则时,我们有很多特别的方式,比如根据当前数据库的负载来选择将数据落入哪一个分区中。
该方案可以动态的增加数据库或表的数量,而不会影响原有数据的存储。但缺点就是很依赖规则生成的服务,如果想要数据均匀的分布,生成服务的代码就需要写的更加复杂。
基于密钥范围的分片
此方案是融合了 HASH 分片与范围分片总结而来,在保证分片键自增的前提下,创建三个数据库,共计十张表。每三个数据库分为一组,该分区组只负责指定范围内的数据存储。比如1组负责分片键为 0-4000万 内的数据,我们需要将这些数据先对3取模,确定数据库的位置,再对10取模,确定表的位置。

之后我们可以随意的增加新的分区组,只需要确定具体的数据范围即可,该方案也保证了数据的尽可能的一致性。
结论
当你应用程序的单个数据库无法处理或存储巨量增长的数据时,分片是一个非常好的方案。分片有助于扩展数据库并提升应用的性能。但是,他也对你的系统增加了一些复杂度。上述的方法和体系架构也非常清楚的展示了每一个分片技术的优缺点。所以在你决定选择什么样的方案之前,请确定你已经看完了本章,再根据你现有数据库的数据类型来确定什么样的分片方案更适合你。