分库分表基本原理
📦 本文已归档到:「blog」
分片(Sharding)的基本思想就要把一个数据库切分成多个部分,存储在不同的数据库(server)上,从而缓解单一数据库的性能问题。
1. 数据分片
数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果。 数据分片的有效手段是对关系型数据库进行分库和分表。分库和分表均可以有效的避免由数据量超过可承受阈值而产生的查询瓶颈。 除此之外,分库还能够用于有效的分散对数据库单点的访问量;分表虽然无法缓解数据库压力,但却能够提供尽量将分布式事务转化为本地事务的可能,一旦涉及到跨库的更新操作,分布式事务往往会使问题变得复杂。 使用多主多从的分片方式,可以有效的避免数据单点,从而提升数据架构的可用性。
通过分库和分表进行数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进行疏导应对高访问量,是应对高并发和海量数据系统的有效手段。 数据分片的拆分方式又分为垂直分片和水平分片。
1.1. 垂直分片
按照业务拆分的方式称为垂直分片,又称为纵向拆分,它的核心理念是专库专用。
在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将压力分散至不同的数据库。下图展示了根据业务需要,将用户表和订单表垂直分片到不同的数据库的方案。
垂直分片往往需要对架构和设计进行调整。通常来讲,是来不及应对互联网业务需求快速变化的;而且,它也并无法真正的解决单点瓶颈。垂直拆分可以缓解数据量和访问量带来的问题,但无法根治。如果垂直拆分之后,表中的数据量依然超过单节点所能承载的阈值,则需要水平分片来进一步处理。
1.2. 水平分片
水平分片又称为横向拆分。 相对于垂直分片,水平分片不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。 例如:根据主键分片,偶数主键的记录放入 0 库(或表),奇数主键的记录放入 1 库(或表),如下图所示。
水平分片从理论上突破了单机数据量处理的瓶颈,并且扩展相对自由,是分库分表的标准解决方案。
2. 分库分表的问题
2.1. 事务问题
解决事务问题目前有两种可行的方案:分布式事务和通过应用程序与数据库共同控制实现事务下面对两套方案进行一个简单的对比。
- 方案一:使用分布式事务
- 优点:交由数据库管理,简单有效
- 缺点:性能代价高,特别是 sharding 越来越多时
- 方案二:由应用程序和数据库共同控制
- 原理:将一个跨多个数据库的分布式事务分拆成多个仅处于单个数据库上面的小事务,并通过应用程序来总控各个小事务。
- 优点:性能上有优势
- 缺点:需要应用程序在事务控制上做灵活设计。如果使用 了spring的事务管理,改动起来会面临一定的困难。
跨库事务也是分布式的数据库集群要面对的棘手事情。 合理采用分表,可以在降低单表数据量的情况下,尽量使用本地事务,善于使用同库不同表可有效避免分布式事务带来的麻烦。在不能避免跨库事务的场景,有些业务仍然需要保持事务的一致性。 而基于 XA 的分布式事务由于在并发度高的场景中性能无法满足需要,并未被互联网巨头大规模使用,他们大多采用最终一致性的柔性事务代替强一致事务。
2.2. 跨节点 Join 的问题
只要是进行切分,跨节点 Join 的问题是不可避免的。但是良好的设计和切分却可以减少此类情况的发生。解决这一问题的普遍做法是分两次查询实现。在第一次查询的结果集中找出关联数据的 id,根据这些 id 发起第二次请求得到关联数据。
2.3. 跨节点的 count,order by,group by 以及聚合函数问题
这些是一类问题,因为它们都需要基于全部数据集合进行计算。多数的代理都不会自动处理合并工作。解决方案:与解决跨节点 join 问题的类似,分别在各个节点上得到结果后在应用程序端进行合并。和 join 不同的是每个结点的查询可以并行执行,因此很多时候它的速度要比单一大表快很多。但如果结果集很大,对应用程序内存的消耗是一个问题。
2.4. 数据迁移,容量规划,扩容等问题
来自淘宝综合业务平台团队,它利用对 2 的倍数取余具有向前兼容的特性(如对 4 取余得 1 的数对 2 取余也是 1)来分配数据,避免了行级别的数据迁移,但是依然需要进行表级别的迁移,同时对扩容规模和分表数量都有限制。总得来说,这些方案都不是十分的理想,多多少少都存在一些缺点,这也从一个侧面反映出了 Sharding 扩容的难度。
2.5. ID 问题
一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的 ID 无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得 ID,以便进行 SQL 路由. 一些常见的主键生成策略
2.5.1. UUID
使用 UUID 作主键是最简单的方案,但是缺点也是非常明显的。由于 UUID 非常的长,除占用大量存储空间外,最主要的问题是在索引上,在建立索引和基于索引进行查询时都存在性能问题。
2.5.2. 结合数据库维护一个 Sequence 表
此方案的思路也很简单,在数据库中建立一个 Sequence 表,表的结构类似于:
1 | CREATE TABLE `SEQUENCE` ( |
每当需要为某个表的新纪录生成 ID 时就从 Sequence 表中取出对应表的 nextid,并将 nextid 的值加 1 后更新到数据库中以备下次使用。此方案也较简单,但缺点同样明显:由于所有插入任何都需要访问该表,该表很容易成为系统性能瓶颈,同时它也存在单点问题,一旦该表数据库失效,整个应用程序将无法工作。有人提出使用 Master-Slave 进行主从同步,但这也只能解决单点问题,并不能解决读写比为 1:1 的访问压力问题。
2.5.3. Twitter 的分布式自增 ID 算法 Snowflake
在分布式系统中,需要生成全局 UID 的场合还是比较多的,twitter 的 snowflake 解决了这种需求,实现也还是很简单的,除去配置信息,核心代码就是毫秒级时间 41 位 机器 ID 10 位 毫秒内序列 12 位。
- 10—0000000000 0000000000 0000000000 0000000000 0 — 00000 —00000 —000000000000 在上面的字符串中,第一位为未使用(实际上也可作为 long 的符号位),接下来的 41 位为毫秒级时间,然后 5 位 datacenter 标识位,5 位机器 ID(并不算标识符,实际是为线程标识),然后 12 位该毫秒内的当前毫秒内的计数,加起来刚好 64 位,为一个 Long 型。
这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生 ID 碰撞(由 datacenter 和机器 ID 作区分),并且效率较高,经测试,snowflake 每秒能够产生 26 万 ID 左右,完全满足需要。
2.6. 跨分片的排序分页
一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。如下图所示:
上面图中所描述的只是最简单的一种情况(取第一页数据),看起来对性能的影响并不大。但是,如果想取出第 10 页数据,情况又将变得复杂很多,如下图所示:
有些读者可能并不太理解,为什么不能像获取第一页数据那样简单处理(排序取出前 10 条再合并、排序)。其实并不难理解,因为各分片节点中的数据可能是随机的,为了排序的准确性,必须把所有分片节点的前 N 页数据都排序好后做合并,最后再进行整体的排序。很显然,这样的操作是比较消耗资源的,用户越往后翻页,系统性能将会越差。
那如何解决分库情况下的分页问题呢?有以下几种办法:
如果是在前台应用提供分页,则限定用户只能看前面 n 页,这个限制在业务上也是合理的,一般看后面的分页意义不大(如果一定要看,可以要求用户缩小范围重新查询)。
如果是后台批处理任务要求分批获取数据,则可以加大 page size,比如每次获取 5000 条记录,有效减少分页数(当然离线访问一般走备库,避免冲击主库)。
分库设计时,一般还有配套大数据平台汇总所有分库的记录,有些分页查询可以考虑走大数据平台。
2.7. 分库策略
- 根据数值范围,比如用户 Id 为 1-9999 的记录分到第一个库,10000-20000 的分到第二个库,以此类推。
- 根据数值取模,比如用户 Id mod n,余数为 0 的记录放到第一个库,余数为 1 的放到第二个库,以此类推。
优劣比较:评价指标按照范围分库按照 Mod 分库库数量前期数目比较小,可以随用户/业务按需增长前期即根据 mode 因子确定库数量,数目一般比较大访问性能前期库数量小,全库查询消耗资源少,单库查询性能略差前期库数量大,全库查询消耗资源多,单库查询性能略好调整库数量比较容易,一般只需为新用户增加库,老库拆分也只影响单个库困难,改变 mod 因子导致数据在所有库之间迁移数据热点新旧用户购物频率有差异,有数据热点问题新旧用户均匀到分布到各个库,无热点实践中,为了处理简单,选择 mod 分库的比较多。同时二次分库时,为了数据迁移方便,一般是按倍数增加,比如初始 4 个库,二次分裂为 8 个,再 16 个。这样对于某个库的数据,一半数据移到新库,剩余不动,对比每次只增加一个库,所有数据都要大规模变动。补充下,mod 分库一般每个库记录数比较均匀,但也有些数据库,存在超级 Id,这些 Id 的记录远远超过其他 Id,比如在广告场景下,某个大广告主的广告数可能占总体很大比例。如果按照广告主 Id 取模分库,某些库的记录数会特别多,对于这些超级 Id,需要提供单独库来存储记录。
2.8. 分库数量
分库数量首先和单库能处理的记录数有关,一般来说,Mysql 单库超过 5000 万条记录,Oracle 单库超过 1 亿条记录,DB 压力就很大(当然处理能力和字段数量/访问模式/记录长度有进一步关系)。
在满足上述前提下,如果分库数量少,达不到分散存储和减轻 DB 性能压力的目的;如果分库的数量多,好处是每个库记录少,单库访问性能好,但对于跨多个库的访问,应用程序需要访问多个库,如果是并发模式,要消耗宝贵的线程资源;如果是串行模式,执行时间会急剧增加。
最后分库数量还直接影响硬件的投入,一般每个分库跑在单独物理机上,多一个库意味多一台设备。所以具体分多少个库,要综合评估,一般初次分库建议分 4-8 个库。
3. 读写分离
面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。
与将数据根据分片键打散至各个数据节点的水平分片不同,读写分离则是根据 SQL 语义的分析,将读操作和写操作分别路由至主库与从库。
读写分离的数据节点中的数据内容是一致的,而水平分片的每个数据节点的数据内容却并不相同。将水平分片和读写分离联合使用,能够更加有效的提升系统性能。
3.1. 挑战
读写分离虽然可以提升系统的吞吐量和可用性,但同时也带来了数据不一致的问题。 这包括多个主库之间的数据一致性,以及主库与从库之间的数据一致性的问题。 并且,读写分离也带来了与数据分片同样的问题,它同样会使得应用开发和运维人员对数据库的操作和运维变得更加复杂。 下图展现了将分库分表与读写分离一同使用时,应用程序与数据库集群之间的复杂拓扑关系。