你好,欢迎进入江苏优软数字科技有限公司官网!

诚信、勤奋、创新、卓越

友好定价、专业客服支持、正版软件一站式服务提供

13262879759

工作日:9:00-22:00

美团面试官:说说你对数据库分库分表的理解?

发布时间:2024-02-10

浏览次数:0

#数据分割

关系型数据库本身更容易成为系统瓶颈,因为单机的存储容量、连接数、处理能力都是有限的。 当单表数据量达到1000W、100G时,由于查询维度较多,即使增加从库、优化索引,执行较多操作时性能仍然会严重下降。 这时候就需要考虑对其进行细分。 分段的目的是减轻数据库的负担,缩短查询时间。

数据库分布的核心内容无非就是数据切分(),以及切分后数据的定位和整合。 数据分割就是将数据分散存储到多个数据库中,从而减少单个数据库的数据量。 通过扩展主机数量,缓解单个数据库的性能问题,从而达到提高数据库运行性能的目的。

数据分割根据其分割类型可以分为两种方式:垂直()分割和水平(​​ntal)分割。

1.垂直()分割

垂直分段有两种常见类型:垂直数据库分区和垂直表分区。

垂直分库是基于业务耦合,将相关性较低的不同表存储在不同的数据库中。 做法类似于将一个大系统拆分成多个小系统,按照业务分类独立划分。 与“微服务治理”的方法类似,每个微服务都使用单独的数据库。 如图所示:

intellij idea 数据库关系图_关系库中的数据表的关系_关系库数据系统有哪些

垂直表分区基于数据库中的“列”。 如果一个表的字段较多,可以新建一个扩展表,将不常用或字段长度较大的字段拆分到扩展表中。 当字段较多时(例如一张大表有100多个字段),“将大表拆分成小表”更容易开发和维护,也可以避免跨页问题。 MySQL底层是通过数据页来存储的。 占用过多空间的录制会导致页面交叉,造成额外的性能开销。

另外,数据库以行为单位将数据加载到内存中,使得表中的字段长度更短,访问频率更高。 内存可以加载更多的数据,命中率更高,同时减少磁盘IO,从而提高数据库性能。

关系库数据系统有哪些_关系库中的数据表的关系_intellij idea 数据库关系图

垂直分割的优点:

缺点:

2.水平(​​ntal)分割

当应用程序很难进行更细粒度的垂直切分,或者切分后的数据行数巨大,且单个数据库的读、写、存储存在性能瓶颈时,就需要水平切分。

水平切分分为库内分片和分库分片。 基于表中数据固有的逻辑关系,将同一张表根据不同的情况分散到多个数据库或者多个表中。 每个表只包含部分数据,从而减少单表数据量,达到分布式效果。 如图所示:

关系库中的数据表的关系_关系库数据系统有哪些_intellij idea 数据库关系图

数据库内部分表只是解决单表数据量过大的问题,并没有将表分布到不同机器的数据库中。 因此,对于减轻MySQL数据库的压力并没有太大帮助。 大家仍然在争夺同一台物理机器。 CPU、内存、网络IO最好通过分库分表的方式解决。

水平切片的优点:

缺点:

水平拆分后,同一张表会出现在多个库/表中,并且每个库/表的内容不同。 几种典型的数据分片规则是:

1.按数值范围

根据时间间隔或ID间隔进行分割。 例如:将不同月份甚至几天的数据按日期分散到不同的库中; 将 1 到 9999 的记录分配给第一个库,将 10000 到 20000 的记录分配给第二个库,依此类推。 从某种意义上说,一些系统中采用的“冷热数据分离”,将一些较少使用的历史数据迁移到其他库,只在业务功能中提供热数据查询,也是类似的做法。

这样做的优点是:

缺点:

关系库中的数据表的关系_intellij idea 数据库关系图_关系库数据系统有哪些

2.根据数值取模

一般采用hash mod的分裂方式。 例如,根据cusno字段将表拆分为4个库,余数为0的放入第一个库,余数为1的放入第二个库。 比喻。 这样,同一用户的数据就会分散到同一个数据库中。 如果查询条件包含cusno字段,则可以明确定位对应的数据库进行查询。

优势:

缺点:

intellij idea 数据库关系图_关系库数据系统有哪些_关系库中的数据表的关系

# 分库分表引起的问题

分库分表可以有效消除单机、单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数瓶颈。 这也带来了一些问题。 下面介绍这些技术挑战和相应的解决方案。

1. 交易一致性问题

分布式交易

当更新的内容同时分布在不同的库中时,不可避免地会出现跨库事务问题。 跨分片交易也是分布式交易,没有简单的解决方案。 一般可以使用“XA协议”和“两阶段提交”来处理。

分布式事务可以最大程度地保证数据库操作的原子性。 但提交交易时,需要多个节点进行协调,这就延迟了提交交易的时间点,延长了交易的执行时间。 这会导致事务访问共享资源时发生冲突或死锁的可能性增加。 随着数据库节点数量的增加,这种趋势会越来越严重,从而成为系统在数据库层面横向扩展的桎梏。

最终一致性

对于那些性能要求高但一致性要求不高的系统,往往不要求系统的实时一致性。 只要在允许的时间内达到最终一致性,就可以使用事务补偿。 与执行过程中发生错误后立即回滚事务的方法不同,事务补偿是一种事后检查和补救措施。 一些常见的实现方法包括:数据的核对检查、基于日志的比较、定期与标准数据源进行比较。 同步等等。 交易补偿也要结合业务系统来考虑。

2、跨节点关联查询join问题

在分段之前,系统中很多列表和详情页所需的数据都可以通过SQL join完成。 分割后,数据可能分布在不同的节点上。 这时候join带来的问题就比较麻烦了。 考虑到性能,尽量避免使用join查询。

解决这个问题的一些方法:

1)全局表

全局表也可以看作“数据字典表”,是系统中所有模块都可能依赖的表。 为了避免跨数据库连接查询,可以在每个数据库中保存此类表的副本。 这些数据通常很少被修改,因此无需担心一致性问题。

2) 现场冗余

典型的反范式设计使用空间换时间并避免连接查询以提高性能。 例如:保存订单表时,也会保存一份冗余副本,这样在查询订单明细时,就不需要再查询“买家用户表”了。

但该方法的适用场景也有限,比较适合依赖字段较少的情况。 冗余字段的数据一致性也很难保证。 就像上面的订单表例子一样,买家进行更改后,是否需要在历史订单中同步更新? 这也要结合实际业务场景来考虑。

3)数据组装

在系统层面,查询分为两部分。 第一次查询的结果重点是找到关联数据ID,然后根据该ID发起第二次请求获取关联数据。 最后,将获得的数据组装成字段。

4) 内质网碎片化

在关系数据库中,如果能够先确定表之间的关联性,并将这些关联的表记录存储在同一个分片上,就可以更好地避免跨分片连接问题。 在1:1或1:n的情况下,通常是根据主表的ID主键进行拆分。 如下所示:

关系库中的数据表的关系_intellij idea 数据库关系图_关系库数据系统有哪些

这样,Data Node1上的订单订单表和订单明细表就可以通过偏关联的方式进行查询,在Data Node2上也是如此。

3、跨节点分页、排序、功能问题

跨节点查询多个数据库时,可能会出现限制分页、按排序排序等问题。 分页需要根据指定字段进行排序。 当排序字段为分片字段时,通过分片规则更容易定位到指定分片; 当排序字段不是分片字段时,就变得更加复杂。

数据需要先在不同的分片节点进行排序返回,然后将不同分片返回的结果集再次汇总排序,最后返回给用户。 如图所示:

intellij idea 数据库关系图_关系库中的数据表的关系_关系库数据系统有哪些

上图只取第一页的数据,对性能影响不大。 但是,如果获取的页面数量很大,情况就会变得复杂得多,因为每个分片节点中的数据可能是随机的。 为了排序的准确性,需要对所有节点的前N页数据进行排序并合并。 最后,再进行整体排序。 这样的操作会消耗CPU和内存资源,因此页面数量越大,系统性能就越差。

使用Max、Min、Sum、Count等函数进行计算时,也需要先在每个分片上执行相应的函数,然后对每个分片的结果集进行汇总并再次计算,最后返回结果。 如图所示:

intellij idea 数据库关系图_关系库中的数据表的关系_关系库数据系统有哪些

