今天的文章其实是我在面试中遇到的一道真题。分库分表大家可能听多了,但是阅读扩散这个问题你了解吗?这里涉及到几个问题。什么是分库分表?什么是读扩散问题?为什么分库分表会造成读扩散的问题?如何解决读扩散问题?这些问题很有意思。相信兄弟们也会有机会见面的哈哈哈。下面开始分库分表的话题。我们一般做分库分表的项目开发。一开始一般先用一张数据表,一般来说数据表写入2kw条数据后,底层B+树的层级结构可能会变高,不同层级的数据页一般存放在不同的磁盘。换句话说,磁盘IO会增加,导致查询性能变差。如果你对上面这句话有疑问,可以看看我之前写的那篇文章。因此,当我们需要在单表中管理越来越多的数据时,就不得不考虑数据库分表了。这里的分表分为水平分表和垂直分表。垂直分表的原理比较简单。一般会把某些列拆分成一张新表,这样单行数据会变小,B+树中单个数据页(固定16kb)可以放的行数会增加,这样单表就可以放更多的数据。立式表就不多说了。下面,我们重点介绍最常见的层级子表。横向分表的方法有好几种,但是不管用哪一种,本质上都是把原来的user表变成了user_0,user1,user2....uerN等N个小表。从读写一个大的user表,到读写user_1...userN这样的N个小表。在分表的每个小表中,只保存一部分数据,保存多少由你决定,一般是500w~2kw。分表呢?我觉得按照id范围分表最好的方法就是按照id范围分表。我们假设每个子表可以容纳2kw行数据。然后user0放主键id为1~2kw的数据。user1放id为2kw+1~4kw,user2放id为4kw+1~6kw,userN放2Nkw+1~2(N+1)kw。假设有一条数据,id=3kw,将这3kw除以2kw=1.5,向下舍入得到1,则可以得到这条数据属于user1表。所以只需要读写user1表即可。这样就完成了数据的路由逻辑。我们把这部分逻辑封装起来,放在数据库和业务代码之间。所以。对于业务代码,它只知道自己在读写一张用户表,不知道下面还有那么多小表。对于数据库来说,它并不知道它已经被分表了。它只知道有几张表,恰好有相似的名字。这只是在一个数据库中拆分表。如果范围比较大,也可以拆分到多个数据库中。这就是所谓的分库分表。不管是单库分表还是分库分表,都可以通过这样的中间层逻辑进行路由。真是应了那句话,没有什么是加个中间层解决不了的。如果是这样,添加一个额外的层。至于这个中间层的实现,就比较灵活了。可以像第三方orm库一样添加到业务代码中。也可以通过orm读写分表,在mysql和业务代码之间添加代理服务。如果是通过第三方的orm库来实现,需要根据不同的语言实现不同的代码库,所以很多工厂选择后者加代理,这样就不用关心使用的语言上游服务。通过proxy管理分表,取模id分表。这时候有兄弟想提问,“我看到很多解法取模id,你的方案是不是不完整?”。模数方案也很常见。比如进来一个id=31,我们一共有5张表,分别是user0到user4。对于31%5=1,取模为1,所以我们可以知道应该读写user1表。根据id取模分表的好处当然是比较简单。而且读写数据可以平均分配到各个子表中。但缺点也很明显。如果要扩展表的数量,例如,从5个表扩展到8个表。那也是id=31的数据,31%8=7,需要读写user7表。跟原版不符。这就需要考虑数据迁移问题。很秃头。为了避免后续扩容的问题,我看到有的业务一开始估计数据很大,然后分成100张表。如果一个表存储2kw个条目,那么它也可以存储20亿条数据。.不是说这不行,只是当最后放弃这个业务的时候,存储的数据有几百万条。每次打开数据库表,都能看到一大堆user_xx,但就是不舒服。专业一点,称之为附加程序。员工的心理负担。在上面的方法中,按照id范围分表可以很好的解决这些问题。当数据少时,表也会少。随着数据的增加,表会逐渐增加。而且这张桌子还可以无限扩大。这是否意味着模方法不再有用?也不是。结合上面两种id取模的方式最大的好处就是新写入的数据实际上是分布到多个表中的。并且按照id范围分表,因为id递增,所以新写入的数据一般会落到某个表中,如果你的业务场景频繁写入数据,那么这个表就会出现写入热点的问题。这时候可以结合id取模和id范围分表。我们可以在某个id范围内引入取模函数。比如2kw~4kw以前是user1表,现在可以分成5个这个范围内的表,即引入user1-0,user1-2到user1-4,取模取这些5张桌子。比如id=3kw,根据范围,会分配到user1表,然后对3kw%5=0取模,即读写user1-0表。这样,写入单个表可以被分摊为写入多个表。这种优势在分库的场景下会更加明显。不同的库可以将服务部署到不同的机器上,这样就可以发挥每台机器的性能。根据id范围分表然后取模读扩散问题我们上面提到的几种分表方式都是以id列作为分表的依据,其实也就是所谓的shardingkey。其实我们一般都是使用数据库主键作为shardkey。这样,理想情况下我们知道一个id,无论使用哪种规则,我们都可以快速定位到读取哪个子表。但是很多时候,我们的查询并不仅仅检查主键,如果我的数据库表有一个列名,并且添加了一个公共索引。这样我执行下面的sqlselect*fromuserwherename="小白";由于name不是shardkey,所以我们无法定位到具体的shardtable来执行sql。所以上面的SQL会针对所有的子表执行,当然SQL不会串行执行,但SQL一般是并发执行的。如果我有100张表,执行100次sql。如果我有200张表,执行sql200次。随着我的桌子越来越多,次数也会越来越多。这就是所谓的读扩散问题。Readdiffusionproblem这是一个比较有趣的问题。确实是个问题,但大部分业务不会去处理。读书100遍有什么错?数据增长之后,如果读取次数继续增加会怎样?但是我不能忍受我的生意不赚钱,我根本不能增长那么多数据。话是这么说没错,但是当面试官问你的时候,你得知道怎么应对。分表引入新表的问题核心是主键是分片键,普通索引列不分片。那好办。我们单独新建一个分片表。这张新表中的列只是旧表的主键id和普通索引列。这次使用普通索引列作为分片键。使用新的索引表解决读扩散问题。这样,当我们要查询普通索引列时,我们可以先在这个新的分片表中进行查询,我们可以快速定位到对应的主键id,然后通过主键id去老的分片中查询切片表中的数据一次。这样,将原来漫无目的的全表扩散查询,简化为只查询固定的几个表。例如。比如我的表本来是这样的,id列是主键也是shardkey,name列是非主键索引。为简单起见,假设一张表中有三条数据。此时子表中id=1、4、6都有name="小白"的数据。当我们执行select*fromuserwherename="Xiaobai";时,需要并发查询3张表。随着表数量的增加,查询的数量会越来越多。一个例子来说明读扩散的问题但是如果我们为name列创建一个新表(nameX),使用name作为新的shardkey。这样,我们可以先执行selectidfromnameXwherename="Xiaobai";然后取结果中的id去查询select*fromuserwhereidin(ids);这样,即使表再多,我们也可以快速定位到某几个特定的??表,减少查询次数。一个例子是通过一个新的索引表来解决读扩散的问题,但是这种方法的缺点也是很明显的。需要维护两套表,更新普通索引列时,需要同时更改两张表。有一定开发量的更简单的解决方案吗?使用其他更合适的存储,我们的常规查询是通过id主键查询对应的name列。而像上面的方案,通过引入一个新表,反过来,先用name找到对应的id,再用id获取具体的数据。这实际上就像创建一个新索引。像这样,通过name列检索原始数据的思路,其实和倒排索引很像。相当于用倒排索引的思想来解决分表下的数据查询问题。回想起来,其实我们最初的需求无非就是在数据量大的场景下,提供普通的索引列或者其他更多维度的查询。这种情况下使用es比较合适,es自然分片,内部使用倒排索引的形式来加速数据查询。哦?猛哥,又是它,倒排索引又是一个小细节,记录一下。比如我还有一行数据id,name,age。在mysql中,你必须按id进行分片。如果要支持name和age的查询,为了防止readspread,就得分别建一个nameshardtable和一个ageshardtable。而如果你用es,它会用里面的id分片键分片,同时建立一个name到id,age到id的倒排索引。这和你上面做的没有什么不同吗?而且,把mysql连接到es上也很简单。我们可以通过开源工具canal监控mysqlbinlog日志变化,然后解析数据写入es,让es提供近实时的查询能力。mysql同步es觉得es+mysql还是很麻烦?还有其他更简洁的解决方案吗?有。不要用mysql,改用tidb。相信大家都听说过这个名字。这是一个分布式数据库。它通过引入Range的概念来对数据表进行分片。比如第一个分片表的id是0~2kw,第二个分片表的id是2kw~4kw。哦?你熟悉吗?这不就是文章开头说的根据id范围划分数据库表吗?支持普通索引,普通索引也是分片的。是不是和上面提到的倒排索引方案类似?另一个小细节。而且tidb的语法和mysql几乎一样,现在有很多现成的工具可以帮你把数据从mysql迁移到tidb。所以开发成本并不高。用TiDB替换mysql总结一下,当单表数据量过大时,mysql的查询性能会变差。因此,当数据量变得庞大时,就需要考虑水平分表了。水平分片需要选择分片键,一般选择主键,然后根据id进行取模,或者根据id的范围进行分表。mysql表横向分表后,对于非shardingkey字段的查询,会出现readdiffusion的问题。可以使用普通的索引列作为shardingkey新建一张表,先查新表得到id,然后回原表查一次原表。这本质上是借鉴了倒排索引的思想。如果想支持更多维度的查询,可以监控mysql的binlog,将数据写入es,提供近实时的查询能力。当然,用tidb替换mysql也是一个思路。tidb真是个好东西。许多工厂已经将其用于新皮肤和标签来制作自己的自研数据库。强烈推荐大家学一波。不要做过早的优化,不要闲来无事就搞出100张表,很多时候真的没用。参考资料《图解分库分表》https://mp.weixin.qq.com/s/OI5y4HMTuEZR1hoz9aOMxg当我还在某游戏项目组开发的时候,从企鹅挖来的策划人发誓说我们会把这款游戏老少皆宜,并且一定很成功。做成全球服务器。至少有1亿人在线注册,10万人同时在线。规划设计好。我们算一算,相信他能有1亿的注册量。idrange用于分片,分4张表。让我热血沸腾。那天晚上下班,夏蝉哭了,赤道吹来的热风拂过我的手臂。听着泽野弘之的歌,即使开着电瓶车,也有开高达的感觉。一年之后。比赛上线前一天,通知运维加机,怕它站不起来,得守个通宵。后来上线了,全球最高在线人数58人,其中7人是项目组成员。还是夏天,还是一样的下班通勤。我想哭,但我不能哭,因为坐电瓶车擦眼泪不安全。
