分库分表经典15连问

前言

大家好,我是田螺。我们去面试的时候,几乎都会被问到分库分表。田螺哥整理了分库分表的15道经典面试题,大家看完肯定会有帮助的。

1. 我们为什么需要分库分表

在分库分表之前,就需要考虑为什么需要拆分。我们做一件事,肯定是有充分理由的。所以得想好分库分表的理由是什么。我们现在就从两个维度去思考它,为什么要分库?为什么要分表?

1.1 为什么要分库

如果业务量剧增,数据库可能会出现性能瓶颈,这时候我们就需要考虑拆分数据库。从这两方面来看:

  • 磁盘存储

业务量剧增,MySQL单机磁盘容量会撑爆,拆成多个数据库,磁盘使用率大大降低。

  • 并发连接支撑

我们知道数据库连接数是有限的。在高并发的场景下,大量请求访问数据库,MySQL单机是扛不住的!高并发场景下,会出现too many connections报错。

当前非常火的微服务架构出现,就是为了应对高并发。它把订单、用户、商品等不同模块,拆分成多个应用,并且把单个数据库也拆分成多个不同功能模块的数据库(订单库、用户库、商品库),以分担读写压力。

1.2 为什么要分表

假如你的单表数据量非常大,存储和查询的性能就会遇到瓶颈了,如果你做了很多优化之后还是无法提升效率的时候,就需要考虑做分表了。一般千万级别数据量,就需要分表。

这是因为即使SQL命中了索引,如果表的数据量超过一千万的话,查询也是会明显变慢的。这是因为索引一般是B+树结构,数据千万级别的话,B+树的高度会增高,查询就变慢啦。MySQL的B+树的高度怎么计算的呢?跟大家复习一下:

InnoDB存储引擎小储存单元是页,一页大小就是16k。B+树叶子存的是数据,内部节点存的是键值+指针。索引组织表通过非叶子节点的二分查找法以及指针确定数据在哪个页中,进而再去数据页中找到需要的数据,B+树结构图如下:

假设B+树的高度为2的话,即有一个根结点和若干个叶子结点。这棵B+树的存放总记录数为=根结点指针数*单个叶子节点记录行数。

如果一行记录的数据大小为1k,那么单个叶子节点可以存的记录数  =16k/1k =16. 非叶子节点内存放多少指针呢?我们假设主键ID为bigint类型,长度为8字节(面试官问你int类型,一个int就是32位,4字节),而指针大小在InnoDB源码中设置为6字节,所以就是 8+6=14 字节,16k/14B =16*1024B/14B = 1170

因此,一棵高度为2的B+树,能存放1170 * 16=18720条这样的数据记录。同理一棵高度为3的B+树,能存放1170 *1170 *16 =21902400,大概可以存放两千万左右的记录。B+树高度一般为1-3层,如果B+到了4层,查询的时候会多查磁盘的次数,SQL就会变慢。

因此单表数据量太大,SQL查询会变慢,所以就需要考虑分表啦。

2. 什么时候考虑分库分表?

对于MySQLInnoDB存储引擎的话,单表多可以存储10亿级数据。但是的话,如果真的存储这么多,性能就会非常差。一般数据量千万级别,B+树索引高度就会到3层以上了,查询的时候会多查磁盘的次数,SQL就会变慢。

阿里巴巴的《Java开发手册》提出:

单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表。

那我们是不是等到数据量到达五百万,才开始分库分表呢?

不是这样的,我们应该提前规划分库分表,如果估算3年后,你的表都不会到达这个五百万,则不需要分库分表。

MySQL服务器如果配置更好,是不是可以超过这个500万这个量级,才考虑分库分表?

虽然配置更好,可能数据量大之后,性能还是不错,但是如果持续发展的话,还是要考虑分库分表

一般什么类型业务表需要才分库分表?

通用是一些流水表、用户表等才考虑分库分表,如果是一些配置类的表,则完全不用考虑,因为不太可能到达这个量级。

3. 如何选择分表键

分表键,即用来分库/分表的字段,换种说法就是,你以哪个维度来分库分表的。比如你按用户ID分表、按时间分表、按地区分表,这些用户ID、时间、地区就是分表键。

一般数据库表拆分的原则,需要先找到业务的主题。比如你的数据库表是一张企业客户信息表,就可以考虑用了客户号做为分表键

为什么考虑用客户号做分表键呢?

这是因为表是基于客户信息的,所以,需要将同一个客户信息的数据,落到一个表中,避免触发全表路由。

4.非分表键如何查询

分库分表后,有时候无法避免一些业务场景,需要通过非分表键来查询。

假设一张用户表,根据userId做分表键,来分库分表。但是用户登录时,需要根据用户手机号来登陆。这时候,就需要通过手机号查询用户信息。而手机号是非分表键。

非分表键查询,一般有这几种方案:

  • 遍历:粗暴的方法,就是遍历所有的表,找出符合条件的手机号记录(不建议)
  • 将用户信息冗余同步到ES,同步发送到ES,然后通过ES来查询(推荐)

其实还有基因法:比如非分表键可以解析出分表键出来,比如常见的,订单号生成时,可以包含客户号进去,通过订单号查询,就可以解析出客户号。但是这个场景除外,手机号似乎不适合冗余userId。

5. 分表策略如何选择

5.1 range范围

range,即范围策略划分表。比如我们可以将表的主键order_id,按照从0~300万的划分为一个表,300万~600万划分到另外一个表。如下图:

有时候我们也可以按时间范围来划分,如不同年月的订单放到不同的表,它也是一种range的划分策略。

  • 优点: range范围分表,有利于扩容。
  • 缺点:可能会有热点问题。因为订单id是一直在增大的,也就是说近一段时间都是汇聚在一张表里面的。比如近一个月的订单都在300万~600万之间,平时用户一般都查近一个月的订单比较多,请求都打到order_1表啦。

5.2 hash取模

hash取模策略:

指定的路由key(一般是user_id、order_id、customer_no作为key)对分表总数进行取模,把数据分散到各个表中。

比如原始订单表信息,我们把它分成4张分表:

  • 比如id=1,对4取模,就会得到1,就把它放到t_order_1;
  • id=3,对4取模,就会得到3,就把它放到t_order_3;

一般,我们会取哈希值,再做取余:

Math.abs(orderId.hashCode()) % table_number