1

万字干货+实战复盘:百亿数据分库分表核心流程详解 - 更多 - dbaplus社群:围绕Data、...

 1 year ago
source link: https://dbaplus.cn/news-160-5123-1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

万字干货+实战复盘:百亿数据分库分表核心流程详解

囧辉 2023-03-03 18:09:53


前言

俗话说:面试造火箭,入职拧螺丝。尽管99.99%的业务都不需要用到分库分表,但是分库分表还是频繁出现在大厂的面试中。

分库分表涉及到的内容非常多,有很多细节,如果在面试中被问到了,既是挑战,也是机会,如果你能回答好的话,会给你的面试加很多分。

由于业务量的关系,绝大部分同学都很难有实际分库分表的机会,因此很多同学在碰到这个问题时很容易懵逼。

因此今天跟大家分享一下分库分表的相关知识,本文内容源于实际高并发+海量数据业务下的实战和个人的思考总结。

一、什么是分库分表

1、分表

分表指的是在数据库数量不变的情况下,对数据库里面的表进行拆分。

例如我们将SPU表从一张拆成四张。

图片

2、分库

分库指的是在表数量不变的情况下对数据库进行拆分。

例如我们本来有一个库里面放了两张表,一张是SPU表,一张是SKU表。我们将这两张表拆到两个不同的库里面去。

图片

3、分库分表

也就是数据库的数量,还有表的数量都发生变更。

例如我们有一个数据库里面本来有一张SPU表。我们将这个SPU表拆成四张表,并且放在两个数据库里面。

图片

二、拆分方式

当前主要的拆分方式有两种:水平拆分和垂直拆分。

水平拆分就是从左往右横着切,垂直拆分就是从上往下竖着切。当然具体切几刀,这个要看具体的业务需求。

图片

1、水平拆分

水平拆分指的是在整个表数据结构不发生变更的情况下,将一张表的数据拆分成多张表。因为当单张表的数据量越来越大时,这张表的查询跟写入性能也会相应的变得越来越慢。

因此这个时候我们可以将单张表拆分成多张表,从而让每张表的数据量都变小,从而可以提供更好的读写性能。

图片

2、垂直拆分

垂直拆分指的是将本来放在一张表的字段拆分到多张表中。

图片

例如在这个例子中,我们将pic这个字段单独拆分出来,然后剩下的三个字段还保留在原表里面。

这种场景主要是因为在业务的初期,为了业务的快速发展,我们将商品的所有字段都放在一张表里面。但是随着后面的业务的发展,我们发现这个pic字段可能变得越来越大,从而影响到我们商品的基本信息的查询性能。因此这个时候我们可以将这个pic字段单独拆分出去。

当然这个pic字段拆分出去之后,它应该要存储这个原来这个商品的这个id。

三、为什么需要分库分表

因为单台MySQL服务器的硬件资源是有限的,随着业务的不断发展,请求量和数据量会不断增加,数据库的压力会越来越大,到了某一时刻,数据库的读写性能可能会开始下降,这个时候数据库就成为请求链路中的瓶颈。

此时可能就需要我们去对数据库进行优化,业务初期我们可能会使用增加索引、优化索引、读写分离、增加从库等手段来进行优化,但是随着数据量的不断增大,这些优化手段的效果会变得越来越小,此时可能就需要使用分库分表来进行优化,对数据进行切分,将单库和单表的数据量控制在合理的范围内,以保证数据库可以提供高效的读写能力。

四、何时需要分库分表

总体来说:当性能出现瓶颈,并且其他优化手段无法很好的解决的时候。

我们这边必须首先明确分库分表一般是作为最终的解决手段,我们会优先使用其他的方法来进行优化。常见的优化手段有增加索引、优化索引、读写分离、增加数据库的从库等等。当我们使用这些手段都无法解决的时候,就需要来考虑分库分表。

单表出现瓶颈:

  • 单表数据量较大,导致读写性能较慢。

