2024-08-04
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/6504261903

回答

分库分表后,我们通常会遇到下面几个查询问题:

  1. 跨库查询问题
  2. 分页查询问题
  3. 排序问题
  4. 聚合问题
  5. 关联查询问题

为了更好地理解,大明哥按照分库分表的维度来对这几个问题进行阐述。

垂直分库产生的问题

垂直分裤一般来说都是按照业务维护来进行拆分,其核心理念就是专库专用,即将不同的业务数据分别放入到不同的数据库中。

将原来单一的数据库按照业务维度拆分为多个库,虽然在性能上能够带来很大的提升,但是带来的问题也是非常多,主要设计如下几个问题:

跨库 join 问题

将不同的业务数据拆分到不同的库中,这就会导致原来一个 join 就能解决的问题现在解决不了。那怎么办呢?有如下几种方案:

  • 数据冗余:将需要 jion 查询的数据表字段冗余在一个库中,这样就避免了跨库 join 查询了。
  • 应用层 join:将原来的 join 操作拆分为多个单表查询,然后在应用层执行 join 操作。
  • 全局表:将需要经常进行 join 操作的小表设计成全局表,然后存储在对应的库中,这样 join 操作就会退化为查询这个全局表,从而解决跨库 jion 问题
  • 使用 shardingsphere-proxyshardingsphere-proxy 是支持跨库 join 查询的,一般我们都是禁止此类操作的。
  • 利用 ElasticSearch:将所有需要检索的字段全部冗余到 ES 中,这样就可以利用 ES 来做处理了。

在实际应用过程中,大明哥还是推荐使用在应用层执行 join 操作,既然做了垂直分库,那么我们项目肯定是分布式架构,因此我们是可以通过调用 API 的方式来执行查询操作的。有小伙伴会疑问,如果我关联的表很多呢?那不要执行很多次调用 API?那大明哥就反问了,是什么业务会要你关联很多表?难道不知道单库情况下关联表多也会有性能问题吗?

事务问题

原来在一个数据库里面我们是不需要考虑事务问题的,毕竟一个 @Transactional 就能解决问题。但是现在我们拆分了多个库,那么就必须要考虑事务问题了。

分布式事务是分布式系统中最核心的一个问题,所有分布式系统都必须解决这个问题,如果不解决则可能会出现数据不一致的情况。

在垂直分库后,原本只需要往一个库里面插入数据,现在则需要往多个库里面插入数据,为了杜绝数据不一致的情况发生,我们需要引入分布式事务来解决这个问题。目前主流的方案有如下几种:

  • Best Efforts 1PC模式。
  • XA 2PC、3PC模式。
  • TTC事务补偿模式。
  • MQ最终一致性事务模式。

分布式事务是一个很大的话题,大明哥就不做过多阐述了,后面会有专门的栏目来介绍的。

水平分库产生的问题

水平分库则是将原本存储在一个库的一张表中的数据拆分到多个库中的多个表中,这种方式避免了单表数据量过大而导致性能不佳的情况,但是水平分库后会带来的问题则是更多。

聚合操作

在单库单表的情况下,我们做聚合操作是非常简单的,一个 SQL 语句就可以搞定,比如 count(1)sum() 等等都是一条 SQL 的事情。但是,做了分库后则不同的,因为数据被分散到多个库中,每个库只有整体的一部分数据,无法得到全局视角。

对于这种情况,我们一般有如下几种解决方案。

  • 应用层聚合

在应用层执行数据聚合操作,即将查询的情况发送给各个数据库实例,每个数据库实例执行自己的聚合操作,返回部分结果。应用层收集这些结果后进行最终的聚合计算。

这种方案比较灵活,适合绝大多数场景,但是它会带来额外的计算,在数据量特别大的情况下,会有比较大的性能开销。

  • 增量更新

记录每个数据库实例的变更日志,在聚合的过程中只处理变化的这部分数据。

