尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

分库分表的设计及实践

分库分表的设计及实践 概述虽然现代数据库理论上可以支持非常大的数据量(TB级、PB级)但在实际应用中达到这些理论极限之前往往就会遇到性能瓶颈、备份与恢复时间过长等问题。这也是为什么在数据量达到一定规模时很多系统会采取分库分表等策略来优化存储和性能的原因。分库分表策略具有以下优点提高性能随着数据量的增加单一数据库的查询、写入速度会逐渐下降。通过分库分表可以将数据分散到多个数据库或表中减少单个数据库的负担从而提高系统的整体性能。提升系统扩展性随着业务的发展对数据库的读写需求可能会急剧增长。分库分表使得可以通过增加更多的数据库服务器来水平扩展系统的处理能力更容易应对高并发场景。优化备份恢复效率大数据量的备份和恢复是一个耗时且可能影响服务的过程。分库分表后每个库或表的数据量减小可以更快地完成备份和恢复操作降低数据丢失的风险。增强容灾能力分布式架构下即使部分数据库发生故障其他数据库仍能继续提供服务保证了系统的高可用性。这对于很多需要7x24小时不间断服务的业务至关重要。尽管分库分表能够有效解决大数据量和高并发带来的问题但它也会带来系统复杂度上升的问题引入了一些新的挑战和潜在问题主要包括数据分布不均如果分片策略设计不当可能导致数据在不同库或表之间的分布不均匀(数据倾斜)某些库或表负载过高而其他则资源闲置影响整体性能。跨分片查询复杂性涉及到多个分片的数据关联查询、分页、排序、聚合处理变得复杂。例如需要同时从多个分表中获取数据时可能需要应用层进行额外的逻辑处理或者使用分布式事务这增加了开发和维护的难度。事务一致性问题在分布式系统中保持事务一致性是一个挑战尤其是在涉及跨分片的事务操作时。需要采用分布式事务、最终一致性的策略或使用分布式锁等机制来确保事务的一致性。全局唯一ID生成问题在分表环境中全局唯一ID的生成变得复杂需要确保不同分表间ID的唯一性常用解决方案包括使用分布式ID等。开发和调试难度提升由于数据分布在多个地方开发者需要处理更多的逻辑来应对分布式环境下的问题这可能使得代码更加复杂调试和测试也更为困难。运维复杂度增加相比于单一数据库分库分表的架构需要更复杂的部署、监控、备份和恢复策略以及更精细的资源管理和故障转移机制。因此在决定是否采用分库分表策略时需要权衡业务需求、数据规模、性能要求及团队的技术能力合理规划以避免或减轻这些问题。分库分表分析分库分表能有效的缓解单机和单库带来的性能瓶颈和高并发压力突破网络IO、硬件资源、连接数的瓶颈同时也带来了数据分布不均、跨节点的关联查询、跨节点分页/排序/聚合函数(如Max、Min、Sum、Count)、事务一致性、全局唯一ID生成等问题。分库分表涉及到水平拆分和垂直拆分这里主要介绍的是水平拆分。对于垂直拆分主要是按照关注点分离的思想将一个大库拆分成多个小库、一张大表拆分成多个小表。如何解决数据分布不均问题为解决分片后数据不均的问题需要合理的设计分片策略(路由规则)、选择合适的分片键(拆分键、分表键、分表字段)。这里给出常见的分片策略、分片键选取原则。分片策略常见的分片策略有以下几种哈希分片○ 原理通过计算分片键的哈希值并根据哈希值的范围或取模运算结果来决定数据存放在哪个分片上。这种方法可以非常均匀地分布数据适用于不需要保持数据顺序的场景。○ 优点数据分布均匀扩展性好容易实现。○ 缺点不适合范围查询且分片键的选择对性能影响大。范围分片○ 原理根据分片键的值范围来决定数据的存储位置。如按时间戳将数据分配到不同的表或库中。○ 优点支持范围查询和排序操作直观易理解。○ 缺点数据分布可能不均匀扩展时可能需要重新分配数据。列表分片也称为指定位分片○ 原理预先定义一系列的分片键值每个值对应一个分片。数据根据分片键值直接映射到对应的分片。○ 优点简单直观适用于分片键取值范围有限且已知的场景。○ 缺点扩展性和灵活性较差分片键值的增减可能需要重新调整分片。一致性哈希○ 原理一种特殊的哈希算法可以解决普通哈希分片在节点增删时重分布数据的问题。数据通过哈希环映射到不同的节点增加或减少节点只影响相邻节点的数据。○ 优点在节点变化时能最小化数据迁移适用于动态扩展的场景。○ 缺点实现相对复杂且在极端情况下仍可能存在数据分布不均。选择合适的分片策略需要根据业务的具体需求、查询模式、数据增长预期以及系统的扩展目标来决定。在实际应用中可能还会结合中间件等技术来进一步优化分片管理、查询路由和数据一致性等问题。分片键选取原则合理选取分片键可以有效提升数据访问效率、平衡负载以及便于数据管理和维护。相反如果不合理的选取分片键则会导致数据分布不均、数据查询效率低下、数据迁移成本增加等问题。以下是选择分片键的一些基本原则数据分布均匀理想的分片键其值应当在整个数据集内均匀分布避免数据倾斜现象。数据倾斜会导致某些分片承受过大的访问压力影响整体性能。因此需要评估候选键的值域和分布情况。业务相关性考虑系统中常见的查询场景确保所选分片键能够支持高效的查询执行。例如如果应用中大量使用基于时间范围的查询按时间字段分片将是不错的选择。避免热点问题分析业务流量和数据访问模式确保所选分片键不会导致某个分片成为访问热点比如使用用户ID作为分片键时需考虑超级用户的访问压力。不经常变更为了避免频繁的数据迁移和重分布分片键应尽量选择不经常更新的字段。如果分片键频繁变动每次更新都可能触发数据在分片间的迁移增加系统复杂度和维护成本。为减少不必要的数据迁移和重分布分片键的最佳设计是一旦创建就不支持变更。扩展性考虑考虑到未来数据量的增长和业务的变化分片键的选择应具有一定的前瞻性和扩展性。例如使用一致性哈希或能够支持动态添加分片的策略。单一职责原则尽量避免使用复合键作为分片键除非确有必要。复合键虽然可以更细粒度地控制数据分布但会增加查询逻辑的复杂度和维护难度。综上所述分片键的选择是一个权衡的过程需要综合考虑业务需求、数据特性、系统架构及未来发展等多种因素以达到既提高性能又能灵活扩展的目标。如何解决跨节点的关联查询的问题全局表全局表也可看做是数据字典表就是系统中所有模块都可能依赖的一些表为了避免跨库join查询可以将这类表在每个数据库中都保存一份。这些数据通常很少会进行修改所以也不担心一致性的问题。这样在join查询时只是单库的join查询不会出现跨库的join查询。字段冗余一种典型的反范式设计利用空间换时间为了性能而避免join查询。例如订单表保存userId时候也将userName冗余保存一份这样查询订单详情时就不需要再去查询买家user表了。但这种方法适用场景也有限比较适用于依赖字段比较少的情况。而冗余字段的数据一致性也较难保证就像上面订单表的例子买家修改了userName后是否需要在历史订单中同步更新呢这也要结合实际业务场景进行考虑。数据组装在系统层面分两次查询第一次查询的结果集中找出关联数据id然后根据id发起第二次请求得到关联数据。最后将获得到的数据进行字段拼装。这种方式的使用较多本质是在客户端组装数据。ER分片关系型数据库中如果可以先确定表之间的关联关系并将那些存在关联关系的表记录存放在同一个分片上那么就能较好的避免跨分片join问题。这种方式是最优的设计如果可以尽量达到这个设计目标。如何解决跨节点分页、排序、聚合函数问题跨节点多库进行查询时会出现limit分页、order by排序、聚合函数等问题。分页需要按照指定字段进行排序当排序字段就是分片字段时通过分片规则就比较容易定位到指定的分片当排序字段非分片字段时就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回然后将不同分片返回的结果集进行汇总和再次排序最终返回给用户。如图所示________________________ ________________________ | 订单表 1 | | 订单表 2 | | id1 ~ id100 | | id101 ~ id200 | |________________________| |________________________| │ │ │ │ select ... limit 0,10 select ... limit 0,10 │ │ ▼ ▼ __________________________________________________________ | | | 合并数据 再执行 select ... limit 0,10 | |__________________________________________________________|上图中只是取第一页的数据对性能影响还不是很大。但是如果取得页数很大情况则变得复杂很多因为各分片节点中的数据可能是随机的为了排序的准确性需要将所有节点的前N页数据都排序好做合并最后再进行整体的排序这样的操作时很耗费CPU和内存资源的所以页数越大系统的性能也会越差。在使用Max、Min、Sum、Count之类的聚合函数进行计算的时候也需要先在每个分片上执行相应的函数然后将各个分片的结果集进行汇总和再次计算最终将结果返回。针对跨节点分页、排序、聚合函数等问题一种友好的方式是使用中间层或代理层收集和处理各节点的数据实现最终的分页、排序、计算。如何解决事务一致性问题分布式事务(强一致性)当更新内容同时分布在不同库中不可避免会带来跨库事务问题。跨分片事务也是分布式事务没有简单的方案一般可使用XA协议和两阶段提交处理。分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点推后了提交事务的时间点延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多这种趋势会越来越严重从而成为系统在数据库层面上水平扩展的枷锁与事务在执行中发生错误后立即回滚的方式不同事务补偿是一种事后检查补救的措施一些常见的实现方法有对数据进行对账检查基于日志进行对比定期同标准数据来源进行同步等等。事务补偿还要结合业务系统来考虑。最终一致性对于那些性能要求很高但对一致性要求不高的系统往往不苛求系统的实时一致性只要在允许的时间段内达到最终一致性即可可采用事务补偿的方式。如何解决全局唯一ID生成问题在分库分表环境中由于表中数据同时存在不同数据库中主键值平时使用的自增长将无用武之地某个分区数据库自生成的ID无法保证全局唯一。因此需要单独设计全局主键以避免跨库主键重复问题。常见的主键生成策略有分片ID数据库自增ID、数据库SEQUENCE、UUID、Snowflake分布式自增ID算法、数据库号段(Segment)算法、基于ZooKeeper生成的分布式自增ID、基于Redis生成的分布式自增ID、分布式ID生成服务等。分片ID数据库自增ID分片ID 自增ID是一种结合了分片思想和数据库自增ID的分布式ID生成策略其基本思路是复用数据库的分库分表能力(分片)每个分片拥有独立的数据库或数据库表每个分片内的ID采用自增方式生成。数据库SEQUENCE除了使用自增ID还有一种实现是使用SEQUENCE这里以Oracle Sequence为例简单介绍下其实现原理。下图是一个示例┌──────────┐ ┌──────────┐ ┌──────────┐ │ APP 1 │ │ APP 3 │ │ APP 2 │ │ │ │ │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ seq1.nextval │ seq1.nextval │ seq1.nextval │ 1 │ 3 │ 2 │ └──────────┬────────┴───────────────────┘ │ ┌─────────┴─────────┐ │ DB │ create sequence seq1 │ ┌─────────────┐ │ minvalue 1 │ │ seq1: 1-1000│ │ maxvalue 99999999 │ │ (cache) │ │ increment by 1 │ └─────────────┘ │ cache 1000 └───────────────────┘ cycle可以看出 Sequence 的一些关键参数minvalueSequence 最小值示例中为1。maxvalueSequence 最大值示例中为99999999。cache数据库在内存中缓存的 ID 范围用于加速序列号生成示例中为1000。cycle/nocycle当生成的 Sequence 值达到 maxvalue 时是否从 minvalue 重新开始。UUIDUUIDUniversally Unique Identifier由128位组成为32个十六进制数字分成五组每组由短横线分隔8-4-4-4-12格式形如xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx)。其中每个UUID都包含一个版本号占128位中的4位指明了生成该UUID所使用的规则。另外还有2位是变体号表明UUID遵循的布局和格式规则确保了不同变体的UUID可以被正确解析。到目前为止业界一共有5种方式生成 UUID详情参见 IETF 发布的UUID规范A Universally Unique IDentifier (UUID) URN Namespace。Snowflake分布式自增ID算法Snowflake算法是Twitter提出的分布式ID生成方案。这里简单介绍下Snowflake算法。可以将Snowflake算法看成是一种以划分命名空间UUID也算来生成ID的一种算法这种方案把64-bit分别划分成多段分开来标示机器、时间等比如在Snowflake中的64-bit分别表示如下图图片来自网络所示snowflake-64-bit: 0 - 00000000000000000000000000000000000000000 - 0000000000 - 000000000000 │ └──────────────────┬──────────────────┘ └─────┬────┘ └─────┬──────┘ │ │ │ │ 1-bit 41-bit 10-bit 12-bit 不用 时间戳 workerID 序列号 (符号位) (毫秒级时间) (机器ID) (计数序列)1-bit的符号为默认是0只使用正数。41-bit的时间戳可以表示1L41/(1000L360024*365)69年的时间。10-bit的WorkID可以分别表示1024台机器。如果我们对IDC(Internet Data Center)划分有需求还可以将10-bit分5-bit给IDC分5-bit给工作机器。这样就可以表示32个IDC每个IDC下可以有32台机器可以根据自身需求定义。12个自增序列号可以表示2^12个ID理论上snowflake方案的QPS约为409.6w/s这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。数据库号段(Segment)算法数据库号段算法是基于数据库的表来模拟实现Sequence一般再组装日期时间与原始的Sequence值来实现分布式ID。某个程度上相当于Oracle Sequence 和Snowflake算法两种方式的结合。基于Zookeeper的分布式自增ID可以使用ZooKeeper的临时节点实现分布式锁配合一个计数器节点来生成分布式自增ID。由于ZooKeeper严重依赖 ZooKeeper 集群并且性能不是很高所以不建议使用。基于Redis的分布式自增ID使用Redis生成分布式自增ID的方法通常是通过Redis的原子操作来实现。一种常见的方法是使用Redis的INCR命令该命令可以将键的值原子地增加1。由于依赖Redis由于增加了中间件提升了系统的复杂度所以不建议使用。分布式ID生成服务随着业务复杂性的提高业务服务对分布式ID的定制化需求要来越高且不同业务服务对分布式ID的定制需求具有相似性如要支持海量数据、支持高性能、支持基于某个业务维度划分分布式ID(不需要全局唯一局部唯一即可)等等。简言之对于分布式ID已经不局限于一个全局唯一的字符更包含了分布式ID在业务上的多种应用场景(分布式ID业务场景)。为此需要一个通用的分布式ID生成服务来服务业务帮助业务服务更好的聚焦到业务层面。分布式ID生成服务只有在业务复杂性达到一定程度时才应考虑使用。如果业务对分布式ID的要求很简单直接使用上述其他方案实现即可。如何解决数据迁移、扩容问题当业务高速发展面临性能和存储的瓶颈时才会考虑分片设计此时就不可避免的需要考虑历史数据迁移的问题。一般做法是先读出历史数据然后按指定的分片规则再将数据写入到各个分片节点中。此外还需要根据当前的数据量和QPS以及业务发展的速度进行容量规划推算出大概需要多少分片一般建议单个分片上的单表数据量不超过1000W。如果采用数值范围分片只需要添加节点就可以进行扩容了不需要对分片数据迁移。如果采用的是数值取模分片则考虑后期的扩容问题就相对比较麻烦。分库分表规则设计在设计分库分表规则时需要根据当前业务的数据量或预期的数据量设置分库分表的规模如十库十表、百库百表、千库千表。单表的数据量最好控制在数百万条记录到几千万条记录范围内以保证性能。如阿里巴巴开发手册建议单表最大不超过500万条记录、业内建议单表最大不超过2000万条记录。除了评估分库分表的规模设计人员还要考虑如何设计分库分表规则具体包括如何定义分库分表的分片策略、库表的映射关系、分片键的选择等。分库分表的规模评估在考虑分库分表时数据规模的评估和规划非常重要可以从以下几个方面进行初始数据量:了解当前的数据量以便于进行初步的分片设计。数据增长速度:● 预测数据的增长趋势规划分库分表的扩展能力。业务需求变化:● 考虑未来可能的业务扩展和新功能需求。单库单表容量限制:● 考虑数据库的容量限制避免单表或单库存储过多数据影响性能。这里采用阿里巴巴手册的建议“单表行数超过500万行或者单表容量超过2GB才推荐进行分库分表。”。通过全面分析和规划确保分库分表设计不仅满足当前的数据规模需求还能应对未来的增长和变化。分库分表规则设计分库分表规则适配业务水平拆分根据一定的业务规则将数据拆分到各个分库和分表中。一般的分库分表规则都是根据表中的某个字段进行拆分的这个字段也叫做拆分键、分表键或分表字段。如无特殊要求拆分键都是唯一的。业务上普遍使用分布式ID作为拆分键并让其保证自增以保证数据库插入的顺序性。现有业务推荐基于Oracle的Sequence实现一个类Snowflake算法(同一个业务域可以使用自定义的Sequence部件)。注意分布式ID一经创建不允许修改否则会带来不必要的数据迁移工作。分库分表规则设计示例(十库百表)如下分库规则(#ID# % 100) / 10 分表规则(#ID#) % 100 [ 逻辑表 ] [ 物理库表 ] ┌──────┬────────┐ ┌─────────────────────────┐ │ ID │ Name │ │ DB_00 │ ├──────┼────────┤ 分库 00 │ ┌────────────────────┐ │ │ 0 │ Jack ├─────────分表 00──────────────►│ │ TABLE_00 │ │ ├──────┼────────┤ │ ├────────┬──────────┤ │ │ ... │ ... │ │ │ ID │ Name │ │ ├──────┼────────┤ 分库 00 │ ├────────┼──────────┤ │ │ 9 │ Lucy ├─────────分表 09─────────┐ │ │ 0 │ Jack │ │ ├──────┼────────┤ │ │ └────────┴──────────┘ │ │ ... │ ... │ │ │ ... │ ├──────┼────────┤ 分库 09 └────►│ ┌────────────────────┐ │ │ 90 │ Tom ├─────────分表 90─────────┐ │ │ TABLE_09 │ │ ├──────┼────────┤ │ │ ├────────┬──────────┤ │ │ ... │ ... │ │ │ │ ID │ Name │ │ ├──────┼────────┤ 分库 09 │ │ ├────────┼──────────┤ │ │ 99 │ Rose ├─────────分表 99─────┐ │ │ │ 9 │ Lucy │ │ └──────┴────────┘ │ │ │ └────────┴──────────┘ │ │ │ └───────────────────────┘ │ │ ... │ │ ┌───────────────────────┐ │ │ │ DB_09 │ │ │ │ ┌───────────────────┐ │ │ └────►│ │ TABLE_90 │ │ │ │ ├────────┬──────────┤ │ │ │ │ ID │ Name │ │ │ │ ├────────┼──────────┤ │ │ │ │ 90 │ Tom │ │ │ │ └────────┴──────────┘ │ │ │ ... │ │ │ ┌───────────────────┐ │ └────────►│ │ TABLE_99 │ │ │ ├────────┬──────────┤ │ │ │ ID │ Name │ │ │ ├────────┼──────────┤ │ │ │ 99 │ Rose │ │ │ └────────┴──────────┘ │ └───────────────────────┘分库规则和分表规则使用相同的拆分键。如果库表是一对一的关系那么分库规则和分表规则是完全一致的。只允许一个数据库中包含多个分表不允许一个分表放置在多个数据库。在设计分库分表规则时一定要考虑可能存在的数据分布不均问题。通常情况下基于分布式ID取模计算分库/分表可以满足大部分业务需求。少数场景需要特别设计分片策略。业务主要操作是基于某一个或某几个业务字段这个时候就可以考虑基于这一个或这几个业务字段使用哈希分片策略。在设计分库分表规则时要充分基于业务考虑业务操作要尽量围绕拆分键来处理对于不能使用拆分键的业务场景要充分评估数据拆分及合并对性能的影响。尽量使用单库的事务能力来保证业务操作的原子性而不是借助分布式事务。如对关联表同样冗余分布式ID并采用相同的分库分表规则则在执行关联操作时关联的数据只会存在同一个数据库中。对于需要执行排序、查找、聚合函数的操作一种对业务服务友好的方式是使用中间层或代理层收集和处理各节点的数据实现最终的分页、排序、计算。数据库扩/缩容方案设计当业务高速发展面临性能和存储的瓶颈时才会考虑扩容设计此时就不可避免的需要考虑历史数据迁移的问题。如需要从单库单表扩容成十库十表或者从十库十表扩容成百库百表等等。一般做法是先读出历史数据然后按指定的分片规则再将数据写入到各个分片节点中。此外还需要根据当前的数据量和QPS以及业务发展的速度进行容量规划推算出大概需要多少分片一般建议单个分片上的单表数据量不超过1000W从而确保分库分表设计不仅满足当前的数据规模需求还能应对未来的增长和变化。在设计数据库扩容方案时要保证现有业务可以平滑的完成扩容。具体来说要确保业务可灰度切流到新库、并在切换出现故障的时候回滚到旧库。此外随着业务的萎缩也会出现缩容的场景。但是基本上不会单独创建缩容计划而是直接跟随业务服务下线回收相关数据库资源。参考https://segmentfault.com/a/1190000044138327 分库分表之拆分键设计https://developer.aliyun.com/special/tech-java Java开发手册https://my.oschina.net/u/4090830/blog/5559454 mysql 最大建议行数 2000w, 靠谱吗
返回列表