写入数据量增加时,如何实现分库分表?

高并发场景下,为提升数据库性能和安全性,常采用读写分离的优化方案。这种方法利用主从复制技术,将数据复制为多份,从而提升对大量并发读请求的处理能力,增强数据库的查询性能。同时,通过在多个节点中存储完整数据,进一步保障数据的安全性。当某个数据库节点(主库或从库)发生故障时,系统仍能依赖其他节点的完整数据来保证数据的可用性,不会造成数据丢失。

在电商系统中引入读写分离后,架构示例如下:

  1. 主库:负责所有写操作(如新增、更新、删除等),并将数据同步至多个从库。
  2. 从库:专用于处理读操作,从而分担主库的查询压力。
  3. 负载均衡:在处理读请求时,系统将流量分配至不同的从库节点,以提高查询性能。
  4. 故障恢复:若某个节点宕机,系统仍能利用其他节点的数据来提供正常服务。

在电商系统的订单量突破五千万后,尽管流量的提升是好消息,但也带来了前所未有的数据库压力。现有的单表存储方式难以承载如此庞大的数据量,查询和写入性能都在下降,且磁盘空间告警频发。为保证系统正常运转,我分析了当前面临的主要问题,并寻求相应的解决方案。

1. 查询性能问题

系统正在持续扩展,用户和订单数据迅速增长,导致数据库单表数据量突破千万甚至亿级别。这时,即便使用了索引,随着数据量的增加,索引占用的空间也逐渐增大。当数据库无法缓存全量索引信息时,查询便需要频繁从磁盘读取索引数据,从而降低查询性能。

解决方案

:优化查询性能的关键在于减少索引的压力。一种方法是对数据进行分片,将数据按一定规则分散到多个表或库中,缩小单个表的索引范围。此外,也可考虑将查询请求引导至专门的从库,进一步分摊主库压力。

2. 数据量增长引发的存储问题

随着订单数据的持续增加,数据库的磁盘空间消耗加剧,备份和恢复时间变长,给系统带来了存储和恢复的双重压力。

解决方案

:为了让数据库支持大规模数据量,可以采用分库分表策略,将数据分散存储到不同的物理库中,缓解单机磁盘存储的瓶颈。这不仅减少了备份时间,也缩短了数据恢复时间,从而保障系统的高可用性。

3. 模块故障隔离问题

当前系统中,不同模块的数据(如用户数据和用户关系数据)都存储在同一个主库中。一旦主库发生故障,所有模块的服务都会受到影响。

解决方案

:为实现模块的故障隔离,可以对不同模块的数据进行逻辑分库,将各模块的数据分散到不同的数据库实例中。这样,即便某个模块的数据库出现问题,其他模块依然能够正常运行,增强了系统的鲁棒性。

4. 高并发写入带来的性能瓶颈

基于测试数据,当前在 4 核 8G 的云服务器上,MySQL 5.7 的写入性能(约 500 TPS)远低于查询性能(约 10000 QPS)。随着系统写入请求量的增加,写入压力日益加重,单一数据库实例难以支撑更高的并发写入。

解决方案

:为解决高并发写入问题,分库分表是一种有效手段。通过将数据水平切分到多个数据库实例或表中,分摊数据库的写入压力,提升整体并发写入能力,突破单机存储和性能瓶颈。

总结来看,上述问题主要归结为数据库的写入压力和可用性问题。采用数据分片策略,进行合理的“分库分表”设计,可以有效缓解这些问题,提升系统的整体性能和可用性。

分库分表是一个很常见的技术方案,你应该有所了解。那你会说了:“既然这个技术很普遍,而我又有所了解,那你为什么还要提及这个话题呢?”因为以我过往的经验来看,不少人会在“分库分表”这里踩坑,主要体现在:1. 对如何使用正确的分库分表方式一知半解,没有明白使用场景和方法。比如,一些同学会在查询时不使用分区键;2. 分库分表引入了一些问题后,没有找到合适的解决方案。比如,会在查询时使用大量连表查询等等。

如何对数据库做垂直拆分

