> * 原文地址：[Understanding Database Sharding](https://www.digitalocean.com/community/tutorials/understanding-database-sharding)
> * 原文作者：[Mark Drake](https://www.digitalocean.com/community/users/mdrake)
> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)
> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/understanding-database-sharding.md](https://github.com/xitu/gold-miner/blob/master/TODO1/understanding-database-sharding.md)
> * 译者：[Romeo0906](https://github.com/Romeo0906)
> * 校对者：[HearFishle](https://github.com/HearFishle)，[ziyin feng](https://github.com/Fengziyin1234)

# 理解数据库分片

![](https://community-cdn-digitalocean-com.global.ssl.fastly.net/assets/tutorials/images/large/Database-Mostov_v4.1_twitter-_-facebook.png?1549487063)

### 概述

任何蓬勃发展的应用或者网站，最终都需要扩容开来以适应流量的增长。对于数据驱动的应用和网站来说，以一种能够保障数据安全和完整性的方式进行扩容尤为重要。要预测一个网站或应用将来会有多火，以及它能火多久是非常困难的，因此一些团队选择的数据库架构都支持动态扩展，

在本篇概念文中，我们将会讨论一种支持动态扩展的数据库架构：**分片数据库**。近年来，数据库分片技术非常抓人眼球，但很多人并不了解它到底是什么，以及什么场景下对数据库进行分片才能物尽其用。我们将在文中讨论什么是数据库分片，分析其优劣以及一些常见的数据库分片方法。

## 什么是数据库分片？

数据库分片是一种**横向分区**的数据库架构模式，所谓的横向分区技术就是将一个表中的数据按行拆分为多个不同的表的实践方式，这些不同的表被称作分区。每个分区都拥有相同的模式和相同的列，但是数据行却完全不同。同样的，每一个分区中的数据都是唯一的，并且独立于其他的分区。

结合**纵向分区**来理解横向分区或许会更容易一些。在纵向分区的表中，成列的数据被分离到另外的新表中。每个纵向分区中的数据都独立于其他分区，并且每个分区的行和列都不相同。下面的图展示了如何对表进行横向和纵向分区：

![例：表的横向分区与纵向分区](https://assets.digitalocean.com/articles/understanding_sharding/DB_image_1_cropped.png)

数据库分片包括将数据分成若干个子集，称为**逻辑分片**。逻辑分片分布在不同的数据库节点上，这些节点被称为**物理分片**。因此，每个物理分片能够被划分为多个逻辑分片。尽管如此，所有这些分片集合中的数据一起表现出来在逻辑上就如同一整个数据集。

数据库分片是[**无共享架构**](https://en.wikipedia.org/wiki/Shared-nothing_architecture)的一个例证。这意味着分片行为是自治行为；各个分片之间不会共享任何数据或者计算资源。然而在某些情况下，将某些特定的数据表作为引用表复制到各个分片中也非常有用。比如，某个应用依赖固定的换算率在不同的重量单位之间换算。设计数据库的时候，把保存所需的换算率数据的表复制到每个分片中，将会帮助确保在每个分片中都保存着换算查询所需要的数据。

数据库分片通常在应用级别实现，应用里的代码定义了用于读、写的数据库分片。然而，一些数据库管理系统内置了分片功能，允许你直接在数据库级别实现分片操作。

了解了以上关于数据库分片的基本知识，我们来看这种数据库架构下的优点和缺点。

## 数据库分片的优点

数据库分片技术最吸引人的要数它能简化**横向扩展**的工作，也被称为**扩张（scaling out）**。横向扩展指的是在已有的基础上增加更多的机器来疏散负载，从而承载更多的流量、实现更快的处理速度。经常拿来与之比较的是**纵向扩展**，或者叫做**扩容（scaling up）**，指的是升级现有机器的硬件配置，通常指加内存、加 CPU。

我们可以很容易地在某一台机器上运行一个关系型数据库实例，只需要通过升级机器的计算资源即可对其进行扩容。然而，所有非分布式的数据库最终都会受限于机器的存储容量和计算力，因此能够自由地横向扩展将使你的应用变得更加灵活。

另一个选择数据库分片架构的原因是提高查询的响应速度。假如不曾对数据库做分片处理，当你向数据库发送查询指令的时候，可能需要查询表中的所有行才能找到需要的数据。对于拥有庞大的单个数据库的应用来说，查询将会变得非常慢。通过分片技术，将数据分散到多个表，查询的时候将会在少部分数据中查找需要的数据，同时能够更快地返回结果集。

数据库分片还能降低宕机的影响，从而使应用更加稳定。如果你的应用或网站依赖于一个未经分片的数据库，宕机有可能会使得整个应用都不可用。但假如是分片的数据库，宕机也只会影响其中的一个分片。尽管我们的应用或网站的某些功能可能无法被一些用户访问，但总的影响仍然会比整个数据库都挂掉要小很多。

## 数据库分片的缺点

尽管对数据库分片能够易于扩展、提高性能，但是它也有一些局限性。我们将在这部分讨论其部分局限性，并解释为什么不应该一股脑儿地对所有的数据库做分片处理。

数据库分片中遇到的第一个困难就是正确地实现数据库分片架构相当复杂。如果没能正确操作，你将会承受极大的风险，因为在数据库分片过程中可能会造成数据丢失、数据表损坏等后果。即使成功了，数据库分片操作很可能会极大地影响团队的工作流。大家不再通过单点来读取、管理数据，他们将跨多个数据库分片来管理数据，这可能会在一些团队中造成混乱的局面。

还有一个问题，大家可能在数据库分片之后遇到分片不平衡的现象。举个例子，假如你的数据库有两个单独的分片，一个保存着姓名以 A 到 M 字母开头的客户，另一个保存着姓名以 N 到 Z 字母开头的客户。然而，你的应用有大量的姓名以 G 字母开头的用户。因此，A-M 分片逐渐产生了比 N-Z 分片更多的数据，导致了应用变得很慢，甚至在部分用户的使用中卡住。这里 A-M 分区就是所谓的**数据库热点**。这个例子中，所有数据库分片带来的好处因为应用的反应慢和崩溃而变得一文不值。因此，数据库需要被修复，被重新分片来达到更加合理的数据分布。

另一个重要的缺点就是，一旦数据库分片完成了，就很难恢复到分片之前的架构了。分片操作之前所做的任何备份都不包含分区之后写入的数据。因此，要重建原始的未分片的数据库架构就必须将新分区的数据和旧的备份数据融合在一起，或者将各个分区的数据库导入到单个数据库中。这两种方式都费时费力，代价不菲。

最后一个需要考量的缺点，并非所有的数据库引擎都原生地支持分片操作。比如，PostgreSQL 没有自动分片的功能，因此我们只能手动地给 PostgreSQL 数据库分片。虽然有很多 Postgres 的分支版本具备自动分片的功能，但是这些版本的发布通常都晚于最新的 PostgreSQL 版本，并且会缺少某些功能。一些专业的数据库技术 - 比如 MySQL 集群或者某些数据库服务产品（比如 MongoDB Atlas）- 具备自动分片的功能，但是普通版本的数据库关系系统中并不具备这样的功能。正因如此，数据库分片操作通常需要你自己动手，这也意味着很难找到关于数据库分片操作的文档或者排查问题的建议。

当然，这些仅仅是数据库分片操作之前需要考虑的一些常见问题。面对不同的使用场景，数据库分片操作可能会遇到更多的问题。

至此我们讨论了一些数据库分片的优缺点，接下来我们将介绍几种数据库分片的架构方式。

## 数据库的分片架构

一旦你决定要对数据库进行分片，接下来你要做的就是弄清楚如何去实现它。查询数据的时候或者保存数据到分片的数据库或数据表中的时候，选用正确的分区至关重要。否则，将会造成数据丢失或者令人痛苦的慢查询。在这部分，我们将要介绍一些常用的数据库分片架构，每种架构在不同分片之间保存数据的方式都略有不同。

### 基于键的数据库分片

**基于键的分片**也叫**基于哈希的分片**，使用新写入数据的值 —— 比如客户 ID、客户端 IP、ZIP 码等等 —— 通过**哈希函数**决定保存的分片位置。哈希函数将输入的数据（如用户的邮件地址）转换成离散数据（也叫做哈希值）输出。在数据库分片中，哈希值作为数据库分片 ID 将数据保存到对应的分片中。总的来说，整个过程是这样的：

![例：基于键的数据库分片图示](https://assets.digitalocean.com/articles/understanding_sharding/DB_image_2_cropped.png)

为了保证所有数据的保存位置正确，保存行为一致（即相同的数据每次都保存到相同的位置，译者注），哈希函数的输入值应当为同一列数据。这列数据被称为**分片键**。简单一点，分区键就类似于[**主键**](https://en.wikipedia.org/wiki/Primary_key)，两者都是用来给每个数据/分片创建一个唯一标识的。一般来说，分片键应该是静态的，也就是说它不应该保存可变的数据。否则，更新数据的时候将会产生更多的工作量，还可能会降低性能。

尽管基于键的数据库分片是一种非常常见的数据库分片架构方式，在这种架构上动态地增加或移除数据库服务器却是一件困难的事情。当你增加服务器的时候，每个数据库分片的哈希值都需要调整，因此，即使不是全部数据，也会有很多数据需要重新映射到正确的哈希值，然后迁移到正确的服务器。当你对数据进行均衡性调整的时候，新旧哈希函数都将失效。因此，在数据迁移期间，数据库服务器将不能写入任何数据，应用也将暂停服务。

这种策略最吸引人的是它能够将数据平均地分配，从而避免数据热点的出现。另外，因为它使用算法来分配数据，无需像其他策略（基于范围或基于目录的数据库分片策略）那样需要维护一个数据分布的映射关系。

### 基于范围的数据库分片

**基于范围的数据库分片**基于给定数据的范围来对数据做分片处理。这么说吧，假如你有一个保存着零售店的商品信息的数据库，你可以创建一些数据库分片，然后根据价格区间将商品数据分布到不同的分片中，像这样：

![例：基于范围的数据库分片图示](https://assets.digitalocean.com/articles/understanding_sharding/DB_image_3_cropped.png)

基于范围的数据库分片最大的好处就是实行起来相对简单一些。每个分片都保存着不同的数据集，但它们却有着与众不同的模式，当然也不同于原始的数据库。应用代码中只需读取数据落在了哪个区间，并且将其写入对应的分区即可。

另一方面，基于范围的数据库分片并不能防止数据的不均匀分布，也不能防止出现前文所说的数据库热点。再来看上文的图例，即使每个分片保存的数据量相等，但是还可能会产生某些产品比其他更加受欢迎，因此各个分片在数据的读取上也会出现分布不均的情况。

### 基于目录的数据库分片

若要实现**基于目录的数据库分片**，你必须创建并维护一个**查找表**，并在表中通过数据库分片的键来记录数据保存的位置。简而言之，查找表就是保存着如何查找指定数据的静态数据集的表。下图展示了一个基于目录的数据库分片的简单的例子：

![例：基于目录的数据库分片图示](https://assets.digitalocean.com/articles/understanding_sharding/DB_image_4_cropped.png)

这个例子中，**Delivery Zone** 列被定义为分片的键，分片键的值和该行数据保存的分区被一起写入查找表中。这跟基于范围的数据库分片类似，但是我们并没有判断数据落在了哪个区间，而是将每条数据绑定到了其所在的分片。相对于基于范围的数据库分片来说，基于目录的数据库分片不失为一个更好的选择，假如分片键的基数本来就很小，那么对于数据库分片来说，保存某个范围的键将变得没有意义。当然，基于目录的数据库分片也不同于基于键的数据库分片，它并不使用哈希函数来处理分片键，而只是在查找表中检查键名然后找到数据需要被写入的位置。

基于目录的数据库分片架构最吸引人的地方就是灵活性。基于范围的数据库分片架构要求你必须制定值的区间，而基于键的数据库分片架构则要求你必须使用一个固定的哈希函数，正如前文所说，要修改该函数是非常困难的一件事。而基于目录的数据库分片架构允许你使用任何系统、任何算法来指定数据保存的分片位置，而且添加分片也相对简单一些。

尽管基于目录的数据库分片架构是目前讨论过的最灵活的分片方法，但是每一次查询、写入数据之前都需要先查询查找表可能会影响应用的性能。而且，查找表可能会产生单点故障：如果查找表损毁了，或者由于其他原因失效了，将会影响写入新数据或者查询现有数据的功能。

## 我应该对数据库分片吗？

究竟该不该采用分片的数据库架构一直是备受争议的问题。一些人认为当数据库达到一定量级的时候，采用分片架构是必然的；另一些人则认为由于操作复杂，除非是万不得已，否则不应该使用分片架构。

由于其复杂性，数据库分片架构多用于处理非常大量的数据。以下这些场景中，对数据库分片可能会非常有用：

* 应用数据不断增长，超出了单点数据库的存储能力。
* 数据库的读写超出了单点数据库或只读从库（读写分离架构下，译者注）的处理能力，从而导致了响应慢或超时。
* 应用所需的网络带宽超出了单点数据库或只读从库的可用带宽，从而导致了响应慢或超时。

对数据库分片之前，你最好先尝试其他的方式来优化你的数据库。比如：

* **使用远程数据库**。如果你有一个庞大的应用，其所有的组件都依赖于同一个数据库服务器，你可以考虑将数据库迁移到一台单独的机器上来提高其性能。这不会像数据库分片那样复杂，因为所有的数据库表都还是完整的。并且，这种方式还允许你能够抛开其他基础设施，单独地对数据库做纵向扩展。
* **使用[缓存](https://en.wikipedia.org/wiki/Database_caching)**。如果应用受制于读数据的性能，使用缓存是改进性能的一种方式。缓存通过将请求过的数据暂时地保存在内存中，加速后续的数据访问。
* **创建若干个只读从库**。另一种能够改进读取性能的方法是将数据从**主库**拷贝到若干个**从库**中。这样一来，新的写操作将使用主库，之后再拷贝到从库中，而读操作将使用从库。这种读写分离的模式使得每一台机器都不会承受过大的负载，有助于防止机器变慢甚至崩溃。值得注意的是，创建只读从库需要更多的计算资源、花更多的钱，某些情况下，这些将会使你步履维艰。
* **升级服务器**。一些情况下，升级数据库服务器的配置比数据库分片简单多了。对比创建只读从库，升级服务器配置可能会花更多的钱。因此，除非这真的是你最好的选择，否则不应该对机器扩容。

谨记只有当你的应用或者网站增长超过了一定量级的时候，以上这些方法都不足以有效地改进其性能的时候，数据库分片才真的是最佳选择。

## 总结

数据库分片对尝试横向扩展数据库的人来说是一个非常好的方案。然而，它也将模型变得更加复杂，也为应用增加了很多潜在的故障点（每个数据库分片都有可能发生故障，译者注）。对一些人来说，数据库分片可能会非常有必要，但是对于另一些人来说，创建和维护数据库分片架构所花费的时间和资源将会大于它带来的好处。

通过阅读本篇概念文，你应该已经对数据库分片的优劣势有了更加清醒的认识。以后，你可以使用本文的观点来判断你的应用是否真的需要数据库分片架构。

> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。

---

> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。