单库出现瓶颈:

  • CPU压力过大(busy、load过高),导致读写性能较慢。



  • 内存不足(缓存池命中率较低、磁盘读写IOPS过高),导致读写性能较慢。



  • 磁盘空间不足,导致无法正常写入数据。



  • 网络带宽不足,导致读写性能较慢。

单表超过千万级,就需要进行分库分表?

这种说法不完全准确。因为有的表它本身的结构比较简单,字段也比较少。这种表可能即使数据量已经超过了亿级,整体的读写性能也是比较高的。而有的表如果整体的结构比较复杂,字段本身也比较大,可能只是百万级,整体的性能已经比较慢了。所以这个还是得结合自己的业务情况来进行分析。这个千万级只能是作为一个参考。

五、如何选择分库分表

1)只分表

  • 单表数据量较大,单表读写性能出现瓶颈。



  • 经过评估单库的容量和性能可以支撑未来几年的增长。

2)只分库

  • 数据库(读)写压力较大,数据库出现存储性能瓶颈。

3)分库分表

  • 单表数据量较大,单表读写性能出现瓶颈。



  • 数据库(读)写压力较大,数据库出现存储性能瓶颈。

4)注意点

我们在进行选择的时候,必须以未来三到五年的业务发展情况去进行评估。不能只是以当前的数据量和业务量来进行评估。否则可能就会出现频繁的进行分库分表的情况。因为分库分表整体的代价是比较大的。所以我们最好是进行充分的评估,保证最少可以支撑未来三到五年的业务增长。

小结



当数据库出现了读写性能瓶颈的时候,我们优先使用一些比较常规的优化手段来进行解决。例如比较常见的有:增加索引、优化索引、读写分离、增加从库等方式。

如果使用这些常规的手段也无法解决的时候啊,我们才会去考虑用分库分表来进行解决。

在使用分库分表的时候,必须充分考虑业务未来的整体发展。至少做到这次分库分表之后,未来的三到五年内不需要再进行分库分表。

六、拆分完整流程概览

1)评估是否需要拆分。主要就是评估是否有其他更轻量的优化手段可以解决问题,从而可以避免进行分库分表。

2)拆分详细技术方案设计。最核心的内容是拆分SOP,也是我们今天后续要详细讲的内容。

3)技术方案评审优化。分库分表的整体改动比较大,需要让大家一起评估下方案是否有问题,或者是否存在可以优化的地方。

4)同步相关影响方。拆分可能需要一些下游配合改造,需要提前周知他们。

5)正式进入拆分。

接下来我们来看一下拆分的SOP。

七、拆分SOP(核心)

1、目标评估

我们首先要评估本次拆分需要拆成几个库和几个表,这个主要取决于我们的拆分目标,例如:读写能力要提升到现在的X倍、负载降低Y%、容量要支撑未来的Z年发展等等。

在大多数情况下,我们可以将单表的行数作为一个重要参考指标,例如将单表控制在千万级以下。特殊情况下如果你要拆分的表单行数据很大,例如字段很多或者某字段很大,这种情况你需要结合实际的性能表现去评估一个合理的值。

一个例子:当前数据20亿,5年后评估为100亿。分几个表?分几个库?

解答:一个合理的答案,1024个表,16个库。按1024个表算,拆分完单表200万,5年后为1000万。

2、切分策略

当前主流的方案有3种:范围切分、中间表映射、hash切分。

1)范围切分

范围切分是指按某个字段的区间来进行切分。例如每个表放1000万数据,id从0~1000万的放在第一个表,1000万~2000万放在第2个表,依次类推。

图片

优点:后续扩容很方便,无需进行迁移数据,甚至可以将后续的表扩容、数据库扩库全部做到自动化。

缺点:存在明显的写偏移,写流量其实是全部集中在最新的表上。因此范围切分并没有起到将写流量均匀分摊到各个库各个表的效果,同时读流量可能也会存在偏移,因为一般来说,最近增加的数据被查询的概率通常会更大一点。

2)中间表映射

中间表映射是将分表键和数据库的映射关系记录在一个单独的表中,每次路由前先查询该表,得到具体路由的数据库,然后进行操作。

