水平分库分表的关键步骤和技术难点

本文探讨了水平分库分表的技术细节,包括分片规则的选择、数据迁移、跨分片查询处理等,并介绍了分布式全局唯一ID生成方案及分片策略。此外,还对比了表分区与分库分表的不同应用场景。

在之前的文章中,我介绍了分库分表的几种表现形式和玩法,也重点介绍了垂直分库所带来的问题和解决方法。本篇中,我们将继续聊聊水平分库分表的一些技巧。

分片技术的由来

关系型数据库本身比较容易成为系统性能瓶颈,单机存储容量、连接数、处理能力等都很有限,数据库本身的“有状态性”导致了它并不像Web和应用服务器那么容易扩展。在互联网行业海量数据和高并发访问的考验下,聪明的技术人员提出了分库分表技术(有些地方也称为Sharding、分片)。同时,流行的分布式系统中间件(例如MongoDB、ElasticSearch等)均自身友好支持Sharding,其原理和思想都是大同小异的。

分布式全局唯一ID

在很多中小项目中,我们往往直接使用数据库自增特性来生成主键ID,这样确实比较简单。而在分库分表的环境中,数据分布在不同的分片上,不能再借助数据库自增长特性直接生成,否则会造成不同分片上的数据表主键会重复。简单介绍下使用和了解过的几种ID生成算法。

 

  1. Twitter的Snowflake(又名“雪花算法”)
  2. UUID/GUID(一般应用程序和数据库均支持)
  3. MongoDB ObjectID(类似UUID的方式)
  4. Ticket Server(数据库生存方式,Flickr采用的就是这种方式)

其中,Twitter 的Snowflake算法是笔者近几年在分布式系统项目中使用最多的,未发现重复或并发的问题。该算法生成的是64位唯一Id(由41位的timestamp+ 10位自定义的机器码+ 13位累加计数器组成)。这里不做过多介绍,感兴趣的读者可自行查阅相关资料。


常见分片规则和策略


分片字段该如何选择

在开始分片之前,我们首先要确定分片字段(也可称为“片键”)。很多常见的例子和场景中是采用ID或者时间字段进行拆分。这也并不绝对的,我的建议是结合实际业务,通过对系统中执行的sql语句进行统计分析,选择出需要分片的那个表中最频繁被使用,或者最重要的字段来作为分片字段。

常见分片规则

常见的分片策略有随机分片和连续分片这两种,如下图所示:

当需要使用分片字段进行范围查找时,连续分片可以快速定位分片进行高效查询,大多数情况下可以有效避免跨分片查询的问题。后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移。但是,连续分片也有可能存在数据热点的问题,就像图中按时间字段分片的例子,有些节点可能会被频繁查询压力较大,热数据节点就成为了整个集群的瓶颈。而有些节点可能存的是历史数据,很少需要被查询到。

随机分片其实并不是随机的,也遵循一定规则。通常,我们会采用Hash取模的方式进行分片拆分,所以有些时候也被称为离散分片。随机分片的数据相对比较均匀,不容易出现热点和并发访问的瓶颈。但是,后期分片集群扩容起来需要迁移旧的数据。使用一致性Hash算法能够很大程度的避免这个问题,所以很多中间件的分片集群都会采用一致性Hash算法。离散分片也很容易面临跨分片查询的复杂问题。

数据迁移,容量规划,扩容等问题

很少有项目会在初期就开始考虑分片设计的,一般都是在业务高速发展面临性能和存储的瓶颈时才会提前准备。因此,不可避免的就需要考虑历史数据迁移的问题。一般做法就是通过程序先读出历史数据,然后按照指定的分片规则再将数据写入到各个分片节点中。

此外,我们需要根据当前的数据量和QPS等进行容量规划,综合成本因素,推算出大概需要多少分片(一般建议单个分片上的单表数据量不要超过1000W)。

