体验一下面试官对于分库分表这个事儿的一个连环炮

770 阅读14分钟

1. 为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的?

面试官心里分析

其实这块肯定是扯到高并发了,因为分库分表一定是为了支撑高并发,数据量大两个问题的。而且现在说实话,尤其是互联网类的公司面试,基本都会这么来一下,分库分表如此普遍的技术问题,不问实在是不行,而如果你不知道那也实在是说不过去!

面试题剖析

(1)为什么分库分表?(设计高并发系统的时候,数据库层面该如何设计?)

  1. 部署MySQL单机,扛不住并发
  2. MySQL单机磁盘容量快满了
  3. MySQL单表数据量太大了,SQL越跑越慢

说白了,分库分表是两回事,大家别搞混了,可能是光分库不分表,也可能是光分表不分库,都有可能。我先给大家抛出来一个场景。

image.png

image.png

比如你单表都几千万数据了,你确定你能抗住么?绝对不行,单表数据量太大了,会极大影响你的sql执行的性能,到了后面你的sql可能跑的很慢了,一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一点,你就得分表了。

分表是啥意思?就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户id来分表,将一个用户的数据就放到一个表中,然后操作的时候你对一个用户就操作那个表就好了,这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在200万以内。

分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒1000左右,不要太大,那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库就好了。

这就是所谓的分库分表,为啥要分库分表?你明白了吗?

(2)用过哪些数据库中间件?不同的分库分表中间件都有什么优点和缺点?

先看看数据库中间件的作用

image.png

这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。

比较常见的包括:cobar、TDDL、sharding-jdbc、atlas、mycat

  • cobar:阿里b2b团队开发和开源的,属于proxy层独立部署的)方案。早些年还可以用,但是最近几年都没更新了,基本没啥人用了,差不多算是被抛弃的状态吧。而且不支持读写分离,存储过程、跨库join和分页等操作。
  • TDDL:淘宝团队开发的,属于client层方案。不支持join、多表查询等语法,就是基本的crud语法是ok,但是支持读写分离。目前使用的也不多,因为还依赖淘宝的diamond配置管理系统。
  • atlas:360开源的,属于proxy层方案,以前是有一些公司在用,但是确实有一个很大的问题就是社区维护好多年没更新了,目前使用的也不多。
  • sharding-jdbc:当当开源的,属于client层直接引用jar,和业务系统在一块的)方案,确实之前用的还比较多一些,因为sql语法支持也比较多,没有太多限制,而且目前推出到了4的版本,支持分库分表、读写分离、分布式id生成、柔性事务(尽最大努力送达型事务、TCC事务)。而且确实之前使用的公司会比较多一些,社区也在一直开发和维护,还算是比较活跃,是一个可以选择的方案。
  • mycat:基于cobar改造的,属于proxy层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃。

综上所述:建议使用的就是sharding-jdbc和mycat,sharding-jdbc这种client层方案的优点在于不用部署,运维成本低,但是如果遇到升级啥的需要各个系统都重新升级版本在发布,mycat这种proxy层方案的缺点在于需要部署,自己及运维一套中间件,运维成本高,但是好处在于对各个项目是透明的,如果遇到升级之类的都是自己中间件哪里搞就行。

个人建议中小型公司选用sharding-jdbc,client层方案轻便,不需要额外增派人手,维护成本低,大型公司,可以选用mycat,人员充足,系统和项目很庞大,方便系统升级。

(3)你们具体是如何对数据库进行垂直拆分或水平拆分的?

先对库的数量取模 然后对表的数量取模 最终确定数据落库落表的位置:

image.png

水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据,水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。

垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表,或者是多个库上去,每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较小的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问评率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

还有表层面的拆分,就是分表,将一个表变成N个表,就是让每个表的数据量控制在一定范围内,保证SQL的性能。否则单表数据量越大,SQL性能就越差。一般是200万行左右,不要太多,但是也得看具体你怎么操作,也可能是500万,或者是100万。你的SQL越复杂,就最好让单表行数越少。

好了无论是分库了还是分表了,上面说的那些中间件都是可以支持的,就是基本上那些中间件可以做到你分库分表之后,中间件可以根据你指定的某个字段值,比如说userid,自动路由到对应的库上去,然后在自动路由到对应的表里去。

你就得考虑一下,你的项目里该如何分库分表?一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;水平拆分,你可以说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了;按什么字段来拆,你自己想好,分表,你考虑一下,你如果哪怕是拆到每个库里去,并发和容量都ok了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。

而且这儿还有两种分库分表的方式,一种是按照range分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少使用,因为容易产生热点问题,大量的流量都打在最新的数据上了,或者是按照某个字段hash一下均匀分散,这个较为常用。

range来分,好处在于说,后面扩容的时候,就很容易,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了,缺点,但是大部分的请求,都是访问最新的数据,实际生产用range,要看场景,你的用户不是仅仅访问最新的数据,而是均匀的访问现在的数据以及历史的数据

hash分法,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦

2. 现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态(不停机)切换到分库分表?

面试官心里分析