图片

优点:很灵活,可以随意设置路由规则。

缺点:引入了额外的单点,增加了复杂度,这个映射表可能也会很大,并且其查询QPS会非常高,怎么保障高性能和高可用会是一个新的问题。

3)Hash切分

通过对分表键进行一定的运算(通常是取模),从而决定路由到哪个库哪个表。

图片

优点:数据分片比较均匀,读写也会比较均匀的分摊到各个库和各个表。

缺点:可能存在跨节点查询和分页等问题。

4)小结

目前大多数互联网服务主要使用的是hash切分。

范围切分存在写流量集中在单表的问题,这个会有严重的写性能问题,特别是随着业务的发展,写流量的QPS会越来越高,这个会成为一个严重的瓶颈,目前看这个方案可能更适合一些归档类的功能。

中间表映射的方案则是太复杂了,如果你的映射数据太多的话,甚至有可能这个映射表也需要进行分库分表,那就进入恶性循环了。

不过,虽然中间表映射虽然有一些问题,但是我觉得可能在一些特殊的场景下可以使用,例如大商家问题。如果有少量商家的数据量特别大,导致出现偏移,一种思路是将这些商家的数据使用单独的表存放,这部分大商家通过中间表映射路由,其他的商家还是走hash路由。当然,这只是一个简单的思考,没有经过严格的验证。

3、选择分表字段

在单库单表的时候,全部数据都放在一张表中,因此我们可以随意的进行 join 操作和分页操作,但是如果进行了分库分表,数据会分到不同的数据库和数据表上,可能导致原本进行分页的数据分到了不同的数据库中,从而导致跨库查询等问题。而分表字段就是决定数据如何划分的关键因素,通过合理的选择分表字段,我们可以将原本需要进行分页的数据划分到同一张表上,从而避免跨库查询的问题。

例子:以美团外卖的商品数据为例,我们可以思考下主要有哪些查询商品的场景。

第一个是用户视角,我们在点外卖时需要查询商品,但是我们在点外卖时会首先进入到商家页面,所以这个地方有商家id字段。

第二个是商家视角,商家在后台管理自己的商品,这个地方也有商家id字段。

因此在美团外卖商品数据的这个例子中,商家id字段作为分表键就是一个比较合理的选择,因为他覆盖了最高频的几个使用场景。

一个例子:10个库,1000张表:0~99、100~199、200~299、...

分表字段:shopId,值为1234

数据表编号:shopId % 1000 = 1234 % 1000 = 234

数据库编号:shopId % 1000 / 10 = 1234 % 1000 / 10 =  2

4、资源准备和代码改造

新集群的所需数据库资源可以尽早跟DBA申请,特别是拆分集群比较多的情况,一方面是因为DBA搭建新集群需要花一定的时间,另一方面是避免出现资源不足导致延期的情况。

至于代码的改造,主要会涉及到几个部分:

  • 将新集群的数据源引入到我们的服务中;



  • 支持灵活的灰度读写操作;



  • 第三是数据全量迁移、一致性校验等任务。

因为整个分库分表过程是不停机,并且无损的拆分,因此拆分过程中新老数据源会同时存在一段时间,在这段灰度期间,我们会通过配置中心和相关规则去灵活的控制究竟是写新库、写老库,还是双写,读操作也类似。

5、增量数据同步(双写)

双写是为了保证增量数据在新库和老库都存在。

写新库是因为我们后续准备切换到新库,因此新库必须要有全部的数据。

写老库是因为我们不确定拆分过程中是否存在问题,通过写老保证了老库有全部的数据,这样万一新流程有问题的时候,我们可以即使切回老库的流程。从而保障了服务的可用性和稳定性。

1)常见方案

  • 同步双写,在所有写数据库的地方进行修改,修改成写两份数据。当然,这个地方一般不会去修改全部的写逻辑,而是在底层使用AOP来实现。



  • 异步双写:写老库,监听binlog异步同步到新库



  • 中间件同步工具:通过一定的规则将数据同步到目标库表