分库分表是一种常见的数据分片方式,其核心思想是通过一定策略将数据尽可能均匀地分布在多个数据库节点或表中。这种方式不同于主从复制,后者将数据全量复制到多个节点,而分库分表则是让每个节点只保存部分数据,从而有效减少了单个数据库节点和表中的数据量。这不仅解决了存储瓶颈问题,还大幅提升了数据查询性能。

此外,数据被分散到多个节点后,写入请求也不再集中在单一主库,而是分散到多个数据分片节点上,增强了并发写入的能力。比如,在我曾负责的一个直播项目中,我们需要存储用户和系统在直播间的消息。热门直播间的留言量常常上万,长期积累下来,数据量达到了数亿级别,查询性能和存储空间都难以承受。为此,我们不得不重构系统,通过分库分表来分摊写入和存储压力。项目中我们启动了多个数据库并完成了单库数据的迁移和分片校验,这项工作虽然耗时费力,但最终成功地提升了系统的稳定性和效率。

数据库分库分表的方式主要有两种:垂直拆分和水平拆分。掌握拆分方式的应用场景是关键,而理解其原理是掌握数据分片的核心。在学习时,最好结合自身的业务需求,思考如何更有效地进行拆分。

垂直拆分

垂直拆分,顾名思义,是将数据库“竖着”拆分,即按照业务类型将表分配到不同的数据库中。其核心理念是专库专用,将耦合度较高的表放在同一个库中。可以把垂直拆分类比为整理衣物:把羽绒服、毛衣、T恤分别放入不同的格子中。这种方式能够有效解决数据层面的故障隔离问题。例如,当某一模块的数据库发生故障时,影响的仅是该模块的功能,而不会波及其他模块的功能。

我还是以微博系统为例来给你说明一下。

在微博系统中有和用户相关的表,有和内容相关的表,有和关系相关的表,这些表都存储在主库中。在拆分后,我们期望用户相关的表分拆到用户库中,内容相关的表分拆到内容库中,关系相关的表分拆到关系库中。

对数据库进行垂直拆分是一种较为常规的处理方式,且在实际业务中较为常见。垂直拆分后,虽然能暂时缓解数据库在存储容量上的瓶颈,但这并不意味着问题完全解决。垂直拆分只能在一定程度上减少存储压力,但它并不能应对单个业务模块数据量剧增的情况。

当某一业务库数据量暴增时,比如在微博系统中,用户关系数据已达到千亿级别,单个数据库或数据表远远无法满足存储和查询需求。这时,仅仅依赖垂直拆分是不够的,你还需要进一步将数据拆分到多个数据库和数据表中。这种方法就是水平拆分,用来应对单表或单库内数据量过大的问题。

水平拆分可以有效地将数据分布在多个数据库和表中,从而进一步提升系统的扩展性和查询性能。

如何对数据库做水平拆分

拆分的规则有下面这两种:

一种常见的水平拆分方法是基于某个字段的哈希值进行分片,这种方法尤其适用于实体表,如用户表、内容表等。通常,我们会选择这些表的 ID 字段作为拆分依据。

例如,假设需要将用户表拆分为 16 个库,每个库下有 64 张表。具体步骤如下:

  1. 计算哈希值:先对用户 ID 进行哈希处理,将 ID 值尽量打散以便均匀分布。
  2. 确定分库索引:将哈希值对 16 取余,得到分库的索引值。
  3. 确定分表索引:再将哈希值对 64 取余,得出分表的索引值。

通过这种方式,数据被均匀地分配到多个库和表中,从而减轻了单一库和表的存储与查询压力

另一种常用的水平拆分方式是基于某个字段的区间进行拆分,通常选择时间字段来划分。例如,在内容表中有“创建时间”字段,我们常常需要根据时间查看某人发布的内容,可能是昨天的内容,也可能是一个月前的内容。

在这种场景下,可以按照创建时间的区间进行分库分表。例如,将一个月的数据放在一张表中,这样在查询时,可以先根据创建时间快速定位数据所在的表,然后再根据其他查询条件获取数据。这种方式特别适合列表型数据的存储,比如用户在一段时间内的订单或发布的内容。

不过,这种区间划分方法也有可能导致热点问题。因为用户通常会更关注近期的内容或订单,因此查询最新数据的请求会更多,从而对系统性能造成一定压力。