如果是采用随机分片,则需要考虑后期的扩容问题,相对会比较麻烦。如果是采用的范围分片,只需要添加节点就可以自动扩容。

跨分片技术问题

跨分片的排序分页

一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。如下图所示:

上面图中所描述的只是最简单的一种情况(取第一页数据),看起来对性能的影响并不大。但是,如果想取出第10页数据,情况又将变得复杂很多,如下图所示:

有些读者可能并不太理解,为什么不能像获取第一页数据那样简单处理(排序取出前10条再合并、排序)。其实并不难理解,因为各分片节点中的数据可能是随机的,为了排序的准确性,必须把所有分片节点的前N页数据都排序好后做合并,最后再进行整体的排序。很显然,这样的操作是比较消耗资源的,用户越往后翻页,系统性能将会越差。

跨分片的函数处理

在使用Max、Min、Sum、Count之类的函数进行统计和计算的时候,需要先在每个分片数据源上执行相应的函数处理,然后再将各个结果集进行二次处理,最终再将处理结果返回。如下图所示:

跨分片join

Join是关系型数据库中最常用的特性,但是在分片集群中,join也变得非常复杂。应该尽量避免跨分片的join查询(这种场景,比上面的跨分片分页更加复杂,而且对性能的影响很大)。通常有以下几种方式来避免:

全局表

全局表的概念之前在“垂直分库”时提过。基本思想一致,就是把一些类似数据字典又可能会产生join查询的表信息放到各分片中,从而避免跨分片的join。

ER分片

在关系型数据库中,表之间往往存在一些关联的关系。如果我们可以先确定好关联关系,并将那些存在关联关系的表记录存放在同一个分片上,那么就能很好的避免跨分片join问题。在一对多关系的情况下,我们通常会选择按照数据较多的那一方进行拆分。如下图所示:

这样一来,Data Node1上面的订单表与订单详细表就可以直接关联,进行局部的join查询了,Data Node2上也一样。基于ER分片的这种方式,能够有效避免大多数业务场景中的跨分片join问题。

内存计算

随着spark内存计算的兴起,理论上来讲,很多跨数据源的操作问题看起来似乎都能够得到解决。可以将数据丢给spark集群进行内存计算,最后将计算结果返回。

跨分片事务问题

跨分片事务也分布式事务,想要了解分布式事务,就需要了解“XA接口”和“两阶段提交”。值得提到的是,MySQL5.5x和5.6x中的xa支持是存在问题的,会导致主从数据不一致。直到5.7x版本中才得到修复。Java应用程序可以采用Atomikos框架来实现XA事务(J2EE中JTA)。感兴趣的读者可以自行参考《分布式事务一致性解决方案》,链接地址:

http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency

我们的系统真的需要分库分表吗

读完上面内容,不禁引起有些读者的思考,我们的系统是否需要分库分表吗?

其实这点没有明确的判断标准,比较依赖实际业务情况和经验判断。依照笔者个人的经验,一般MySQL单表1000W左右的数据是没有问题的(前提是应用系统和数据库等层面设计和优化的比较好)。当然,除了考虑当前的数据量和性能情况时,作为架构师,我们需要提前考虑系统半年到一年左右的业务增长情况,对数据库服务器的QPS、连接数、容量等做合理评估和规划,并提前做好相应的准备工作。如果单机无法满足,且很难再从其他方面优化,那么说明是需要考虑分片的。这种情况可以先去掉数据库中自增ID,为分片和后面的数据迁移工作提前做准备。

很多人觉得“分库分表”是宜早不宜迟,应该尽早进行,因为担心越往后公司业务发展越快、系统越来越复杂、系统重构和扩展越困难…这种话听起来是有那么一点道理,但我的观点恰好相反,对于关系型数据库来讲,我认为“能不分片就别分片”,除非是系统真正需要,因为数据库分片并非低成本或者免费的。

这里笔者推荐一个比较靠谱的过渡技术–“表分区”。主流的关系型数据库中基本都支持。不同的分区在逻辑上仍是一张表,但是物理上却是分开的,能在一定程度上提高查询性能,而且对应用程序透明,无需修改任何代码。笔者曾经负责优化过一个系统,主业务表有大约8000W左右的数据,考虑到成本问题,当时就是采用“表分区”来做的,效果比较明显,且系统运行的很稳定。

小结

最后,有很多读者都想了解当前社区中有没有开源免费的分库分表解决方案,毕竟站在巨人的肩膀上能省力很多。当前主要有两类解决方案:

  1. 基于应用程序层面的DDAL(分布式数据库访问层) 

    比较典型的就是淘宝半开源的TDDL,当当网开源的Sharding-JDBC等。分布式数据访问层无需硬件投入,技术能力较强的大公司通常会选择自研或参照开源框架进行二次开发和定制。对应用程序的侵入性一般较大,会增加技术成本和复杂度。通常仅支持特定编程语言平台(Java平台的居多),或者仅支持特定的数据库和特定数据访问框架技术(一般支持MySQL数据库,JDBC、MyBatis、Hibernate等框架技术)。

  2. 数据库中间件,比较典型的像mycat(在阿里开源的cobar基础上做了很多优化和改进,属于后起之秀,也支持很多新特性),基于Go语言实现kingSharding,比较老牌的Atlas(由360开源)等。这些中间件在互联网企业中大量被使用。另外,MySQL 5.x企业版中官方提供的Fabric组件也号称支持分片技术,不过国内使用的企业较少。 

    中间件也可以称为“透明网关”,大名鼎鼎的mysql_proxy大概是该领域的鼻祖(由MySQL官方提供,仅限于实现“读写分离”)。中间件一般实现了特定数据库的网络通信协议,模拟一个真实的数据库服务,屏蔽了后端真实的Server,应用程序通常直接连接中间件即可。而在执行SQL操作时,中间件会按照预先定义分片规则,对SQL语句进行解析、路由,并对结果集做二次计算再最终返回。引入数据库中间件的技术成本更低,对应用程序来讲侵入性几乎没有,可以满足大部分的业务。增加了额外的硬件投入和运维成本,同时,中间件自身也存在性能瓶颈和单点故障问题,需要能够保证中间件自身的高可用、可扩展。

总之,不管是使用分布式数据访问层还是数据库中间件,都会带来一定的成本和复杂度,也会有一定的性能影响。所以,还需读者根据实际情况和业务发展需要慎重考虑和选择。

作者介绍

丁浪,技术架构师。关注高并发、高可用的架构设计,对系统服务化、分库分表、性能调优等方面有深入研究和丰富实践经验。热衷于技术研究和分享。

### ShardingSphere 分库分表原理及面试题解析 #### 分库分表原理概述 ShardingSphere 是一款开源的分布式数据库中间件,支持分库分表、读写分离、数据加密等功能。其核心原理是通过解析 SQL 语句,根据配置的分片策略将数据分布到不同的数据库或数据表中。ShardingSphere 提供了两种主要的分片方式:水平拆分垂直拆分。 - **水平拆分**:将一张表的数据按照一定规则分散到多个物理表中,适用于数据量大但结构相同的场景。 - **垂直拆分**:将不同业务模块的数据拆分到不同的数据库或表中,适用于数据结构差异较大的场景[^3]。 #### 分片键与路由算法 分片键(Sharding Key)是决定数据分布的关键字段,选择合适的分片键可以有效避免数据倾斜。常见的路由算法包括: - **取模**:将分片键的值对分片数量取模,确定数据所在的分片。 - **范围分片**:根据分片键的范围划分数据,例如按时间或ID区间划分。 - **哈希分片**:通过哈希算法将分片键映射到特定的分片中。 在实际应用中,分片键的选择应遵循以下原则: - 尽量选择查询频率高的字段作为分片键。 - 避免选择唯一性过强的字段,以防止数据分布不均。 - 考虑业务逻辑,确保分片键能够支持常见的查询模式。 #### 分库分表中间件选型 在选择分库分表中间件时,ShardingSphere 是一个较为流行的选择,因为它提供了丰富的功能良好的扩展性。其他常见的分库分表中间件还包括 MyCat TDDL。ShardingSphere 支持多种分片策略,并且可以与 Spring Boot 等框架无缝集成,简化了开发流程[^2]。 #### 经典面试题解析 1. **如何避免分库分表后的数据倾斜?** 数据倾斜是指某些分片的数据量远大于其他分片,导致性能瓶颈。为了解决这个问题,可以采取以下措施: - 选择合适的分片键,避免唯一性过强的字段。 - 使用一致性哈希算法,确保数据分布均匀。 - 对热点数据进行单独处理,例如使用缓存或异步写入。 2. **分页查询如何优化?** 分页查询在分库分表环境中较为复杂,尤其是跨分片的分页查询。常见的优化方法包括: - **子查询优化**:先查询出主键,再根据主键回表查询详细信息。 - **全局排序优化**:对于需要全局排序的分页查询,可以考虑使用中间表或缓存。 - **限制分页深度**:避免查询过深的分页,减少性能开销。 3. **分布式ID生成方案对比** 在分库分表环境中,生成全局唯一的ID是一个重要问题。常见的分布式ID生成方案包括: - **Snowflake**:基于时间戳机器ID生成唯一ID,适用于高并发场景。 - **UUID**:生成128位的随机字符串,虽然唯一性高,但存储空间较大。 - **数据库自增ID**:通过数据库的自增特性生成ID,但不适用于分库分表环境。 - **Redis自增**:利用Redis的原子操作生成自增ID,性能较好,但依赖Redis服务。 4. **跨分片JOIN如何解决?** 跨分片JOIN 是分库分表中的一个难点,通常可以通过以下方式解决: - **应用层拼接**:在应用层分别查询多个分片的数据,然后进行拼接。 - **广播表**:将小表复制到每个分片中,减少跨分片查询的次数。 - **ETL处理**:定期将数据同步到统一的查询库中,避免实时JOIN操作。 #### 实战问题解决方案 1. **扩容如何平滑进行?** 扩容是分库分表环境中常见的需求,可以通过以下步骤实现平滑扩容: - **预估数据量**:根据业务增长情况预估未来的数据量,提前规划扩容策略。 - **分片再平衡**:通过重新分配数据,确保新增的分片能够均匀承载数据。 - **在线迁移**:使用工具进行在线数据迁移,尽量减少对业务的影响。 2. **分布式事务实现** 在分库分表环境中,分布式事务的实现通常依赖于两阶段提交(2PC)或柔性事务(如TCC)。ShardingSphere 提供了对XA协议的支持,可以实现跨分片的事务一致性。 3. **热点问题处理** 热点问题是指某些分片或数据频繁被访问,导致性能瓶颈。常见的解决方案包括: - **缓存热点数据**:使用Redis等缓存技术减少对数据库的直接访问。 - **读写分离**:将读操作写操作分离,减轻主库的压力。 - **异步处理**:对于非实时性要求较高的操作,采用异步处理方式。 #### 示例代码 以下是一个使用 ShardingSphere 进行分表配置的示例: ```yaml spring: shardingsphere: sharding: tables: t_order: actual-data-nodes: ds${0..1}.t_order${0..1} table-strategy: inline: sharding-column: id sharding-algorithm-name: t_order-table-inline key-generator: column: id type: SNOWFLAKE sharding-algorithms: t_order-table-inline: type: INLINE props: algorithm-expression: t_order${id % 2} ``` 上述配置表示将 `t_order` 表按照 `id` 字段进行分表,分片数量为2,数据将分布在 `t_order0` `t_order1` 中。 ####
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁码农

请作者喝杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值