异步双写和中间件工具同步两者本质上类似,都是通过binlog的方式将数据写入到新库。只不过一个是你自己做,一个是中间件团队帮你做。

这几种方式一般来说不会差别太大,同步双写的写入延迟可能会稍微小一点。

6、全量数据迁移

光有增量数据同步还没法保证新库有全部的数据,我们还需要将以前的老数据全部迁移到新库中。通过增量同步+全量迁移,我们才能保证新库有完整的数据。

1)常见方案

  • 自己开发一个任务将老库数据迁移到新库。



  • 使用中间件同步工具,将老库数据同步到新库。如果中间件有现成工具支持的话,一般建议好接使用现成的工具,这样自己就不用再花时间去额外开发了。

2)注意点

  • 控制好同步速率;



  • 增量同步和全量迁移会同时进行,因此可能会存在并发写同一条数据,从而可能导致一些数据不一致的问题。



7、数据校验、优化和补偿

在全量数据迁移完毕,增量同步也正常运行后,并不能直接将流量切到新库。因为可能存在很多情况,导致新库和老库的数据可能没法完全一致。

例如:我们的改造存在遗漏的地方,或者说并发修改导致数据问题,等等。因此,我们需要进行新老库的数据校验和补偿,直到新老库的数据一致了,才能进行流量切换。

1)方案

  • 增量数据校验

  • 全量数据校验

2)核心流程

① 读取老库数据

② 读取新库数据

③ 比较新老库数据,一致则继续比较下一条数据

④ 不一致则进行补偿:

  • 新库存在,老库不存在:新库删除数据



  • 新库不存在,老库存在:新库插入数据



  • 新库存在、老库存在:比较所有字段,不一致则将新库更新为老库数据

3)注意点

数据校验是整个流程中最重要,通常也是花时间最多的一步。一方面是在并发下会出现很多种不一致的场景,另外是因为这一步是切读之前的最后一个保障,因此我们必须再三确认数据是正确的。否则,切读后可能就会导致一些线上问题。

8、灰度切读

在数据一致性校验通过后,我们开始将部分读流量切换到新数据库。

这一步必须遵循以下几个原则:

  • 必须支持灵活的切换,有问题可以及时切回老库。



  • 支持灵活的灰度规则,灰度早期我们会先拿少量门店进行灰度,观察一段时间,如果没问题再继续增加灰度门店。依此类推,然后到后面开始逐步使用比例来进行灰度,直到最终我们将全部流量都切到新的数据库上。



  • 灰度放量先慢后快,每次放量观察一段时间。



9、binlog 切新库

在读流量全部切换到新库后,此时新流程已经验证通过,我们开始为停写老库做准备,首先就是将监听的 binlog 从老库切换到新库。

1)核心流程

  • 启动新库的 binlog,此时下游会同时收到新老库的 binlog;



  • 观察一段时间是否正常;



  • 如果不正在,则将新库的 binlog 关闭,排查修复问题;



  • 如果一切正常,则将老库的 binlog 关闭,此时监听的 binlog 切换到新库。

2)注意点

监听 binlog 的流程我们一般会收敛在团队内部,如果外部团队想监听 binlog,一般会使用我们封装过的消息,这样在改造时,对外部团队就基本没有影响,我们改造起来也比较方便。

10、下游切换数据源

目前来看,除了 binlog 之外,主要的下游是数仓。数仓会将商品数据定期同步到 hive 上,用于进行数据的相关工作,因此需要让数仓同学将数据源切换到新数据源。

数仓一般是定期同步数据,例如一天同步一次全量数据,对实时性要求不高,因此只需在指定时间内切换即可。

11、停写老库

在我们确认老库数据源的所有依赖都切换和下线后,停写老库,此时读写流程全部切换到新数据源。至此,整个拆分流程基本结束。

八、完整SOP

图片

最后我们通过一张流程图来回顾下整个拆分流程,整个流程主要包含5个阶段。