4.全局主键回避问题

在分库分表环境下,由于表中的数据同时存在于不同的数据库中,通常的主键值自增就无用武之地了,而某个分区的自生成ID不能保证数据库是全局唯一的。 因此,需要单独设计全局主键,避免跨数据库主键重复。 有一些常见的主键生成策略:

1)通用唯一标识

UUID的标准形式包含32个十六进制数字,分为5段,36个字符,形式为8-4-4-4-12。

例如:-e29b-41d4-a716-

UUID为主键,这是最简单的解决方案。 本地生成,性能高,不消耗网络时间。 但缺点也很明显。 由于UUID很长,会占用大量的存储空间。 另外,创建索引作为主键并基于索引进行查询时也会存在性能问题。 这种情况下,UUID乱序会导致数据位置频繁变化,从而导致。

2)结合数据库维护主键ID表

在数据库中创建一个表:

CREATE TABLE `sequence` (    `id` bigint(20) unsigned NOT NULL auto_increment,    `stub` char(1) NOT NULL default '',    PRIMARY KEY  (`id`),    UNIQUE KEY `stub` (`stub`)  ) ENGINE=MyISAM;

存根字段被设置为唯一索引。 相同的存根值在表中只有一条记录,可以同时为多个表生成全局ID。 表内容如下:

+-------------------+------+  | id                | stub |  +-------------------+------+  | 72157623227190423 |    a |  +-------------------+------+

请改用存储引擎,以获得更高的性能。 采用表级锁,对表的读写是串行的,所以不用担心并发时两次读取同一个ID值。

当需要全局唯一的64位ID时,执行:

REPLACE INTO sequence (stub) VALUES ('a');  SELECT LAST_INSERT_ID();

这两条语句是级别的,()必须和into在同一个数据库连接下才能得到刚刚插入的新ID。

使用into代替into的好处是可以避免表行数过大,并且不需要定期清理。

这种方案比较简单,但是缺点也很明显:存在单点问题,对DB的依赖强。 当DB异常时,整个系统将不可用。 配置主从可以提高可用性,但是当主库故障、主从切换时,特殊情况下数据一致性很难保证。 另外,性能瓶颈仅限于单个MySQL的读写性能。

团队使用的主键生成策略与上面的表解决方案类似,但更好地解决了单点和性能瓶颈的问题。

该方案的总体思路是建立两台以上的全局ID生成服务器,每台服务器上只部署一个数据库,每个数据库都有一张表记录当前的全局ID。 表中ID增长的步长是库的数量,起始值按顺序错开,这样ID的生成就可以散列到每个库。 如下所示:

intellij idea 数据库关系图_关系库中的数据表的关系_关系库数据系统有哪些

ID由两个数据库服务器生成并设置为不同的值。 第一站起始值为1,步长每次增加2。 对方站起始值为2,步长每次增加2。 结果,第一站生成的ID都是奇数(1、3、5、7……),第二站生成的ID都是偶数(2、4、6、8……)。 .)。

该方案将生成ID的压力平均分配到两台机器上。 它还提供系统容错能力。 如果第一台机器出现错误,可以自动切换到第二台机器获取ID。 但它有以下缺点:系统添加机器时,横向扩展比较复杂; 每次获取ID,都需要读写DB。 DB的压力还是很大,只能依靠堆机来提升性能。

可以在解决方案的基础上继续优化,使用批量的方式来减少数据库的写入压力,每次获取一个范围的ID号段,使用完后再去数据库获取,这样可以大大减少数据库的压力。 如下所示:

关系库数据系统有哪些_intellij idea 数据库关系图_关系库中的数据表的关系

或者使用两个DB来保证可用性。 数据库中仅存储当前最大ID。 ID生成服务每次批量拉取6个ID,先改为5个。应用访问ID生成服务时,不需要访问数据库,从号段缓存中依次调度ID 0~5 。 这些ID下发后,将其更改为11,下次可以分配ID 6~11。 这样一来,数据库的压力就减少到原来的1/6。

3)分布式自增ID算法

该算法解决了分布式系统生成全局ID的需求,生成64位的Long类型数,组成:

