在很多业务里,分库分表键都是根据主要查询筛选出来的,那么不怎么重要的查询怎么解决呢?
比如电商场景下,订单都是按照买家ID来分库分表的,那么商家该怎么查找订单呢?或是买家找客服,客服要找到对应的订单,又该怎么找?
分库分表键的选择
选择进行分库分表的业务字段,有的时候会有多个字段,如何选择合适的字段呢?
关键点就是 根据查询来选择 。例如在订单里面,最常见的是按照买家来进行分库分表,理由是买家查询自己的订单是最主要的场景,这样收益最大。这个完全是业务驱动的,最常用的分库分表键有主键、外键、索引列;如果是范围分库分表,那么日期类型的列也很常用。
重试方案
设置一个重试方案要考虑3个方面的内容:
- 重试次数:无限次重试意义并不大
- 重试间隔:等间隔重试或指数退避重试。后者指的是重试间隔的时间在增长,一般是两倍增长,面试的时候可以设计一些更加灵活的重试,比如最开始按照两倍增长,再按照50%增长,最后保持最大重试间隔不断重试
- 是否允许跨进程重试:在进程A里触发了重试,但是重试一次后,是否可以在进程B上重试第二次?对应到分布式环境上,意味着是否可以在不同的机器上重试。
理论上说,重试是为了避开前一次失败的原因,比如因为偶发的网络抖动失败。设计指数退避的重试策略的原因也很简单,通过不断延长重试时间间隔,有更大的概率避开引发失败的因素。
面试准备
- 分库分表的主键生成策略
- 如果使用了后续提到的引入中间表、二次分库分表和使用其他中间件支持查询中的任何一个方案,就需要搞清楚数据同步的方案;换句话说,如果数据不一致,多久会发现,最终要多久才能达成一致。
简历里或面试里提到分库分表方案设计的时候,主动提起是如何解决这个问题的?
面试常见问题引导
- 问到了主键生成策略,那么你可以说主键生成会影响分库分表的中间表设计。
- 面试官问到了从其他维度怎么查询数据的问题。例如在订单这里问到了客服怎么查、运营怎么查等。
- 面试官问到了数据同步和数据一致性,你可以用这里面谈到的场景来展示你是如何解决这些问题的。
- 面试官问到了如何选择合适的分库分表键,那么你就可以强调非分库分表键的查询更加复杂,需要额外的支持。
基本思路
面试官都是直接问类似的问题,比如介绍了分库分表方案后,提到订单表是按照买家ID来进行分库分表的之后,会顺势问如果卖家要查询ID应该怎么办?
可以按照这个模板来介绍不同的方案
接下来按照这些关键词一个一个地问
主键生成策略
有一种主键生成的策略是在主键里面带上分库分表的列,如果能够拿到主键,就应该知道去哪个数据库上的哪个数据表里查找。
比如:在订单ID里带上了买家ID,那么在根据订单ID来查询数据的时候,就可以通过订单ID来判断订单的数据在哪个库哪个表里。
这是一种很优雅的解决方案**,既不需要任何第三方工具的帮助,也不需要额外存储数据**。但是这个方案只能解决一部分问题,而且大多数时候主键都不是采用这种策略生成的,只能考虑其他方案了,比如引入中间表。
引入中间表
如果想支持按照卖家来搜索,可以引入一个中间表,记录了ID、卖家ID和买家ID三个数据。
当然也可以考虑把买家ID换成目标库和目标表,这样就省去了根据买家ID来定位目标库和目标表的步骤。
查询的基本步骤也很清晰:
- 先在中间表里根据卖家ID找到想要的订单ID和买家ID
- 再根据买家ID和订单号找到具体的订单数据
关键词就是中间表:
这个基本方案可以从两个角度刷亮点:
第一个角度是结合主键生成策略,优化中间表的设计
第二个角度是讨论中间表的缺陷,最大的缺陷是性能瓶颈
中间表最让人害怕的就是写瓶颈,可以考虑提供一个解决方案:
中间表还有两个明显的缺陷:难以适应灵活多变的查询场景,还有数据一致性问题。
进一步思考,中间表要想解决写瓶颈,是不是也可以分库分表?
二次分库分表
二次分库分表指复制出来一份数据,然后尝试再进行分库分表。所以你的系统里会有两份数据,分别按照不同的分库分表规则来存储。比如卖家需要查询订单,那么可以再一次按照卖家ID来进行分库分表。
数据复制一份的问题解决起来也很简单:只需要复制关键表以及关键表的关键字段就可以了。部分表是不需要复制的,比如订单详情表完全不需要复制,在拿到订单ID之后再次查询订单详情表
即使复制表,也不是所有的字段都需要复制,一些BLOB、TEXT字段占用存储空间多,还不会出现在查询条件里,根本不需要复制。如果真的需要这些字段,可以拿主键和分库分表键二次查询。
这里的查询也分为了两部,但是要尽量做到大部分查询只查卖家库,只有少部分查询需要回归到买家库。
关键词是减轻存储压力
在这里可以进一步讨论两次查询引入的问题,以及可行的优化方案
使用其他中间件
为了支持复杂多样的查询,可以尝试使用别的中间件,比如Elasticsearch。在引入Elasticsearch的时候也可以采用引入中间件方案中的一个优化措施,即只同步部分和搜索相关的字段。
如果你选了同步部分数据到 Elasticsearch,那么你最终就会面临一个问题:总有一些业务的查询,你完全没办法支持。那这个时候你就只剩下最后一个手段了:广播。
广播
如果不能断定数据可能出现在哪一张表上,那么就直接把全部表上都查询一遍。
当卖家想要知道自己究竟卖了多少单的时候,就可以在所有的表上都问一遍,汇总之后就是卖家的所有订单
这种做法的缺陷:对数据库的压力太大。只有兜底的时候才使用。
引入中间表和二次分库分表
实际上可以理解为二次分库分表是中间表的升级加强版
- 中间表是性能瓶颈,害怕维护写频繁的字段;二次分库分表没有这种担忧
- 中间表本身的字段会很少,往往需要回归原表再次查询数据
- 二次分库分表成本要更高,因为需要复制更多的字段
一般来说:优先考虑使用中间表,其次考虑只复制部分数据的二次分库分表方案,逼得不得已再考虑全量复制数据的二次分库分表方案
数据同步问题
引入中间表、二次分库分表和使用其他中间件三个解决方案里,都面临同样一个问题:如何进行数据同步?
一般有两种数据同步的思路:
- 双写:在写入源数据表的时候,同时写到另一个地方。可以通过改造ORM或分库分表中间件达成。
- 利用 Canal 之类的框架监听 binlog,然后异步地把数据库同步到其他地方。
不管是双写,还是监控 binlog,都绕不开失败这个话题。那失败的时候怎么办呢?**无非就是各种重试,在重试都失败之后,就人手工介入处理。**在实践中,双写方案用得不多。高端一点的做法就是在重试失败之后,加上一个异步修复程序进一步尝试修复。如果修复程序本身也失败了,那确确实实就只能人手工介入了。这些内容之前我反复提到过,你需要记住。
亮点方案
在分库分表之后,为了充分满足不同情况下的查询需求,我们公司综合使用了三种方案:引入中间表、二次分库分表和 Elasticsearch。对于卖家查询来说,我们直接复制了一份数据,按照卖家 ID 分库分表。对于一些复杂的查询来说,就是利用 Elasticsearch。还有一些查询是通过建立中间表来满足,比如说商品 ID 和订单 ID 的映射关系。
数据同步方案是使用监听 binlog 的方案。买家库插入数据之后,就会同步一份到卖家库和 Elasticsearch 上。这个过程是有可能失败的,那么在失败之后会有重试机制,如果重试都失败了,那么就只能人手工介入处理了。
这个架构里的另一个问题是:卖家库的数据需要反向同步到买家库吗?
第一个回答是:如果允许卖家修改卖家库的数据,就需要反向同步,架构变成了下图。
这里提到了数据一致性问题更加严重,这也是为了引出第二个回答,就是除了买家库,其他库都是只读的。