第一阶段:拆分前的相关准备,包含了拆分的目标评估、切分策略和分表字段的选择,还有数据库相关资源的准备。

第二阶段:代码改造,主要是将新数据源引入到服务中,同时支持灵活的灰度读写。

第三阶段:数据迁移,包含了全量和增量数据迁移,还有数据一致性的校验和修复。

第四阶段:流量迁移,主要是将数据库的读写流量按灰度规则逐步切换到新库。

第五阶段:停写老库,当读写流量全部迁移到新库,老库的相关依赖都全部下线后,停写老库并释放相关资源。

九、相关工具

1、binlog监听工具

  • Databus

  • Canal

1)关于binlog

binlog是一个二进制文件,用于记录数据库表结构和表记录的变更。简单点说,就是通过 binlog 文件你可以知道数据库中究竟哪些数据发生了变更,从什么变成了什么。

而binlog监听工具主要就是用于监听MySQL产生的binlog,然后进行解析,解析成我们比较容易懂的格式,最后通过一定的手段发送到下游,例如比较常见的方式是消息队列。

在分库分表中就可以通过binlog监听工具来将老库的数据变更实时同步到新库中,以保证新老库的数据一致。

2、分库分表工具

目前主要有两种,一种是增强版JDBC驱动,另一种是数据库代理。

1)增强版JDBC驱动

以客户端 jar 包形式提供了对 JDBC 的封装,客户端直连数据库。

开源:Sharding-JDBC、TDDL、Zebra。

2)数据库代理

需要单独部署,客户端连接代理服务,代理服务负责跟数据库打交道。

开源:Sharding-Proxy、MyCat。

两种方案的核心思想都是类似的,就是他们负责将分库分表的逻辑进行抽象封装,做到让分库分表对使用方无感知,使用方只需按照制定的规则进行简单的配置和开发,就可以像没有分库分表一样正常的使用分库分表规则了。

两者的主要区别在于使用增强版JDBC驱动只需要依赖一个jar包,此时应用服务还是直连数据库的。

而数据库代理则需要额外部署一个单独的代理服务,应用服务从之前的直连数据库,变成调用代理服务,由代理服务来负责跟数据库打交道。

目前使用的比较广泛的是增强版JDBC驱动,一方面是增强版JDBC驱动比较轻量,另外是性能也会比较好。

十、分库分表问题

在我们使用分库分表之后,系统的性能和容量都会有很大的提升,但是也会随之带来一些问题。我们一起来看一下有哪些问题,当前的主流方案是如何解决的。

1、分布式唯一ID

在单库单表情况下,我们使用表的自增ID就可以保证ID的唯一性,但是分库分表后,一张表被拆成了多张表,此时自增ID就没办法保证唯一性了。因此,需要引入一种方案来保证ID的唯一性。

目前主流的方案有3种:UUID、雪花算法、号段模式。

1)UUID

UUID相信大家都不陌生,UUID是JDK中自带的一个工具类。什么都不需要引入就可以直接使用了,同时因为是本地生成的,性能也非常好。

但是UUID并不适合拿来做MySQl数据库的主键,MySQL的主键一般推荐使用单调递增的数字,这个因为MySQL主键使用的是聚簇索引,会把相邻主键的数据放在相邻的物理存储位置上。

当MySQL的主键是单调递增时,每次只需要简单的将数据追加到索引的最后面即可,类似于顺序写磁盘。而如果MySQL的主键是无序的,则可能需要将数据插入到之前已有的数据中间。如果这个插入位置所在的数据页不在内存中,则需要先从磁盘读取到内存中,这会导致产生磁盘的随机IO。同时,如果该数据页的空间不足,则可能会产生页分裂,导致需要移动大量数据。

最后就是,MySQL的普通索引需要存储主键索引值,如果主键值更占用空间了,会导致普通索引的B+树层高变高,磁盘IO次数变多,最终导致性能变慢。

2)雪花算法

图片

雪花算法的核心思想是通过一定的规则生成一个64位的long类型数字。除了最高位的1位不用之外,其他63位由三部分组成。分别是41位用于存储时间戳,10位用于存储机器ID,12位用于存储序列号。