你看看,你现在已经明白了为啥要分库分表,你也知道常用的分库分表中间件了,你也设计好你们如何分库分表的方案了(水平拆分、垂直拆分、分表),那么问题来了,你接下来该怎么把你那个单库单表的系统给迁移到分库分表上去?

所以这是一环扣一环的,就是看你有没有全流程经历过这个过程

建议自己找找sharding-jdbc和mycat的例子跑一跑

面试题剖析

这个其实从low到高大上有好几种方案,我们都玩过,我给你说一下

(1)长时间停机分库分表

半夜停机,临时写个程序,把老库的数据分别写到几个新库,修改项目配置

(2)双写迁移方案

image.png

image.png

标准的规范化的表设计里面,都会包含一个最后修改的时间字段,判断一下,分库分表中是否存在?不存在,直接写入,如果存在,比较两个数据的时间戳,如果单库单表读到的数据,比分库分表中读到的数据要新,那么就覆盖分库分表中的数据,绝对不允许旧数据覆盖新数据,只能新数据覆盖旧数据

迁移一轮,600万数据迁移完这么一轮,此时就需要执行一次检查,检查单库单表中的数据,跟分库分表中的数据,是不是一模一样,如果是一模一样的话,那么就ok了,迁移就成功了,如果有差别的话,针对不一样的数据,从单库单表中读取出来,看看是否需要再次覆盖分库分表中的数据

依次循环往复,这个后台程序你可能得跑个好几天,到了凌晨的时候,几乎没有什么新的数据进来了,此时一般来说老库和新库的数据都会变成是一样的

最后一步,就是修改系统的代码,将写单库单表的代码给删除掉,仅仅写分库分表,再次部署一下,就ok了

3. 如何设计可以动态扩容缩容的分库分表方案?

面试官心里分析

(1)选择一个数据库中间件,调研、学习、测试

(2)设计你的分库分表的一个方案,你要分成多少个库,每个库分成多少个表,例如3个库 4个表

(3)基于选择好的数据库中间件,以及在测试环境建立好的分库分表的环境,然后测试一下能否正常进行分库分表的读写

(4)完成单库单表到分库分表的迁移,双写方案

(5)线上系统开始基于分库分表对外提供服务

(6)扩容了,扩容成6个库,每个库需要12个表,你怎么来增加更多的库和表呢?

面试题剖析

(1)停机扩容

这个方案就跟停机迁移一样,步骤几乎一致,唯一的一点就是那个导数据的工具,是把现有库表的数据抽出来慢慢导入新的库表里去,但是最好别这么玩,有点不靠谱,因为既然分库分表就说明数据量实在是太大了,可能多达几亿条甚至几十亿,你这么玩会出问题的。

(2)优化后的方案

一开始上来就是32个库,每个库32张表,1024张表,为啥要1024呢?就是不用改变表的数量,你只是改变数据库服务器的一个数量,不用改变数据库和表的数量

我可以告诉各位同学说,这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支持还是数据量支撑都没问题。

image.png

这么扩,有一个好处,就是你实际上是不需要将数据读出来,再次分发,直接迁移库的方式,对dba来说,他们有对应的工具可以使用,可以方便快捷很多。

最多可以扩到32个数据库服务器,每个数据库服务器上放一个库,这个库有32张表

按照下图的思路取模,可以让数据更加均匀的扩散

image.png

总结:

image.png

4. 分库分表之后,id主键如何处理?

面试官心里分析

其实这是分库分表之后你必然要面对的一个问题,就是id咋生成?因为要是分成多个表之后,每个表都是从1开始累加,那肯定不对啊,需要一个全局唯一的id来支持。所以这都是你实际生产环境中必须考虑的问题

面试题剖析

(1)数据库自增id

image.png

这个就是说你的系统里每次得到一个id,都是往一个库的一个表插入一条没什么义务含义的数据,然后获取一个数据库自增的一个id,拿到这个id之后在往对应的分库分表里去写入。

这个方案的好处就是方便简单,谁都会用;缺点就是单库生成自增id,要是高并发的话就会有瓶颈,如果你要是硬要改进一下,那么就专门开一个服务出来,这个服务每次就拿当前id最大值,然后自己递增几个id,一次性返回一批id,然后再把当前最大id值修改成递增几个id之后的一最大个值;但是无论怎么说都是基于单个数据库。

适合场景:你分库分表就俩原因,要么就是单库并发太高,要不就是单库数据量太大,磁盘不够了,除非是你并发不高,由于单库数据量太大导致分库分表的扩容,你可以使用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生产自增主键即可。

(2)uuid

好处是本地生成,不要基于数据库来了,不好之处在于,uuid太长了,作为主键性能太差了,不适合用于主键

适合的场景:如果你是要随机生成个什么文件名了,编号之类的,你可以用uuid,但是作为主键是不能用uuid的。

(3)获取系统当前时间戳

这个就是获取当前时间戳,但是问题来了,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的,基本不用考虑

使用场景:一般如果用这个方案,是将当前实践跟很多其他业务的字段拼接起来,作为一个id,如果业务上你觉得可以接受,那么也是可以的。

(4)snowflake算法

image.png

image.png

有兴趣的可以看看源码的讲解,加深理解

image.png

image.png