这样做的好处是:毫秒数处于较高水平,生成的ID一般按照时间趋势增加; 不依赖第三方系统,稳定性和效率高。 理论上QPS约为409.6w/s(1000*2^12)。 并且整个分布式系统不会出现ID冲突; 可以根据自己的业务灵活分配比特。

缺点是它严重依赖机器时钟。 如果时钟被调回,可能会导致重复的 ID 生成。

总结

结合数据库和独特的ID解决方案,可以参考业界比较成熟的解决方案:Leaf——美团点评的分布式ID生成系统,兼顾了高可用、容灾、分布式降频等问题。

5. 数据迁移和扩展问题

当业务快速发展,面临性能和存储瓶颈时,就会考虑分片设计。 这时候就不可避免地要考虑历史数据迁移的问题。 一般的做法是先读取历史数据,然后按照指定的分片规则将数据写入到各个分片节点。 另外,还需要根据当前的数据量和QPS以及业务发展的速度进行容量规划,计算出大概需要的分片数量(一般建议单表的数据量)单个分片不应超过1000W)

如果采用数值范围分片,只需要增加节点扩容即可,无需迁移分片数据。 如果采用数值模分片,考虑后期扩容问题会相对麻烦。

#何时考虑分段?

我们来谈谈什么时候需要考虑数据分割。

1、可以的话尽量不要剪。

并不是所有的表都需要拆分,主要看数据的增长速度。 细分会在一定程度上增加业务的复杂性。 数据库除了承载数据存储和查询之外,辅助业务更好地实现需求也是其重要任务之一。

除非绝对必要,否则不要使用分库分表的大招,避免“过度设计”和“过早优化”。 分库分表之前,不要为了分而分。 尽量先做能做的,比如升级硬件、升级网络、读写分离、索引优化等。当数据量达到单表的瓶颈时,可以考虑分库分表。

2、数据量过大,正常运维影响业务访问。

这里所说的运维是指:

1)对于数据库备份,如果单表太大,备份时会需要大量的磁盘IO和网络IO。例如通过网络传输1T数据,占用50MB,则需要20000秒完成传输。 整个过程的风险是比较高的。

2)当对大表进行DDL修改时,MySQL会锁定整个表。 这个时间会很长。 这段时间业务无法访问表,影响很大。 如果使用pt---,使用过程中会创建触发器和影子表,这也会花费很长时间。 在此操作期间,计为风险时间。 拆分数据表并减少总量可以帮助降低这种风险。

3)大表访问更新频繁,更容易出现锁等待。分割数据,以空间换取时间,变相降低访问压力

3、随着业务的发展,有些领域需要进行垂直拆分。

例如,如果项目开始时设计的用户表如下:

id                   bigint             #用户的IDname                 varchar            #用户的名字last_login_time      datetime           #最近登录时间personal_info        text               #私人信息.....                                   #其他信息字段

在项目初期,这样的设计满足简单的业务需求,有利于快速迭代开发。 当业务快速发展时,用户数量从10万激增至10亿。 用户非常活跃,每次登录都会更新字段,导致用户表不断更新,压力很大。 其他字段:id、name 不变或很少更新。 从业务角度来说,需要将它们拆分出来,创建一个新表。

属性的更新和查询频率较低,并且文本字段占用太多空间。 这时候就需要对表格进行垂直分割。

4、数据量快速增长

随着业务的快速发展,单表数据量会不断增长。 当性能接近瓶颈时,就需要考虑水平分片,建立独立的数据库和表。此时,必须选择合适的分片规则并提前估算数据容量。

5. 安全性和可用性

不要把鸡蛋放在同一个篮子里。 纵向切分是在业务层面进行的,将不相关业务的数据库分开。 由于每个业务的数据量和访问量都不同,不能因为一项业务影响数据库而牵连到其他业务。 使用水平切片,当数据库出现问题时,不会影响100%的用户。 每个数据库只承载部分业务数据,可以提高整体可用性。

#案例分析

1、用户中心业务场景

用户中心是一个很常见的业务,主要提供用户注册、登录、查询/修改等功能。 其核心表是:

User(uid, login_name, passwd, sex, age, nickname)
uid为用户ID, 主键login_name, passwd, sex, age, nickname, 用户属性

任何脱离业务的建筑设计都是流氓。 在分库分表之前,需要梳理一下业务场景需求:

1、用户侧:前端访问,访问量较大,需要保证高可用性和高一致性。 需求主要有两类:

用户登录:通过/电话/邮箱查询用户信息,1%的请求属于此类 用户信息查询:登录后通过uid查询用户信息,99%的请求属于此类

2、运营端:后端访问,支持运营需求,根据年龄、性别、登录时间、注册时间等进行分页查询,属于内部系统,访问量不大,对可用性和一致性要求不高。

2、水平分割法

当数据量越来越大时,就需要对数据库进行水平分段。 上述分割方法包括“基于数值范围”和“基于数值模”。

“基于取值范围”:基于主键uidintellij idea 数据库关系图,将数据按照uid的范围水平划分到多个数据库中。 例如:user-db1存储uid范围为0~1000w的数据,user-db2存储uid范围为1000w~的数据。

优点是:扩展简单。 如果容量不够,只需添加新的db即可。

缺点是请求量不均匀。 一般新注册的用户会比较活跃,所以新的user-db2的负载会比user-db1更高,导致服务器利用率不平衡。

“基于数值取模”:同样以主键uid作为划分依据,根据uid的取模值将数据水平拆分到多个数据库中。 例如:user-db1 存储 uid 数据模 1,user-db2 存储 uid 数据模 0。

优点是:数据量和请求量分布均匀

缺点是:扩容麻烦。 当容量不够时,需要添加新的db。 需要考虑数据的平滑迁移。

3.非uid查询方法

水平切分后,可以很好的满足通过uid查询的需求,可以直接路由到具体的数据库。 例如,对于基于非 uid 的查询,不知道应该访问哪个库。 这样的话就需要遍历所有的库,性能会下降很多。

对于用户侧,可以采用“建立非uid属性到uid的映射关系”的方案; 对于运营端,可以采用“前后端分离”的解决方案。

建立非uid属性与uid的映射关系

1)映射关系

例如:如果无法直接定位到数据库,可以建立→uid映射关系,使用索引表或者缓存来存储。 访问时,先通过映射表查询对应的uid,然后通过uid定位到具体的库。

映射表只有两列,可以承载大量数据。 当数据量太大时intellij idea 数据库关系图,还可以对映射表进行水平分割。 这类kv格式的索引结构可以利用缓存来优化查询性能,而且映射关系不会频繁变化,缓存命中率会很高。

2) 遗传法

分库基因:如果通过uid将库分为8个库,并且使用uid%8进行路由,那么uid的最后3位决定了这行User数据落在哪个库上,那么这3位可以看成子库基因。

上述映射关系方式需要额外存储映射表,当通过非uid字段查询时,需要额外的数据库或缓存访问。 如果想消除冗余的存储和查询,可以使用f函数获取该基因作为uid的子库基因。 生成uid时,参考上面介绍的分布式唯一ID生成方案,加上最后3位值=f()。 查询时,只需计算f()%8的值即可定位到具体的库。 但这需要提前进行容量规划,预估未来几年需要将数据量划分为多少个数据库,并预留一定数量的数据库基因位。

关系库中的数据表的关系_intellij idea 数据库关系图_关系库数据系统有哪些

前台与后台分离

对于用户端来说,主要需求是关注单行查询。 需要建立/phone/email到uid的映射关系,可以解决这些字段的查询问题。

操作方面,有很多批量分页、各种条件的查询。 此类查询需要大量计算,返回大量数据,对数据库性能消耗较高。 此时,如果与用户侧共享同一批服务或数据库,少量的后台请求可能会占用大量的数据库资源,导致用户侧访问性能下降或超时。

此类业务最好采用“前后端分离”的方案。 运营侧后端业务抽取独立的数据库和数据库,解决与前端业务系统的耦合。 由于运营方对可用性和一致性要求不高,因此不需要访问实时库,而是通过数据的异步同步来访问运营库。 当数据量较大时,还可以使用ES搜索引擎或Hive来满足后台复杂的查询方式。

# 支持分库分表中间件

站在巨人的肩膀上,可以省掉很多力气。 目前有一些比较成熟的分库分表开源解决方案:

如有侵权请联系删除!

13262879759

微信二维码