阅读说明:
- 如果有排版格式问题,请移步www.yuque.com/mrhuang-ire… 《数据库优化-分库分表》,选择宽屏模式效果更佳。
- 本文为原创文章,转发请注明出处。如果觉得文章不错,请点赞、收藏、关注一下,您的认可是我写作的动力。
业务早期时,为了快速上线,在库表设计时,通常会设置单库单表,这样开发维护起来也简单,便于快速迭代上线。随着业务快速发展,数据规模也越来越大。尤其在互联网时代,海量数据基本是每个应用的共性,大量数据将对数据库性能产生影响。通常来说数据库表的数据量逐渐累积到一定的数量时(5000W 行或 100G 以上),操作数据库的性能会出现明显下降。一方面,Mysql在InnoDB引擎下索引是通过B+树实现,查询时IO次数跟树的高度有密切关系,树高度越高,IO次数越多,查询性能也就越差。另一方面,大量数据通常也会有大量的读写操作,数据库资源有限不足以应对大量的请求。
对于数据库性能问题,优先使用索引优化或采用一主多从部署方案进行读写库分离。如果依然存在瓶颈,有以下几种解决方案:
-
冷热数据分离: 将过往不经常访问的数据定时迁移到冷库,这样热数据查询性能得到提高,冷数据虽然查询变慢但是访问本身少,所以数据库整体性能有所提高。这种方案比较简单,但是对业务本身具有一定的要求。
-
分库分表: 对原有数据拆分到多个库,多张表上,分散读写压力。
分库分表方案比较实用,能够适应多种业务问题。本文将阐述分库分表方案。
分库分表方案
垂直拆分
包含两种方案:垂直分表和垂直分库。垂直分表通俗点就是"大表拆小表", 将列字段中不常用的,数据较大的字段拆分出来放到扩展表。垂直分库是将系统按照业务进行拆分,比如用户,商品,订单放到独立的库,并且部署到不同的服务器上。
水平拆分
水平拆分同样包含两种方案:水平分表和水平分库分表。水平分表按照某种规则(Range, Hash取模等), 将单表数据切分到多张表,俗称单库多表。水平分库分表将切分出来的多张表进一步拆分到不同的分库, 分库部署到不同的服务器上,俗称多库多表。
单库多表的情况下,数据库连接数、磁盘 I/O 以及网络吞吐等资源都是有限的,并发能力也是有限的。如果需要进一步提高并发能力,需要进一步采取分库方案。多库分表因为部署在不同的机器上,能有效缓解单库多表方案中单机和单库的性能瓶颈和压力。
原则上,如果数据量比较大,且热点数据比较集中,可以进行表垂直拆分。如果热点分散,可以使用水平分表。如果并发量比较高,可以考虑多库多表,是企业中经常采用的解决方案。
容量预估
具体分多少个库多少个表,需要考虑以下几个因素:
- 单表记录行数,容量上限参考值5000w。如果查询量高,需要降低容量上限。
- 单库记录数据,容量上限参考值500G。
- 单库写入TPS, 跟存储磁盘类型和并发度设置都有关系,参考值为2K。
在拆分时,起码满足三年内的数据容量需求,可根据三年后的数据量进行评估。考虑到机器性能的影响,设计时需要留有足够的冗余。
水平分库分表分片策略
原数据按照一定的规则分配到不同的库表,这个规则就是分片策略,包含分片键和分片算法。
分片键
分片键决定了数据落地的位置,也就是数据将会被分配到哪个数据节点上存储。在关系型数据库中,通常选择一个或者多个列字段。分片键选择需要考虑以下要素:
- 高频
分库分表后,SQL 只有带上分片键字段才能在指定节点运行,否则会导致全路由,即每个节点都执行。选择业务中的高频字段作为分片键可以保证大多数的 SQL 都能带上分片键高效执行,有效提高分片键的命中率。可以统计业务代码中的SQL或者统计执行次数最多的n条SQL, 按照执行的次数分析出使用哪个字段作为分片键。
- 数据均匀
尽量保证数据分布均匀,避免出现数据倾斜的问题。
- 事务在同一分片
事务在同一分片,对于写操作简单高效,减少了数据不一致的问题。
分片算法
分片算法则是用于对分片键进行运算,将数据划分到具体的数据节点中。常用的分片算法有很多:
- 哈希分片:对分片键进行哈希值计算,然后根据哈希值决定数据应该落到哪个节点上。例如,根据用户名称的哈希值对数据库的数量进行取模分片,同一个用户的数据将分配到同一个节点上。哈希方法一般采用取模,为便于后续扩容,分片数量选取2^n。
- 范围分片:分片键值按区间范围分配到不同的节点上。
- 查找映射:直接定义分片和分片键值之间的映射关系,将其维护在配置或数据库中。为了提高查询性能,通常配合缓存一起使用。
- 取模分片: 按照某个字段的取模结果进行分片。例如,根据用户id对数据库的数量进行取模分片,来决定该用户数据应该存储到哪个节点。
分库分表工具
- ShardingSphere: 全栈式分布式数据库解决方案,提供数据分片、读写分离、分布式事务、数据加密、影子库、高可用、弹性伸缩(Scale)等功能,支持任何遵循SQL标准的数据库(核心支持MySQL, PostgreSQL, openGauss, Oracle, SQLServer等),能够满足复杂的分布式场景需求。
- MyCAT: 经典的开源数据库中间件,提供数据分片、读写分离等基础功能,主要面向MySQL生态,具有丰富的路由策略和分布式事务处理能力,适用于大数据、高并发场景。
- Vitess: 云原生、大规模、面向MySQL的数据库集群系统, 主要用于管理海量MySQL集群,提供了分片、复制和执行分布式SQL查询的功能,适用于需要处理极其海量数据和超高并发的MySQL场景(PB级,百万级QPS)。
Apache ShardingSphere 凭借其强大的功能、灵活的部署、活跃的社区和良好的前景,通常是当前最通用和推荐的选择。Vitess 在超大规模MySQL运维场景下无可替代。MyCAT 在特定透明代理需求下仍有价值。
数据扩容流程
主要有以下几种方案。
停机扩容方案
在业务低峰期(通常是深夜),完全停止应用服务,然后执行数据迁移和配置变更。完成后,再重新启动服务。这是最传统、最“粗暴”但也最直接、实现相对简单的方案。
关键实施步骤
- 停止服务:预定时间点,停止所有应用程序服务。
- 数据迁移:从现有节点导出需要迁移的数据, 将导出的数据按照新的分片规则(例如,原2个分片扩展到4个分片)导入到新节点。
- 切换路由:修改 ShardingSphere、MyCAT 等中间件的配置,应用新的分片规则。
- 数据验证:进行基础功能验证和性能抽查。
- 重启服务:验证无误后,恢复应用服务,允许用户访问。
优点
- 实现简单: 技术难度相对较低,不需要复杂的增量数据同步和流量切换逻辑。
- 数据一致性容易保证: 在停机期间,数据是静态的,迁移过程简单直接,最终一致性容易达到。
- 成本较低: 不需要开发或部署复杂的双写、数据对比等组件。
缺点
-
业务中断: 最大的缺点! 需要较长时间的停机窗口(几小时甚至更久,取决于数据量),严重影响用户体验和业务连续性(7x24服务基本不可行)。
-
风险集中: 所有操作在有限窗口内完成,压力大,一旦迁移或配置出错,回滚复杂且耗时,可能导致窗口延长或事故。
-
时间压力: 必须在预定窗口内完成所有步骤,容易因意外情况导致超时。
-
影响范围广: 所有业务服务都需要停止。
双写平滑扩容方案
通过在不停机的情况下,让应用在一段时间内同时向新旧数据库节点写入数据(双写) ,并辅以数据迁移和流量灰度切换,最终完成扩容。目标是实现业务无感知或感知极小。
关键步骤
-
双写: 在应用代码或分库分表中间件配置中,启用双写逻辑。
-
数据迁移: 使用专门的数据迁移工具(如 ShardingSphere-Scaling, DataX, Canal + Kafka + Flink 等),将存量历史数据从旧节点按照新分片规则迁移到新节点,存量数据迁移完成后增量数据再迁移到新节点。迁移完成后,进行严格的数据一致性校验(全量和增量校验),确保新旧节点数据在双写开始时刻的基线+后续增量完全一致。这是保证平滑切换的基础。
-
灰度切读: 少量只读流量(如特定用户ID、特定查询类型) 切换到新分片规则,验证新节点数据的正确性和查询性能。根据验证情况,逐步将更多的只读流量切换到新规则。
-
关闭双写: 当确认新节点数据完整、一致、稳定后,修改应用/中间件配置,只写新节点。
优点
- 业务影响最小化: 核心优势! 理论上可以实现业务零停机(RTO≈0),用户无感知或感知极小(切换瞬间可能有毫秒级抖动)。
- 风险可控: 操作分阶段进行,每个阶段都可以验证和回退。数据迁移和切换过程相对独立。
- 适用于海量数据: 数据迁移可以在后台长时间运行,不受一次性窗口限制。
缺点
- 实现复杂: 技术难度高。需要在应用层或中间件层实现可靠的双写逻辑、数据迁移/同步工具、数据对比校验工具。
- 开发与测试成本高: 双写逻辑、迁移工具、校验脚本、切换流程都需要大量开发和严格的测试。
- 双写期间性能开销: 每次写操作需要执行两次(或更多次),增加数据库负载和网络延迟,可能对性能有10%-30%的影响。
- 双写一致性问题: 必须确保双写的原子性和最终一致性。如果双写过程中一个成功一个失败,需要完善的异常处理机制(如异步补偿)来保证数据最终一致。这是最大的技术挑战。
对于现代的核心业务系统,尤其是互联网服务,要求7x24小时高可用,不允许停机,双写平滑扩容是主流的、推荐的选择。
分库分表面临的问题
分库分表之后,虽然能够带来性能上的提升,但是由此引发想当多的问题。一旦分表,可能会涉及到多表分页查询、多表JOIN 查询,增加了业务的复杂度。而一旦分库,除了跨库分页查询、跨库 JOIN 查询,还将引发跨库事务的问题。这些问题无疑会增加我们系统开发的复杂度。
水平分库分表SQL查询列不带分片键
前文中讲过分片键选择时尽量选择高频字段,但是不可避免存在SQL不带分片键的场景。这里有两种解决方案:
- 索引表法
添加SQL中查询列到分片键的索引表。执行SQL时,先查索引表得到分片键,再根据分片键路由到对应的节点。如果请求并发量比较大,可以添加缓存提高查询性能。如果数据量很大,可以将其分库分表,这里可根据索引表中SQL查询列作为索引表的分片键。当然,也可复用原有的分片键进对索引表进行分库分表,此时必须搭上缓存,缓解索引表全路由时导致的性能影响。
- 基因法
对SQL中的查询列植入分片的基因,这样就能通过查询列直接计算出所在的分片节点。这种方案有比较大的局限性,历史数据需要清洗,不如索引表法灵活,适合项目刚开始就做好设计,以后要进行分库分表。
分布式事务问题
在单库中,数据库事务能够保证多张表的数据一致性。分库分表之后,一次请求可能会涉及多个库表的写操作,那么就会带来数据一致性的问题。
对于此,业务开发上采用分布式事务解决方案,企业中两种通用解决方式,两阶事务提交(2PC)以及补偿事务提交(TCC)。企业项目中经常采用的补偿事务提交(TCC),添加必要监控+对账等辅助手段,最终达到数据最终一致性。
跨节点 JOIN 查询问题
分库分表之后,原本SQL涉及的表拆分到不同的库之后,可能产生跨库和多表查询,这将产生跨库JOIN的问题。比如用户在查询订单时,往往还需要通过表连接获取商品信息,而商品信息表可能在另外一个库中,这就涉及到了跨库 JOIN 查询。对于固定不变的信息,我们会冗余表或冗余字段来优化跨库 JOIN 查询。
跨节点分页查询问题
分库分表之后,原本SQL涉及的表拆分到不同的库之后,另一个比较麻烦的是分页查询。比如订单系统中指定过滤条件翻页查看最近的订单。查看碰到此问题,一种方案是谨慎选择分片键,试图将请求量比较大的查询放到一个库完成。另外一种方案是同步到专门的搜索引擎,对全局数据建立索引。分页查询先在搜索引擎执行搜索,再回到数据库进行查询。
数据倾斜问题
若对分库分片策略选择不当,容易数据数据分布不均匀,出现数据倾斜问题。解决方案是根据业务场景具体分析,选择合适的分片策略。对于少数场景,选择特殊处理,对数据量大的一方,选择单独的节点进行存储,比如,订单系统,某些用户订单量特别多时,需要调整分片算法,单独成立一个分库,把数据量较大的几个用户id路由到这个分库。
电商订单系统示例
文中最后讲解一个订单系统分库分表的方案,订单系统有买家维度(用户查看订单列表、订单详情), 订单号维度(客服查看用户订单、其他系统查看订单)。那么选择的分片键有以下两种方案:1.用户Id;2.订单号。
两种方案都不直接满足要求,需要进行改造。
- 用户id方案:使用订单号查询时,需要做一个订单号-> userId的映射表。
- 订单号方案:需要同步到搜索引擎之后,支持列表查询。
当然,如果是新系统,订单号在最开始的设计上可以采用基因法加上分片信息,对于后续查询复杂度能够简化很多。
分片算法上,有以下两种方案:
- 范围分片。比如用户id从1-100,000放到0分库,100,001到200,000放到1分库,以此类推。
- hash分片。用户id对分库数取模。
范围分片容易出现数据倾斜问题,用户id是增长的,老用户比新用户订单量更多。所以优先选择用户取模。
迁移时,为了不影响线上运行,通常采取双写迁移方案,大致步骤如下:
1.数据双写,使数据同时写入旧数据库和新数据库,需要检查两边数据的一致性。
2.双读去重,新旧数据库同时读取,移除重复数据。
2.对存量数据进行不停机迁移。
4.切换为只读新数据库。
参考
cloud.tencent.com/developer/a…
zhuanlan.zhihu.com/p/629492994