柒夭日志:分库分表篇

362 阅读37分钟

🕹️引言

在现代应用程序开发中,数据库扮演着一个至关重要的作用。随着数据量以及并发访问次数的增加,单一数据库可能没有办法满足高性能、高可用性以及高可拓展性的需求,这个时候,分库分表就成为了一种常见的解决方案。

作为新时代程序猿的我们,如果说不了解分库分表,那怎么可以呢?所以今天我就带来了一些关于柒夭日志的分库分表篇,从头告诉你什么是分库分表,从而帮助你更好地回答面试官的问题

1. 📚什么是分库?什么是分表?什么是分库分表?

简单来说,分库分表就是将原本存储在一个数据库中的数据,分散到多个数据库或者多个表中进行存储和查询,其是企业中常见的一种针对高并发、数据量大的场景下的一种技术优化方案。

所谓分库分表,其进行的不是一件事情,而是三件事情,他们不同的事情解决的问题也是不一样的。

分库分表是哪三件事情?

分库分表针对的三件事情分别是“只分库不分表”,"只分表不分库",以及 "即分库又分表",现在我们探究一下这三件事情。

分库分表之分库(只分库不分表)

分库主要解决的是并发量过大的问题,因为并发量一旦上升了,那么数据库就可能成为系统的瓶颈,因为数据库的连接数量是有上限的,虽然你可以进行调整,但并不是无限调整的。所以,当你的数据库的读或者写的 QPS 太高,从而导致你的数据库连接数量不足的时候,就需要考虑到分库了,通过在增加数据库实例的方式来提供更多的数据库连接,从而提升系统的并发度。

这里我们拿一个电商系统的数据库来举例,当你微服务在做服务拆分的时候,你会按照功能模块去把你的系统进行服务拆分,这个时候,就需要将每个模块的数据从一个单独的数据库中拆开,分成多个数据库,如下图所示,分别将订单、物流、商品、用户等数据分别放到单独的数据库中。

然后随着你业务的不断发展,订单数目可能不断增加,这个时候如果你订单数据过多的话,甚至可以继续划分,即订单数据库可以继续分,如下图所示,分成已完成订单的数据库(历史订单)或者未完成订单的数据库(现存订单),这个需要你根据实际场景进行设置。

分库分表之分表(只分表不分库)

相比较于分库,分表主要解决的是数据量大的问题,即通过将数据拆分到多个表,减少单表的数据量,从而提升查询速度。、

分库分表(既分库又分表)

上面说完分库和分表之后,接下来我们来说第三件事,既分库又分表,这一般发生在数据库连接不够(分库解决的问题)以及单表数据量过大导致查询速度比较慢(分表解决的问题)这两个问题共同存在的时候,不过在分库分表之前,可以先考虑一下能不能优化先。

一般来说,单表的行数如果超过了 500 万行或者单表容量超过 2 GB 之后,就需要考虑做分库分表了,小于这个数据量的时候,遇到性能问题可以通过其他方式来进行优化。

PS:以上数据量是阿里巴巴 Java 开发手册中给出的数据,仅供参考

常见的优化手段

  1. 数据库的基本优化:做好索引、减少多表 join、减少冗余字段
  2. 减少数据库压力:在数据库之前加一层缓存,把一些可以接受延迟的,以及数据库变化频率较低的内容放到本地缓存或者分布式缓存当中。
  3. 冷热数据的隔离:即数据归档,可以将一些更新以及不经常使用的数据单独隔离出来,可以放到历史表或者离线数仓当中,减少表中的数据量来提升效率
  4. 数据库分区:数据库分区之后,将数据存储在不同的表当中,尽量减少单表的数据量,提升查询性能。
  5. 分布式数据库:将数据分散到多个节点上,提升容量

如果以上优化都不能解决了,这个时候我们就可以开始考虑分库分表的情况了,即既分库又分表

2. 🚇分区和分表之间有什么区别呢?

我们上面提过一个点,就是在数据库中,如果数据量比较大的话,优先考虑的是如何对数据进行优化,而不是进行分库分表,我们在优化里面提到了一个点,就是对数据实现分区操作,那么这个过程要怎么理解呢?以及这两个过程有什么区别?我们现在来探究一下。

首先我们先说一个点,就是分区和分表相同的点,都是按照一定的规则,对一张数据量特别大的表进行分解,使得表的数据量减少,从而提高查询效率。

这样听起来你可能感觉没差多少,因为两者的区别都是把表进行拆分,那具体有什么差别吗?

主要就是分区和分表后数据的存储方式发生了变化。

这个要从 MySQL 的索引说起,在 Innodb 中(8.0之前),表存储主要依赖两个文件,分别是 .frm 文件和.ibd 文件。

.frm文件用于存储表结构定义信息,而.ibd文件则用于存储表数据。

这里依旧我们拿我们熟悉的电商系统来举例,假设我们现在有一张 order 表,如果要对他进行分区和分表,区别如下:
MySQL InnoDB 存储引擎在分区表的时候,会将每一个分区都分别存放在一个单独的 .ibd 文件中,所有的 .ibd 文件组合构成和表的物理结构,即 Table Space。

对于上面分区的 order表,存储时会在 MySQL 的 data 目录下创建一个用户名+表名+分区名.ibd 的文件(如:order_p1.ibd),用来存储 order 表中第一个分区的数据,同样会有 order_p2.ibd 和 order_p3.ibd 来存储第二和第三个分区的数据:

order_p1.ibd
order_p2.ibd
order_p3.ibd
order.frm

MySQL InnoDB 存储引擎在分表的时候,会将每一个分表分别存放在一个单独的 .frm文件中,所有的 .frm 文件组合构成表的逻辑结构,即 Table Definition。

对于上面分表的 order 表,存储的时候会在 MySQL 的 Data 目录下创建一个后缀为 .frm,名字为 "order_1.frm"的表格文件,存储 order表中第一个分表的数据,同样会有另外的 order_2.frm 和 order_3.frm 来存储第二个和第三个分表的数据:

order_1.ibd
order_1.frm
order_2.ibd
order_2.frm
order_3.ibd
order_3.frm

现在可以说明二者之间的区别了:

  • 数据在做了分区后,表面是还是只有一张表,只不过数据保存在不同的位置上了(同一个.frm文件),在做数据读取的时候操作的表名还是users表,数据库会自己去组织各个分区的数据。
  • 数据在进行了分表复制后,不管是表面上还是实际上,都已经不是同一张表了,其分成了多张表(多个.frm 文件 )。所以数据库在进行操作的时候需要去指定对应的表名。

所以一般来说,当数据量比较大的时候,我们优先考虑分区,在分区搞不定的时候再来考虑分表。

因为分表可以在分区的基础上,进一步减少查询时带来的系统的开销,因为你在分表之后,单表的数据量更小,其页缓存率更高, I/O 读写性能比较高,另外分表也能降低锁带来的阻塞,也可以提高事务处理的效率。还有就是数据量小的表其可以提升备份和恢复的速度、且具有更好的横向扩展性。

分区的方式

原本打算在分库分表常用技术选型那里讲的,不过感觉这个点知识也不多,就简单讲讲了。

表分区的方式一般有水平分区、垂直分区:

  1. 水平分区:将表根据行进行划分,即把一个表的数据划分成多个表的数据,每个表形成一个分区;这些细分出来的部分存放在多个不同的分区表中(比如按年份等)。每块数据都存放在不同的表中,可以显著提高操作的效率。
  2. 垂直分区:将表根据表字段进行划分,将表中的列(或字段)分割成多个数据表,用于存储不同的业务场景内的数据。使分区后的数据表垂直分离,可以有效减少数据库查询中非必要的访问。

这里注意一个点:MySQL 支持的分区类型是水平分区

分区的原则

常见的表分区实践中,可以按照以下一些原则进行分区:

  1. 按照系统负载,将数据分到不同的区域中;
  2. 按照应用程序查询模式,将数据库分为不同的分区;
  3. 按照月份或者年份分区;
  4. 通过实践哈希法可以将记录放置到不同的分区中;
  5. 基于范围查询,使用分段来将记录放置到不同的分区中,以便提高查询效率。

MySQL 实现水平分区的方法

这里使用年限来进行分区,然后用一个订单表创建的 SQL 语句来作为案例,一共分成 5 页,如下所示:

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    order_date DATE,
    customer_id INT,
    total_amount DECIMAL(10, 2)
) PARTITION BY RANGE (YEAR(order_date)) (
    PARTITION p0 VALUES LESS THAN (2021),
    PARTITION p1 VALUES LESS THAN (2022),
    PARTITION p2 VALUES LESS THAN (2023),
    PARTITION p3 VALUES LESS THAN (2024),
  	PARTITION p4 VALUES LESS THAN MAXVALUE
);

3. 🎭分库分表会带来什么问题?

我们上面说了,能不用分库分表的情况下最好就是不使用分库分表,那么是为什么呢?

那肯定是分库分表会带来一些问题,这里我们就来盘点一下,分库分表会带来哪些问题?

  1. 查询和写入数据问题

做了分库分表之后,所有的读写操作都需要带着分表字段,因为只有这样才能知道我们具体去操作哪张表、然后去哪张表中查询数据,如果不带分表字段的话,就必须支持全表扫描。

但是,单表查询的时候实现全表扫描比较容易,一旦做了分库分表,就没有办法实现扫表操作了,如果要实现扫表操作的话,就要把所有物理表都扫描一遍。

其次,如果我们要从多个数据库中查询或者写入数据的话,就很多事情都不能实现了,比如常见的就是跨库事务不支持,所以在实现分库分表之后就容易出现因为不支持事务导致的数据一致性问题。

其次,在实现了分库分表之后,以前单表中很简单的分页查询以及排序等操作就全部失效了,因为我们不能跨多张表实现分页以及排序这个有什么解决方法呢,我们后面来讲。

  1. 分表之后如果要实现二次分表,要怎么实现?

这个就涉及到了常见的分表算法,常见的方法有取模,哈希,关键字这三种方式,这三种方式虽然很不错,但是这三种方式对于二次分表的时候,就有点心有余而力不足了,那么又有什么方法解决来呢,这里我们埋一手坑,就是我们要讲到的一致性哈希,这里怕大家可能没有听说过,所以专门补了一个链接去帮助大家理解。如果之前没有了解的过同学可以看看:

juejin.cn/post/729974…

  1. 如何保证生成的表的主键 ID 具有全局唯一性呢?

由于分表不同,但是要保证分表之后的 ID 具有全局唯一性,这里就涉及到了分布式 ID 的生成策略了,那要怎么进行处理呢?这里我们会逐一去进行分析,可以剧透一下,这里我们最终使用的算法是雪花算法,我们之后会对常见的分布式 ID 生成策略进行逐一分析,然后分析我们为什么使用雪花算法。

  1. 分库分表之后如何进行分页查询?

我们在分库分表之后,数据会散落在不同的数据库当中,这个时候如果跨多个数据库进行分页查询,或者排序等操作,这个都不用想都知道有多麻烦。

如果分的库不是很多,我们还可以通过扫表的方式来把多个数据库中的数据提取出来,在内存中进行分页和排序,但是这只是数据量不多的情况下才可以这样做,如果数据量多了又要怎么实现呢?这个我们先埋个坑,然后后面一一进行讲解,已经迫不及待的同学也可以先跳转到后面去看看。

4. ✨分库分表如何进行拆分?

既然到了分库分表,那接下来要考虑的就是如何进行拆分的问题了。通常在做拆分的时候有两种拆分的方法,分别是水平拆分以及垂直拆封

水平拆分

水平拆分就是将数据库中的表数据分散到多个表中,即数据表中列数不变,行数减少,来使得每个表中的数据量下降,我们这里拿用户表来进行举例,将不用的用户拆封到不通过的表中。

纵向拆分

纵向拆封一般是通过将数据库表中的字段减少,然后数据的行数不变,列数减少,来使得每个表中的数据量下降,这里我们依旧拿订单表来举例,我们根据订单的数据来对订单表数据进行拆分。

还有我们说的数据库拆分,即将一个数据库拆分成多个数据库,也是这种情况,

然后在说完常见的拆分方法之后,我们接下来就来说一下分库分表过程中可能遇到的问题。

5. 📏分表字段怎么选择?

在分库分表的过程中,如果我们选择纵向拆分的形式,我们就需要用一个字段来进行分表,比如按照用户进行分表,按照时间进行分表、按照地区进行分表,这里面的用户、时间、地区就是我们常常说的分表字段

那么,在选择这个分表字段的时候,就要注意一个点,那就是要根据实际的业务情况来进行选择

比如我们要针对交易的订单数据进行分表的时候,我们可以选择的分表字段有很多,比如买家 ID、商家 ID、订单、时间、地区等,那我们应该如何进行选择呢?

这个时候我们就需要考虑一个问题,我们分表的时候,根据不同的字段进行分表,会不会导致一个问题,就是有的表数据量非常得大,有的表数据量反而非常得小,这个就是我们常常说的数据倾斜问题(热点数据) ,也是我们在分库分表的时候需要考虑的重要问题。(如下图所示)

数据倾斜举例

在说完数据倾斜的概念之后,我们来进行一个实践,还是拿我们最熟悉的电商系统来进行实操,然后还是拿订单表进行实际操作,我们这个时候有许多的特殊需求,比如按照月度进行汇总、按照地区进行汇总等,这个时候我们一般采用的分表方式一般是根据买家 ID 来进行分表,那是为什么呢,接下来我们来分析一下:

买家 & 卖家

首先肯定有人会有疑问,为什么我们在进行分库分表的时候,不按照卖家来进行分表呢?

因为我们知道,在一个大型的电商系统中,肯定有许多的买家和卖家,但是一个卖家可能会产生很多的订单,比如天猫、京东、拼多多等这种大型的电商平台,他们每天产生的订单量非常得大,如果按照卖家 ID 去进行划分的话,那么一个卖家可能有很多的订单都分到同一个表,然后有的卖家由于店铺不是很热门,其订单又比较少,这样就导致了数据倾斜问题的出现,如下图所示。

这个时候卖家 2 的数据就变成了热点数据,随着时间的增长,其数据量越来越大,这样就会导致这个卖家的所有操作都变得非常缓慢。

但是,如果我们用买家的 ID 做分表就不会出现这种情况,因为你一个买家总不可能数据量特别大,大到可以就数据买倾斜(富豪这种是另外一种情况,不在我们的考虑范围之内 )。

但是我们需要注意一个点,我们如果按照买家 ID 作为分表字段的话,我们只需要保证同一个买家的所有订单都在同一张表里面即可以了,不需要给每一个买家单独分配一张表

那要怎么实现了,这个时候我们可以利用哈希槽的思想,即我们再设计分表路由的时候,比如我们要分 1024 张表,我们就可以用卖家 ID 或者买家 ID 的 HashCode 对 1024 进行取模,结果肯定是 0000 - 1023 ,那么根据对应的编号存储到对应的分表中就可以了,如下图所示。

卖家查询怎么办?

这个时候肯定会来一个问题,如果我们根据买家的 ID 进行分表,那么卖家的查询要怎么处理,这不就意味着我们要进行跨表查询了吗?

首先,业务问题我们要在业务背景下进行讨论,那就是电商网站的订单查询一般有多少种高频的查询场景呢?

  1. 买家查询自己的订单
  2. 卖家查询自己的订单
  3. 平台客服查询用户的订单

分析完几种高频的查询情况之后,我们针对每种情况来进行分析:

  1. 买家查询自己的订单

这个时候我们已经根据买家 ID 进行了分表,那么买家来查询的时候,一定是携带买家 ID 过来的,我们直接与对应的表里面查询就可以了。

  1. 卖家查询自己的订单

卖家查询的话,其同样是带着卖家 ID 过来的,那么,我可以有一个基于 binlog、flink 等准实时的同步一张卖家维度的分表,这张表只用来查询,来解决卖家查询的问题。

我们现在来分析一下这种做法,其本质上就是一种用空间换时间的做法,但是这里可能有一个疑问,那就是同步一张卖家表,那不就又带来热点卖家的数据倾斜问题了吗

我们来分析一下,同步一张卖家维度的表,其本质上所有的写操作还是同步到买家表里面的,只不过需要准实时同步的方案同步到卖家表里面,也就是说,我们这个卖家表理论上来说是没有任何业务上的写操作的,只有读操作。

所以这个卖家库我们只需要有高性能的读操作就可以了,那这样的话我们就有很多种选择了,比如可以将卖家库部署到一些配置比较廉价的机器上面,或者直接不用 MySQL,用 HBase、PolarDB、Lindorm 等这些数据库就可以了,这些数据库虽然写入的效率不是很高,但是还是可以支持海量数据,从而提供高性能查询的。

还有一个点,就是针对热点卖家来说的,就是我们可以针对热点卖家,把热点卖家的订单按照一定的规则拆分到多个表中,因为只有读操作,没有写操作,所以拆分成多张表的时候也不需要考虑事务的问题。

  1. 平台客服查询用户的订单

上面说的两种情况都是针对有买家和卖家 ID 的情况,那如果没有买家和卖家的 ID 呢?用订单号直接查询怎么处理?这个时候就可以用到一种新的方法,叫做基因法

这种问题也有对应的解决方案,那就是在生成订单号的时候,我们一般会把分表结果编码到订单号中去,因此订单生成的时候一定是知道买家的 ID的,那么我们就可以把买家 ID 的路由结果比如 1023,作为一段固定的值放到订单号里面就可以了,这就是所谓的 “基因法”。

这样我们按照订单号进行查询的时候,解析出这段数字,我们直接根据对应的分表去进行查询就好了。

至于还有人问其他的查询,没有买卖家ID,也没订单号的,那其实就属于是低频查询或者非核心功能查询了,那就可以用ES等搜索引擎方式进行查询来解决了。

6. 🔅常见的分表算法有哪些?

在了解常见的分表算法之前,我们需要强调一个点,那么就是不管是哪种分表算法,我们需要明确一个前提,那就是同一个分表字段,其经过这个算法处理之后,其得到的结果是一致的,不可变的

一般情况下,我们对 对order表进行分表的时候,比如我们要分成128张表的话,那么得到的128表应该是:order_0000、order_0001、order_0002.....order_0126、order_0127

常见的分表算法有以下几种:

  1. 直接取模

在分库分表时,我们是事先可以知道要分成多少个库和多少张表的,所以,比较简单的就是取模的方式。

比如我们要分成 1024 张表,就和我们上面的方式一样,采用哈希槽的思想,来对 1024 进行镀膜,如果得到的结果是 0045,则我们只需要将数据放到 order_0045 这张表就可以了。

  1. 按照关键字

有的时候,我们可以按照关键字进行拆分,比如按照时间、或者地点什么的,将符合要求的数据单独放到某一个表中,如下图所示,根据地区分表就是比较常见的,不过需要考虑一个点,部分地区的数据过少可能到导致数据倾斜问题。

  1. Hash 取模

那如果分表字段不是数字类型,而是字符串类型怎么办呢?有一个办法就是哈希取模,就是先对这个分表字段取Hash,然后在再取模。

但是需要注意的是,Java中的hash方法得到的结果有可能是负数,需要考虑这种负数的情况。

  1. 一致性哈希

这个应该熟悉分布式缓存以及负载均衡策略的同学应该不会很陌生,虽然前面几种分表方式都比较不错,可以使得我们的数据均匀地分配,但是可能存在一个缺点,就是当你需要扩容进行二次分表的时候,这个时候你表的总数可能发生变化,那就需要重新计算哈希值了,这个时候就需要考虑到一个问题——数据迁移。

所以为了解决扩容的问题,我们这个时候可以采用一种新的方式进行分表,即一致性哈希的方式。

这里为了方便大家理解,专门补上一篇我写的关于一致性哈希的文章,如果之前没有了解的同学可以看看:

juejin.cn/post/729974…

一致性哈希可以按照常用的 Hash 算法来对对应的 key 映射到一个具有 个节点的空间中,形成一个顺时针首尾相接的圆形环。然后每次新加一台数据库服务器的时候,只有增加服务器的位置和逆时针方向第一台服务器直接的键值会受到影响。

7. 🏷️分表之后的全局 ID 如何保证全局唯一性?

前面我们说过,如果我们使用了分库分表的形式,就肯定会遇到一个问题,如何使得分布式系统中生成的主键 ID 符合唯一性的问题,因为我们在单表中可以采用数据库主键来做唯一的 ID,但是如果使用了分库分表的形式,那么多张单表中的自增主键就一定会发生冲突,那这样的话你生成的 ID 就不具备全局唯一性了。

那我们现在来剖析一下,有哪些生成全局唯一 ID 的方法:

UUID

很多人对于 UUID 可能并不陌生,其是可以做到全局唯一性的,而且生成方式也很简单,而且性能很高,不依赖于网络,可以利用 MySQL 系统自动生成,使用起来也比较方便。

但是我们通常不使用他作为唯一 ID,主要原因有两个:

  1. UUID 太长了,它有 32 位 16进制的数字,且字符串的查询效率也比较慢,在作为分布式 ID 查询的时候存在查询效率较低,不适合范围查询以及不方便展示等问题。
  2. 不具有业务性, 标准的 UUID 格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),一共32个字符,这个时候我们随便举一个例子,“340a3413-b8a2-c1e6-f922-895398142390”,对于这个字符串来说,很难看出其表达的含义,如果使用其作为全局唯一标识,在分布式系统中,很难进行问题的排查以及开发过程中的调试,所以我们一般不考虑 UUID 作为全局唯一标识。

基于某个单表做自增主键

多张单表生成的 ID 主键自增在分布式系统中可能发生冲突问题,这个时候可能有人会想到一个问题:

既然我多张表的自增主键可能产生冲突问题吧,那么我们如果都利用一张表生成主键不就可以了吗?

这种思路大致实现如下,就是我利用一个表来存放自增 ID ,然后所有的表在需要主键的时候来这张表里面取就可以了,这样做就可以实现全局唯一,还实现了自增效果,一举两得。

这样不否定,确实可以实现全局唯一而且还能实现自增效果,但是有一个问题,就是这张单表最终会成为了系统扩展的瓶颈,而且也存在单点问题,一旦这张表有一天突然寄了(挂了),那整个数据库的插入不就瘫痪了吗?

所以用这种方式也不行。

基于多个单表+步长做自增主键

针对以上基于这种问题,我们提出了一种新的解决方案,即基于多个单表,这样的话就可以有效地解决单表生成主键导致的单点问题了。

但是如何保证多张表里面生成的 ID 不重复呢?我们只需要实现以下这种方式就OK 了:

实例1生成的ID从1000开始,到1999结束。 实例2生成的ID从2000开始,到2999结束。 实例3生成的ID从3000开始,到3999结束。 实例4生成的ID从4000开始,到4999结束。

这样就可以很好地避免 ID 重复问题了,但是又有了一个新的问题,那就是第一个实例如果 ID 到了 1999 ,这个怎么办?那就重新生成一个起始值:

实例1生成的ID从5000开始,到5999结束。 实例2生成的ID从6000开始,到6999结束。 实例3生成的ID从7000开始,到7999结束。 实例4生成的ID从8000开始,到8999结束。

我们把步长设置为1000,确保每一个单表中的主键起始值都不一样,并且比当前的最大值相差1000就行了。

雪花算法

最后介绍的就是我们分布式 ID 生成策略的主角——雪花算法(标准名言:世界上不可能有两片相同的雪花)。

雪花算法是一种比较常见的分布式 ID 的生成方式,它具有全局唯一、递增、高可用的特点

雪花算法是由 Twitter 研发的一种分布式 ID 生成算法,它可以生成全局唯一且递增的 ID。它的核心思想就是将一个 64位的ID划分成多个部分,每个部分都有不同的含义,包括时间戳、数据中心标识、机器标识和序列号等。

一般来说,雪花算法生成的 ID 有以下几个部分:

  1. 符号位(1bit):预留的符号位,始终为0,占用1位。
  2. 时间戳(41bit):精确到毫秒级别,41位的时间戳可以容纳的毫秒数是2的41次幂,一年所使用的毫秒数是:365 * 24 * 60 * 60 * 1000,算下来可以使用69年。
  3. 数据中心标识(5bit):可以用来区分不同的数据中心。
  4. 机器标识(5bit):可以用来区分不同的机器。
  5. 序列号(12bit):每个节点每毫秒 0 开始不断叠加,最多可以叠加到 4095 ,因此其一共可以生成4096个不同的序列号。

综上所述,一个雪花算法在同一毫秒内,最多可以生成 32 x 32 x 4096 = 4194304 个唯一的 ID,所以,用雪花算法来作为分布式系统的 ID 生成策略是比较合适的,所以,其同样适用于分库分表的全局 ID 生成的场景。

8. 📓分库分表后怎么进行分页查询?

我们再分库分表常见的问题那里说过,我们在做完分库分表之后,数据会散落在不同的数据库当中,这个时候跨多个库的分页查询、以及排序等都会非常麻烦,接下来我们就来说一下常见方法:

将表放到内存中,然后进行扫表查询

当分的库不多的时候,我们可以通过扫表的方式将多个库中的数据读取出来,在内存中进行分页和排序。

比如我要查询limit 100,100 的话,有三个库,那我就分别到这三个库中把0 - 200之间的数据都取回来,然后再在内存中给他们排序,之后,再取出第100-200之间的数据。

这种做法可以是可以,但是你会发现一件事,那就是这种做法非常麻烦,而且随着你数据量越来越大,再加上如果分页太多的话,这种方法可能没有办法满足需求

实用做法

虽然网上有很多的做法,比如全局视野法、二次查询法、业务折衷法等很多种做法,但是我看了一下都没那么好理解,o(╥﹏╥)o,而且不是那么得实用,都是有前提条件限制的,所以,我们这里使用几种实用的方法,会更好理解一点,这几个方法来源于我之前大二时候看到的一个大神的写法,当时就记录下来了,不过由于当时只是记录下来,忘记记录大神的名字了,在这里膜拜一手。

这里还是拿我们最熟悉的电商系统来分析,然后依旧是拿订单的分库分表的案例来讲,我们还是用买家 ID 进行分表:

shardingkey 查询(分页主件查询)

一般来说,买家的订单查询是最高频的,而对于买家来说,查询的时候天然就是可以带买家ID的,所以就可以路由到单个库中进行分页以及排序了。

非 shardingkey 的关键查询

我们之前分析过电商网站,电商网站上不仅有买家,还有卖家,他们的查询也很高频,该怎么做呢?

之前我们说过,针对卖家查询,一般会采用空间换时间的方案,同步出一张按照卖家维度做分表的表来,同步的过程中一般是使用canal基于bin log 做自动同步。虽然这种情况下可能存在秒级的延迟,但是一般业务上来说都是可以接受的。

也就是说,当一条订单创建出来之后,会在买家表创建一条记录,以买家ID作为分表字段,同时,也会在卖家表创建一条记录出来,用卖家ID进行分表。并且这张卖家表不会做任何写操作,只提供查询服务,完全可以用一些比较廉价的机子去部署数据库实例。这样,卖家的分页等查询就可以直连卖家表做查询了。

非 shardingkey 的复杂查询

那除了买家、卖家以外,其他的查询怎么办呢?

一般来说,大厂用的比较多的方案就是使用分布式数据仓库来实现,也就是说我们会把这些数据同步到像TiDB、PolarDB、HBase等这些数据库中,或者同步到ES中,然后在这些数据库中做数据的聚合查询。

9. 🔭分库分表常用技术选型

分库分表的技术方案从总体上来说,可以分为两大类:应用层依赖类中间件中间层代理类中间件

首先我们来说一下这两种中间件之间的区别:

应用层依赖类中间件

  • 定义:这类分库分表中间件的特点就是和应用强耦合,需要应用显示依赖相应的jar包(以Java为例),比如知名的TDDL、当当开源的sharding-jdbc、蘑菇街的TSharding、携程开源的 Ctrip-DAL 等。
  • 基本思路:重新实现JDBC的API,通过重新实现DataSource、PrepareStatement等操作数据库的接口,让应用层在基本(注意:这里用了基本)不改变业务代码的情况下透明地实现分库分表的能力。中间件给上层应用提供熟悉的JDBC API,内部通过sql解析、sql重写、sql路由等一系列的准备工作获取真正可执行的sql,然后底层再按照传统的方法(比如数据库连接池)获取物理连接来执行sql,最后把数据结果合并处理成ResultSet返回给应用层。

中间层代理类中间件

  • 定义:这类分库分表中间件的核心原理是在应用和数据库的连接之间搭起一个代理层。
  • 基本思路: 上层应用以标准的MySQL协议来连接代理层,然后代理层负责转发请求到底层的MySQL物理实例,这种方式对应用只有一个要求,就是只要用MySQL协议来通信即可,所以用MySQL Workbench这种纯的客户端都可以直接连接你的分布式数据库,自然也天然支持所有的编程语言。

这里我们就看几个最常用的,即 SahrdingSphere、TDDL、MyCat

ShardingSphere

开源地址:shardingsphere.apache.org

ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。

Sharding-JDBC

定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。

  • 适用于任何基于Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  • 基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  • 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL。

Sharding-Proxy

定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。 目前先提供MySQL版本,它可以使用任何兼容MySQL协议的访问客户端(如:MySQL Command Client, MySQL Workbench等)操作数据,对DBA更加友好。

  • 向应用程序完全透明,可直接当做MySQL使用。
  • 适用于任何兼容MySQL协议的客户端。

Sharding-Sidecar(TBD)

定位为Kubernetes或Mesos的云原生数据库代理,以DaemonSet的形式代理所有对数据库的访问。 通过无中心、零侵入的方案提供与数据库交互的的啮合层,即Database Mesh,又可称数据网格。

Database Mesh的关注重点在于如何将分布式的数据访问应用与数据库有机串联起来,它更加关注的是交互,是将杂乱无章的应用与数据库之间的交互有效的梳理。使用Database Mesh,访问数据库的应用和数据库终将形成一个巨大的网格体系,应用和数据库只需在网格体系中对号入座即可,它们都是被啮合层所治理的对象。

ShardingSphere功能列表

数据分片【Sharding-JDBC】

  • 分库 & 分表
  • 读写分离
  • 分布式主键

分布式事务(Doing)【Sharding-Proxy】

  • XA强一致事务
  • 柔性事务

数据库治理【Sharding-Sidecar(TBD)】

  • 配置动态化
  • 熔断 & 禁用
  • 调用链路追踪
  • 弹性伸缩 (Planning)

ShardingSphere混合架构

  • Sharding-JDBC采用无中心化架构,适用于Java开发的高性能的轻量级OLTP应用;
  • Sharding-Proxy提供静态入口以及异构语言的支持,适用于OLAP应用以及对分片数据库进行管理和运维的场景。

OLTP与OLAP的介绍
数据处理大致可以分成两大类:联机事务处理OLTP(on-line transaction processing)联机分析处理OLAP(On-Line Analytical Processing)
OLTP是传统的关系型数据库的主要应用,主要是基本的、日常的事务处理,例如银行交易。
OLAP是数据仓库系统的主要应用,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。
OLTP 系统强调数据库内存效率,强调内存各种指标的命令率,强调绑定变量,强调并发操作;
OLAP 系统则强调数据分析,强调SQL执行市场,强调磁盘I/O,强调分区等。

ShardingSphere是多接入端共同组成的生态圈。 通过混合使用Sharding-JDBC和Sharding-Proxy,并采用同一注册中心统一配置分片策略,能够灵活的搭建适用于各种场景的应用系统,架构师可以更加自由的调整适合于当前业务的最佳系统架构。

TDDL

开源地址:github.com/alibaba/tb_…

TDDL 是淘宝开源的一个用于访问数据库的中间件, 它集成了分库分表, 读写分离,权重调配,动态数据源配置等功能。封装 jdbc 的 DataSource给用户提供统一的基于客户端的使用。

MyCat

开源地址:github.com/MyCATApache…

MyCat是一个开源的分布式数据库系统,是一个实现了MySQL协议的服务器,前端用户可以把它看作是一个数据库代理,用MySQL客户端工具和命令行访问,而其后端可以用MySQL原生协议与多个MySQL服务器通信,也可以用JDBC协议与大多数主流数据库服务器通信,其核心功能是分表分库,即将一个大表水平分割为N个小表,存储在后端MySQL服务器里或者其他数据库里

在技术实现上除了和应用层依赖类中间件基本相似外,代理类的分库分表产品必须实现标准的MySQL协议,某种意义上讲数据库代理层转发的就是MySQL协议请求,就像Nginx转发的是Http协议请求

上述无论哪种类型的产品,除了实现分库分表这一主要功能外,都会额外实现一些其他很有实用价值的功能,比如读写分离、负载均衡等。

参考文章:

  1. 分库分表最全中间件对比
  2. 分库?分表?分库分表?
  3. 数据库的分库分表,可能真的要退出历史舞台了!
  4. 数据库分表分库到底应该怎么查询?
  5. 深入Sharding-JDBC分库分表从入门到精通
  6. 基因法实现亿级数据分库分表
  7. 电商公司表数据增长过快,用基因法解决分库分表难题!
  8. 数据量多大的时候要开始分表分库?
  9. 阿里二面:为什么要分库分表?