简单来说就是支持部署1024台服务器,同时每台服务器1毫秒最多可以生成4096个ID,也就是每秒可以生成四百零九万个,并且可以使用69年。

这个量级应该基本可以满足任何业务了,当然在实际使用过程中,这三部分的位数可以结合自己的场景去进行修改。

3)号段模式

20230303061639199.png

在讲号段模式之前,我们先介绍下数据库生成的方式。

数据库生成指的是使用一个额外表的自增ID来作为分布式ID,因为ID都是由同一张表自增生成,所以可以保证全局唯一性。但是这种方案有个严重的问题,每次使用分布式唯一ID都需要来读写这张表。一旦并发量比较大,数据库会有严重的性能问题。

号段模式就是在此基础上进行了优化,之前是每次获取分布式ID都需要读写数据库,号段模式优化成批量的方式,每次读写数据库时获取一批ID,例如每次获取1000个,将这1000个ID放在本地缓存中,1000个用完之后再来申请下一批,从而大大降低数据库的读写压力。

4)小结

这三种方案中,目前应用的比较广泛的是雪花算法和号段模式,美团开源的分布式ID生成组件 Leaf 就是提供了这两种方案,如果大家对底层细节感兴趣的话,可以去自己下载源码来看。

最后需要说一下的是,对于订单ID这种比较特殊的字段来说,一般可能不会直接使用上述的方案,而是会按照一定的规则去生成。同时可能会携带一些业务字段,例如用户ID和商家ID。

2、分布式事务

在分库分表之前,全部的表都在同一个库里,我们可以使用本地事务来保障数据的正确性。引入了分库分表之后,数据库表被分到不同的数据库中,此时就没办法使用本地事务了,因此就需要引入分布式事务来保障数据的正确性,我们来看一下当前有哪些常见的分布式事务。

1)2PC

两阶段提交,核心思想是将事务操作分为两个阶段。

图片

第一阶段:协调者首先询问所有的事务参与者是否可以执行事务提交操作。

第二阶段:协调者根据所有参与者的返回结果决定是否提交事务,如果全部的参与者都返回成功,则协调者向所有参与者发送事务提交请求。否则,协调者向所有参与者发送事务中断回滚请求。

两阶段提交是目前比较出名也是用的相对比较多的分布式事务,优点是整体流程比较简单,缺点是存在同步阻塞、协调者单点等问题。

2)TCC

核心思想是针对每个操作都有一个对应的确认和取消操作。

图片

TCC中有主服务和从服务两个角色,例如在下单的流程中,首先会走到交易服务,然后交易服务分别请求定订单服务和库存服务进行订单创建和库存扣减,此时交易服务就是主服务,而订单服务和库存服务为从服务。

TCC的核心流程如下:

首先,主服务调用所有从服务的try接口,进行业务检查和资源预留。

接着,主服务根据所有从服务的返回结果决定是否提交事务,如果所有从服务都返回成功,则调用所有从服务的confirm接口执行事务确认提交操作。否则,调用所有从服务的cancel接口执行事务取消,并释放预留资源。

估计大家应该发现了,TCC其实跟两阶段提交非常像。其实很多分布式事务的思想都是很类似的,核心都是先询问,然后提交。这两者的主要区别在于TCC是应用层的处理,而两阶段提交是数据库层面的处理。

这两种分布式事务应该是目前分布式事务中比较出名的了,其他的分布式事务还有三阶段提交、本地消息表、事务消息等等,这边不做过多的介绍,有兴趣的可以自己查阅资料。

3)高并发业务实际使用

首先说一下结论:在实际的高并发业务中一般都不会使用强一致性的分布式事务,金融场景是个特例,因为涉及到太多钱了,所以可能会用强一致性的分布式事务。

更多的是通过各种各样的手段来保证最终的一致性,常见的手段有:回滚、重试、监控、告警、幂等、对账等等,终极手段就是人工补偿。