这种方案虽然他减少了计算的过程,提升了聚合的效率,但是他实现会很复杂,同时也需要维护聚合数据的准确性,难度较大。一般情况下,不是很推荐这种方案。

  • 引入中间层

记住一句话:没有什么是引入一层中间层解决不了的,不行,那就再引入一层。比如引入 ES,在 ES 中执行聚合操作。又或者引入数据库仓库等等。

分页问题

在分库分表前,我们可以直接通过 limit n,m 来实现分页,但是进行水平分开后,每个库都只有拥有部分数据,通过常规的手段我们是无法在全局视角下获取准确的分页。一般解决方案有如下几个:

  1. 全局查询法:即每个分库获取前 N 页的数据,然后在应用层进行分页处理。这种方案的优点是实现比较简单,但是缺点很大,因为每次都要返还前 N 也的所有的数据,在页码较多的情况下会导致返回的数据量过大,从而占用带宽、对应应用程序的性能和内存都有比较大的挑战。
  2. 禁止跳页法:不允许跳页,只能通过下一页、上一页的按钮来进行分页查询,在点击下一页时,我们将这一页的最大值传递给后端,然后后端通过与这个最大值的比较来获取当前页码的数据。这种方案牺牲了业务的灵活性,但是它的优点是只需要返还每个分库的当前页码的数据。
  3. 二次查询法
  • 把分页查询的数据提前聚合到 ES 中,分页查询直接在 ES 中查询就可以了
  • 搭建数据中心,将所有子库数据的数据全部汇聚到数据中心,后面所有的分页数据全部都从数据中获取

详细情况请阅读:分库分表后,如何解决分页查询问题?

ID 主键问题

在进行水平分库后有一个问题我们一定要考虑就是主键 id 的问题,在单库时,我们可以使用递增策略来作为表的主键,但是水平分库后,这种方案就不是那么可行了,因为会碰到主键重复的情况,这对业务的影响是非常大的。目前我们有如下几种方案:

  1. 依然采用主键递增策略,只不过是每个库的初始值和递增步长是不一样的,假如我们拆分了 3 个库,第一个库的初始值为 1001、第二个库的初始化值为 1002、第三个库的初始值为 1003,所有库的递增步长为 3 ,这样就可以控制每个库的 id 是交叉递增的,保证了唯一性。但是这种方案不利于库容,每次扩容时都需要对递增步长和初始化都重新设置下。
  2. 采用特殊算法,比雪花算法、uuid 等算法来实现全局唯一的 id,但是不推荐 uuid,因为它对数据库会有一定性能的损耗。
  3. 采用第三方中间件,例如数据库、Redis 或者专门的分布式 id 生成器,例如百度 Uidgenerator、美团 Leaf、滴滴 TinyID

对于分布式 id,大明哥后面会有专门的栏目来讲,这里就不过多阐述了。

节点扩容问题

当现有的分库再次无法满足业务时,这个时候我们就需要考虑库容了,扩容是一个非常复杂的操作,我们需要考虑非常多的情况,例如如何保证在不影响业务的情况下平滑扩容,例如扩容后,如何处理数据分片的路由规则等等。

目前业内有两种主流做法:水平双倍扩容法、异步双写扩容法。详情见:分库分表后,如何在不影响业务的情况下进行扩容?

垂直分表产生的问题

垂直分表后我们面临的问题相比垂直分库就小的多了,它无非就是将原本在一个表的数据字段拆分到多个表去了,唯一的影响点就是在插入的时候我们需要插入多个表,查询的时候需要关联下查询,设置好对应的外键、保证事务的一致性就可以完美解决了,因为不会存在事务问题,一个 @Transactional 注解就搞定了。

水平分表产生的问题

水平分库遇到的问题,在水平分表中我们都会碰到,只不过解决方案相比水平分库就简单些了,比如如果不是特别在乎性能,聚合操作、分页我们都可以使用 union 来解决。

对于 id 主键问题和节点扩容问题还是采用上述方案比较靠谱。

阅读全文