此外,这种方式还要求提前创建好数据表。否则,如果到了2020年元旦,数据库管理员(DBA)忘记建立新的表,就会导致2020年数据无表可写,进而引发故障。因此,做好表的提前规划和维护非常重要。

数据库在分库分表之后,数据的访问方式也有了极大的改变,原先只需要根据查询条件到从库中查询数据即可,现在则需要先确认数据在哪一个库表中,再到那个库表中查询数据。这种复杂度也可以通过数据库中间件来解决,我们在08 讲中已经有所讲解,这里就不再赘述了,不过,我想再次强调的是,你需要对所使用数据库中间件的原理有足够的了解,和足够强的运维上的把控能力。不过,你要知道的是,分库分表虽然能够解决数据库扩展性的问题,但是它也给我们的使用带来了一些问题。

解决分库分表引入的问题

分库分表带来的一个主要问题是分库分表键(也称为分区键)的引入,即对数据库进行分库分表时所依据的字段。无论是通过哈希分片还是区间分片,首先都需要选择一个字段作为分区键。然而,这也带来了一个限制:之后的所有查询都必须包含该分区键才能找到数据所在的库和表。否则,查询将不得不遍历所有库和表。

例如,如果我们将数据拆分成 16 个库,每个库包含 64 张表,那么一次查询可能会扩展为 16 × 64 = 1024 次查询,导致查询性能急剧下降。

当然,对于这个问题,也有一些解决方法。比如,在用户库中可以使用 ID 作为分区键,但如果之后需要按昵称查询用户,理论上可以再按昵称做一次拆分。然而,这会大幅增加存储成本。如果以后还需要按注册时间查询,是否要继续进行新的拆分呢?这种方法显然不具备长远性,因此需要更加灵活的解决思路。

因此,较为合适的解决方案是建立一个昵称和 ID 的映射表。在查询时,可以先通过昵称查找到对应的 ID,再利用 ID 获取完整的数据。这个映射表同样可以采用分库分表策略,虽然需要一定的存储空间,但由于表中仅包含昵称和 ID 两个字段,所需空间相比重新拆分主表要少得多。

分库分表引入的另一个问题是某些数据库特性的实现可能变得更加复杂。例如,多表的 JOIN 操作在单库情况下可以通过一条 SQL 语句完成,但拆分到多个数据库后,无法跨库执行 JOIN。不过好在我们对 JOIN 的需求并不高,即使有,也通常是将两个表的数据提取到业务代码中,再进行数据筛选,尽管实现稍显复杂,但仍然可行。

另外,在未进行分库分表之前,获取数据总数只需在 SQL 中执行 count() 即可;而数据分散到多个库表中后,就需要其他方案来实现。例如,可以将计数数据单独存储在一张表中,或将其记录到 Redis 中。

尽管分库分表会带来一定的操作复杂性,但相比它对系统扩展性和性能的提升,依然值得实施。经过分库分表的系统,能够突破单机的容量和请求量瓶颈。正如我们电商系统中的订单表,正是通过分库分表,才解决了因数据量激增导致的性能衰减和存储容量瓶颈。

所以,从我的经验出发,对于分库分表的原则主要有以下几点:

在考虑分库分表时,首先要遵循一个原则:如果系统在性能上还没有遇到瓶颈,那么尽量不要实施分库分表。

如果确实需要分库分表,那么应尽量一次到位,比如选择 16 个库、每个库包含 64 张表,这样的方案基本可以满足未来几年内的业务需求,避免频繁调整带来的复杂性。

另外,许多 NoSQL 数据库(如 HBase、MongoDB)提供了自动分片(auto-sharding)功能。如果团队对这些组件有较强的熟悉度和运维能力,可以考虑使用这些 NoSQL 数据库来替代传统关系型数据库,从而更高效地管理大规模数据。

在我看来,许多人并没有真正理解拆分的必要性及其潜在影响,只是盲目跟随大厂的拆分方法,导致问题频出。因此,在使用某个方案解决问题时,务必要弄清原理,了解方案可能带来的问题,并提前考虑应对之道。做到知其然并知其所以然,才能在解决问题的同时避免踩坑。

9