我之前在某篇文章中说过:每个看着光鲜亮丽的系统背后可能都有一堆苦逼的程序员在默默的修数据,这个不是开玩笑的。

例子:

以外卖下单为例,整个用户下单流程会涉及到很多步骤,最核心的包括:创建订单、扣减商品库存、核销优惠券、核销会员红包等等,如果其中有一步失败,则会导致整个下单流程失败,需要将其他的流程都进行回滚,以保证不会产生资损,否则有可能出现用户下单失败,但是会员红包却被扣掉等情况。

为了避免网络抖动等情况导致回滚失败,一般都会有回滚重试流程,但是重试一般会有次数上限,因为如果重试多次还是失败,则可能是其他问题,例如代码BUG,这种情况再怎么重试也没用。因此在重试达到上限后,如果还是回滚失败,则需要发送告警,人为介入排查,然后人工修复这些数据。

而对于这些订单的下游服务来说,例如库存、优惠券等等,就需要做好接口的幂等,如果没做好幂等,可能会导致数据出现重复回滚,造成数据错误和资损。

当然,从广义上来说,保证最终一致性,也是属于分布式事务的一种。

为什么不直接使用强一致性事务?

个人觉得主要有以下几个原因:

  • 会带来严重的性能损耗,导致下单流程的耗时增加,最终导致服务吞吐量下降、用户下单体验变差。



  • 会引入额外的复杂度,开发和维护成本较高。



  • 实际业务中,由于部分成功导致数据不一致的场景,发生的概率比较低。

总结来说就是一个取舍的问题,目前大部分业务场景,使用强一致性分布式事务的ROI不够高,因此一般不会选择强一致性事务,而是选择柔性事务,保障事务的最终一致性。

3、跨库JOIN/分页查询问题

在单库单表的时候,全部数据都放在一张表中,因此我们可以随意的进行 join 和分页操作,但是如果进行了分库分表,数据会分到不同的数据库和数据表上,可能导致原本进行分页的数据分到了不同的数据库中,从而导致跨库查询问题。

目前业界主流解决方案有以下几种。

1)选择合适的分表字段

图片

这个在上文已经详细解释过了。总结来说就是,分表字段的选择,要能保证绝大部分高频查询场景,不会出现跨库的问题。在实际业务中,分表字段选择合理的话,基本可以避免95%,甚至99%以上的跨库查询问题,从而将问题的难度大大降低了。

2)使用搜索引擎支持,例如ES

我们可以将全量数据冗余一份到ES中,当出现分表字段支持不了的跨库查询时,可以使用ES来支持。除此之外,ES也会用于支持一些复杂搜索查询请求。

使用ES需要注意的是:

  • ES只存储需要进行搜索的字段,查询完ES后再根据关键字段去数据库查询完整的数据,这样是为了控制ES的大小,否则ES会容易过大,导致性能和存储问题。



  • ES只用于支持数据库难以支持的查询,就如上面说的跨库查询、复杂搜索查询,这种复杂的查询一般不会太多,因此可以保障ES的整体压力不会太大。

3)分开查询,内存中聚合

这个方案跟使用join其实大同小异。区别在于,join是数据库来做这个聚合操作,分开查询是应用层面来做聚合操作。

即使不分库分表,当表的数据量比较大时,通常也是建议不要在数据库中使用join操作,而是分开查询,然后在应用层内存中聚合。

这是因为数据库资源相对应用服务器来说会更宝贵,通常也更容易成为链路中的瓶颈,因此尽量不要让其做复杂的查询,避免占用过多的数据库资源。

注意点:

  • 查询出来的数据量

  • 占用内存情况

4)冗余字段

如果每次join操作只是为了获取少量的字段,那么可以考虑直接将这些字段冗余到表上。

5)小结

这几种方案在实际工作中都挺常使用的,一般看具体的业务场景选择合适的方案即可。

作者丨囧辉 来源丨公众号:程序员囧辉(ID:程序员囧辉) dbaplus社群欢迎广大技术人员投稿,投稿邮箱:[email protected]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK