2025-12-15
面试与规划
0

目录

为什么分库分表?
分库分表所带来的问题?

在面试中回答过多次分库分表的问题以后,经过几次复盘,我对分库分表的内容已然掌握的炉火纯青,接下来我将根据实际的业务场景,系统性地总结分库分表的回答思路与个人思考的解决方案。

为什么分库分表?

在回答分库分表的问题以前,你是否清晰的了解到你们业务中分库分表的目的?需要以结果为导向,描述业务场景中遇到的问题,以及通过分库分表解决的是哪些问题。

image.png

首先我们需要描述下业务的背景,以及遇到的问题,例如我当前的业务场景如下:

我们主要是做光伏电站建设的,每个电站下会有 12 个逆变器,每个逆变器在光照后会进行发电的工作,每隔 5分钟 会传输一条发电量数据,一个逆变器平均每天产生 120 条发电量数据,一年可到到 4w 多条数据。

我们公司整体规划的目标是第一年建站 2w,第二年到 10w 个电站,由于电站的数量是一个持续增长的过程,很快我们的数据量达到几千万,查询电站下逆变器的发电量信息的性能急剧下降,为此我们采用了分库分表。

描述完场景,我们来引用下分库分表解决了哪些问题

分库分表主要解决了 mysql 读写分离下,写入的性能瓶颈问题,提高系统的吞吐量,同时也解决了单表下数据量存储过大,从而导致的查询性能缓慢的问题。基于我们的业务场景,此处也是为了解决海量数据存储的问题。

接下来结合业务场景,我们来考虑拆分的维度,以及分片数量的考量

分库分表的拆分维度主要分为垂直分库、垂直分表、水平分库、水平分表4个维度,根据我们的数据量上的预估,在3年内会达到 百亿,而一条数据占用 1.6K 左右,从数据量与内存占用的角度,我们拆分的维度为 水平分库水平分表,拆分为 8 个库,每个库下 128 张表,每张表可平均存储千万数据。

分库分表后,我们需要结合业务来进行分片键的抉择

从业务模块的角度进行考虑,我们是展示的是电站分页信息,详情中展示所有逆变器信息,每个逆变器下展示近期的发电量折线图。整体关系为 电站 1 → n 逆变器 1 → n 发电量,基于此场景我们选择 电站ID(psId) 作为分库的分片键,选择 逆变器ID(inveterId) 作为分表的分片键,这样保证了查询同一电站下的逆变器数据在同一个数据库中,同一个逆变器的发电量数据都在同一个表当中,提升查询的效率。

选择了分片键,我们需要考虑如何将数据更均匀的分布到表中,如何选择分片的路由算法,与分片键组成 分片策略

其实分片算法有很多,其核心保障均分分配,防止数据倾斜问题。因此分表算法不需要太精密,太复杂,只需要能实现均分分配就行了。弄得太复杂的算法,反而肉眼无法识别,算不明白,这不是很麻烦么。

所以一般就是 取模算法,而我们的业务也是基于取模算法,对电站ID取模,对逆变器ID取模。

到这我们的业务场景已经描述清楚了,接下来就到了面试官的提问环节,大体的方向就是基于我们设计的方案实现,以及分库分表所带来的问题。

分库分表所带来的问题?

分库分表看似美好,其实也带来了很多的分布式问题。

image.png

接下来我将总结一下面试官可能会问到的问题。

你们分库分表后是如何保证唯一性ID不重复的?

分库分表以后,分布式序列ID就需要我们自行维护。默认有 UUID、雪花算法两种方案,UUID 基本我们就不考虑,毕竟不符合我们的B+树底层存储结构,也容易造成页分裂,索引碎片化等问题。常用的就是雪花算法,也可以自定义实现,例如结合 Leaf 的号段模式,实现自增的主键ID。

而我们此处采用的是数据库自增ID,根据我们的业务场景,此处的ID并不存在任何意义,所以使用默认的自增主键即可。

采用取模算法,如果扩容是如何保障数据的均匀分布的

取模算法是不支持动态伸缩的,所以说需要需要扩容,我们需要采用 一致性哈希 的算法作为路由算法,一致性哈希算法的实现原理就是通过一个 0 - 2^32 长度的哈希环,类似于循环数组,通过对物理节点进行哈希取模,映射到哈希环中,通过对分片键进行哈希取模,到哈希环中顺时针查找到物理节点,则为存储的节点,本质就是 路由寻址 的方式。通过扩容后新增物理节点映射到哈希环上,将部分指向物理节点的数据,改指向最近的新节点即可。

有没有遇到数据倾斜的问题?如何解决的

对于数据分布不均的问题,一致性哈希 算法中,采用虚拟节点的形式,一个物理节点下有多个虚拟节点,映射到哈希中,分片键哈希取模映射到哈希环时,寻址找到虚拟节点时,最终会路由到屋里节点上。虚拟节点越多,数据分布的也更均匀。

分库分表后,排序、遍历、分组、聚合等查询是如何实现的?

像我们使用 ShardingJDBC 中,是通过 执行引擎 进行实现的,最核心的是 归并引擎, 从结构划分,可分为流式归并内存归并装饰者归并。像流式归并中,是逐条从结果集中获取正确的数据,遍历、排序以及流式分组都属于流式归并的一种。而内存归并,则是将所有数据加载到内存中,然后进行分组,聚合封装结果集进行返回。而装饰者归并是在流式归并和内存归并的基础上,进行统一的功能增强,主要有分页归并和聚合归并2种处理。

核心就是数据库的结果集是逐条返回的,无需一次性加载到内存中。

分库分表后,分页的多条件查询如何实现?深度分页是如何处理的?

分页的条件查询,基本分为2种实现,简单查询复杂查询

如果是简单查询,例如我们想根据订单号(一个订单号就是一个电站,也是一个psId)查询所有分片数据,可以采用基因法,在订单号的设计之初,携带上基因分子,这个基因分子可以是分片键也就是逆变器二进制的后7位(分表数量是128,2的7次方,取模128即可获取到路由表),也可以直接是取模数(后四位类似 0128),在通过订单号查询时,即可获取到分片表,进行直接路由查询。

如果是复杂查询,可以结合ES,构建复杂查询条件的 索引结构,包含主键的ID,在通过ES进行复杂条件查询后,返回其ID,然后通过ID直接查询数据库表即可。

对于深度分页,为了避免加载大量数据到内存中,我们采用 select * from tablename where id > 999999 limit 10; 的方式,结合ES深度分页,最佳方案也是通过 Search After 实现, 原理与上述sql类似,通过记录上个分页排序值,继续查找下一页的数据,但是此原理实现的方案有一个缺点,就是不允许跳页。

实现原理

分库分表中的实现也是通过 执行引擎 实现的,结构上采用 装饰者归并

跨库事务的一致性是如何保障的

其实跨库的事务实现,也就是分布式事务的一种处理,ShardingJDBC 中提供了基于 XA 协议的两阶段事务 和 基于 Seata 的柔性事务 两种方式,其本质就是分布式事务中 CPAP + BASE 的抉择。

开始描述分布式事务的内容,从CAP原理 + BASE理论,到 DTP 全局事务模型 与 XA 的两阶段提交,以及具体实现 2PC;再到最终一致性的各种实现方案。

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!