分库分表会带来读扩散问题?怎么解决?

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
云数据库 RDS PostgreSQL,高可用系列 2核4GB
简介: 分库分表会带来读扩散问题?怎么解决?


今天这篇文章,其实也是我曾经面试中遇到过的真题。

分库分表大家可能听得多了,但读扩散问题大家了解吗?

这里涉及到几个问题。

  • 分库分表是什么?
  • 读扩散问题是什么?
  • 分库分表为什么会引发读扩散问题?
  • 怎么解决读扩散问题?

这些问题还是比较有意思的。

相信兄弟们也一定有机会遇到哈哈哈。

我们先从分库分表的话题聊起吧。

分库分表

我们平时做项目开发。一开始,通常都先用一张数据表,而一般来说数据表写到两千万条数据之后,底层 B+ 树的层级结构就可能会变高,不同层级的数据页一般都放在磁盘里不同的地方,换言之,磁盘 IO 就会增多,带来的便是查询性能变差。

于是,当我们单表需要管理的数据变得越来越多,就不得不考虑数据库分表。而这里的分表,分为水平分表和垂直分表。

垂直分表的原理比较简单,一般就是把某几列拆成一个新表,这样单行数据就会变小, B+ 树里的单个数据页(固定 16KB)内能放入的行数就会变多,从而使单表能放入更多的数据。

垂直分表没有太多可以说的点。下面,我们重点说说最常见的水平分表。

水平分表有好几种做法,但不管是哪种,本质上都是将原来的 user 表,变成 user_0, user1, user2 .... userN 这样的 N 多张小表。

从读写一张 user 大表,变成读写 user_1 … userN 这样的 N 张小表。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

分表

每一张小表里,只保存一部分数据,但具体保存多少,这个自己定,一般就定 500 万~ 两千万。

那分表具体怎么做?

根据 id 范围分表

我认为最好用的,是根据 id 范围进行分表。

我们假设每张分表能放两千万行数据。那

  • user0 放主键 id 为 1~2千万的数据;
  • user1 定 id 为 两千万 +1 ~ 4千万;
  • user2 定 id 为 4千万 +1 ~ 6千万;
  • userN 放 2N千万+1 ~ 2(N+1)千万。

根据id范围分表

假设现在有条数据,id=3 千万,将这个 3 千万除 2 千万= 1.5,向下取整得到 1,那就可以得到这条数据属于 user1 表。于是去读写 user1 表就行了。这就完成了数据的路由逻辑,我们把这部分逻辑封装起来,放在数据库和业务代码之间。

这样。对于业务代码来说,它只知道自己在读写一张 user 表,根本不知道底下还分了那么多张小表。

对于数据库来说,它并不知道自己被分表了,它只知道有那么几张表,正好名字长得比较像而已。

这还只是在一个数据库里做分表,如果范围再搞大点,还能在多个数据库里做分表,这就是所谓的分库分表。

不管是单库分表还是分库分表,都可以通过这样一个中间层逻辑做路由。

还真的就应了那句话,没有什么是加中间层不能解决的

如果有,就多加一层。

至于这个中间层的实现方式就更灵活了,它既可以像第三方orm库那样加在业务代码中。

通过 ORM 读写分表

也可以在 MySQL 和业务代码之间加个 Proxy 服务。

如果是通过第三方 ORM 库的方式来做的话,那需要根据不同语言实现不同的代码库,所以不少厂都选择后者加个 Proxy 的方式,这样就不需要关心上游服务用的是什么语言。

通过 Proxy 管理分表

根据 id 取模分表

这时候就有兄弟要提出问题了,"我看很多方案都对id取模,你这个方案是不是不完整?"

取模的方案也是很常见的。

比如一个 id=31 进来,我们一共分了 5 张表,分别是 user0 到 user4。对 31%5=1,取模得 1,于是就能知道应该读写 user1 表。

根据 id 取模分表

优点当然是比较简单。而且读写数据都可以很均匀的分摊到每个分表上。

但缺点也比较明显,如果想要扩展表的个数,比如从 5 张表变成 8 张表。那同样还是 id=31 的数据,31%8 = 7,就需要读写 user7 这张表。跟原来就对不上了。

这就需要考虑数据迁移的问题 。很头秃。

为了避免后续扩展的问题,我见过一些业务一开始就将数据预估得很大,然后心一横,分成 100 张表,一张表如果存个两千万条,那也能存 20 亿条数据了。

也不是说这样不行吧,就是这个业务直到最后放弃的时候,也就存了百万条数据,每次打开数据库表能看到茫茫多的 user_xx,就是不太舒服。专业点叫做加了程序员的心智负担

而上面一种方式,根据 id 范围去分表,就能很好地解决这些问题。数据少的时候,表也少。随着数据增多,表会慢慢变多。而且这样表还可以无限扩展。

那是不是说取模的做法就用不上了呢?

也不是。

将上面两种方式结合起来

id 取模的做法,最大的好处是,新写入的数据都是实实在在地分散到了多张表上。

而根据 id 范围去做分表,因为 id 是递增的,那新写入的数据一般都会落到某一张表上,如果你的业务场景写数据特别频繁,那这张表就会出现写热点的问题。

这时候就可以将 id 取模和 id 范围分表的方式结合起来。

我们可以在某个 id 范围里,引入取模的功能。比如 以前 2千万~4千万 是 user1 表。现在可以在这个范围再分成 5 个表,也就是引 入user1-0、user1-2 到 user1-4,在这 5 个表里取模。

举个例子,id=3千万,根据范围,会分到 user1 表,然后再进行取模 3千万 % 5 = 0,也就是读写 user1-0 表。

这样就可以将写单表分摊为写多表

这在分库的场景下优势会更明显。不同的库,可以把服务部署到不同的机器上,这样各个机器的性能都能被用起来。

根据 id 范围分表后再取模

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

读扩散问题

我们上面提到的好几种分表方式,都用了 id 这一列作为分表的依据,这其实就是所谓的分片键

实际上我们一般也是用的数据库主键作为分片键。

这样,理想情况下我们已知一个 id,不管是根据哪种规则,我们都能很快定位到该读哪个分表。

但很多情况下,我们的查询又不是只查主键 ,如果我的数据库表有一列 name,并且加了个普通索引。

这样我执行下面的 SQL:

select * from user where name = "芋道源码";

由于 name 并不是分片键,我们没法定位到具体要到哪个分表上去执行 SQL

于是就会对所有分表都执行上面的 SQL,当然不会是串行执行 SQL,一般都是并发执行 SQL 的。如果我有 100 张表,就执行 100 次 SQL。如果我有 200 张表,就执行 200 次 SQL。

随着我的表越来越多,次数会越来越多,这就是所谓的读扩散问题。

读扩散问题

这是个比较有趣的问题,它确实是个问题,但大部分的业务不会去处理它。读 100 次怎么了,数据增长之后读的次数会不断增加又怎么了?但架不住我的业务不赚钱啊,也根本长不了那么多数据啊。

话是这么说没错,但面试官问你的时候,你得知道怎么处理啊。

引入新表来做分表

问题的核心在于,主键是分片键,而普通索引列并不分片

那好办,我们单独建个新的分片表,这个新表里的列就只有旧表的主键 id 和普通索引列,而这次换普通索引列来做分片键。

通过新索引表解决读扩散问题

这样当我们要查询普通索引列时,先到这个新的分片表里做一次查询,就能迅速定位到对应的主键 id,然后再拿主键 id 去旧的分片表里查一次数据。这样就从原来漫无目的的全表扩散查询,缩减为只查固定几个表了。

举个例子:比如我的表原本长下面这样,其中 id 列是主键,同时也是分片键,name 列是非主键索引。为了简化,假设三条数据一张表。

此时分表里 id=1,4,6 的都有 name="芋道源码" 数据。

当我们执行 select * from user where name = "芋道源码"; 则需要并发查 3 张表,随着表变多,查询次数会变得更多。

举例说明读扩散问题

但如果我们为 name 列建个新表 (nameX) ,以 name 为新的分片键。

这样我们可以先执行 select id from nameX where name = "芋道源码";

再拿着结果里的 ids 去查询  select * from user where id in (ids); 这样就算表变多了,也可以迅速定位到某几张具体的表,减少了查询次数。

举例说明通过新索引表解决读扩散问题

但这个做法的缺点也比较明显,你需要维护两套表 ,并且普通索引列更新时,要两张表同时进行更改,有一定的开发量。

有没有更简单的方案?

使用其他更合适的存储

我们常规的查询是通过 id 主键去查询对应的 name 列。

而像上面的方案,则通过引入一个新表,倒过来,先用 name 查到对应的 id,再拿 id 去获取具体的数据。这其实就像是建立了一个新的索引一样。像这种,通过 name 列反查原数据的思想,其实就很类似于倒排索引 。相当于我们是利用了倒排索引的思路去解决分表下的数据查询问题。

回想下,其实我们的原始需求无非就是在大量数据的场景下依然能提供普通索引列或其他更多维度的查询 。这种场合,更适合使用 es,es 天然分片,而且内部利用倒排索引的形式来加速数据查询。

哦?兄弟萌,又是它,倒排索引,又是个极小的细节,做好笔记。

举个例子,我同样是一行数据 id、name、age。在 MySQL 里,你得根据 id 分片,如果要支持 name 和 age 的查询,为了防止读扩散,你得分别再建一个 name 的分片表和一个 age 的分片表。

而如果你用 es,它会在它内部以 id 分片键进行分片,同时还能建一个 name 到 id,和一个 age 到 id 的倒排索引。这是不是就跟上面做的事情没啥区别。

而且将 MySQL 接入 es 也非常简单,我们可以通过开源工具 Canal 监听 MySQL 的 binlog 日志变更,再将数据解析后写入 es,这样 es 就能提供近实时的查询能力。

MySQL 同步 es

觉得 es+MySQL 还是繁琐?有没有其他更简洁的方案?

有。

别用 MySQL 了,改用 TiDB 吧,相信大家多少也听说过这个名称,这是个分布式数据库。

它通过引入 Range 的概念进行数据表分片。比如第一个分片表的 id 在 0~2千万,第二个分片表的 id 在 2千万~4千万。

哦?有没有很熟悉,这不就是文章开头提到的根据 id 范围进行数据库分表吗?

支持普通索引,并且普通索引也是分片的 ,这是不是又跟上面提到的倒排索引方案很类似。

又是个极小的细节。

并且 TiDB 跟 MySQL 的语法几乎一致,现在也有非常多现成的工具可以帮你把数据从 MySQL 迁移到 TiDB 。所以开发成本并不高。

用 TiDB 替换 MySQL

总结

  • MySQL 在单表数据过大时,查询性能会变差,因此当数据量变得巨大时,需要考虑水平分表;
  • 水平分表需要选定一个分片键,一般选择主键,然后根据id进行取模,或者根据 id 的范围进行分表;
  • MySQL 水平分表后,对于非分片键字段的查询会有读扩散的问题,可以用普通索引列作分片键建一个新表,先查新表拿到 id 后再回到原表再查一次原表。这本质上是借鉴了倒排索引的思路;
  • 如果想要支持更多维度的查询,可以监听 MySQL 的 binlog,将数据写入到 es,提供近实时的查询能力;
  • 当然,用 TiDB 替换 MySQL 也是个思路。TiDB 属实是个好东西,不少厂都拿它换个皮贴个标,做成自己的自研数据库,非常推荐大家学习一波;
  • 不要做过早的优化,没事别上来就分 100 个表,很多时候真用不上。

最后

当年我还在某个游戏项目组里做开发的时候,从企鹅那边挖来的策划信誓旦旦的说,我们要做的这款游戏老少皆宜,肯定是爆款。要做成全球同服。上线至少过亿注册,十万人同时在线。要好好规划和设计。

我们算了下,信他能有个 1 亿注册。用了 id 范围的方式进行分片,分了 4 张表。

搞得我热血沸腾。

那天晚上下班,夏蝉鸣泣,从赤道吹来的热风阵阵拂过我的手臂,我听着泽野弘之的歌,就算是开电瓶车,我都感觉自己像是在开高达。

一年后。

游戏上线前一天通知运维加机器,怕顶不住,要整夜关注。

后来上线了,全球最高在线人数 58 人。其中有 7 个是项目组成员。

还是夏天,还是同样的下班路,想哭,但我不能哭,因为骑电瓶车的时候擦眼泪不安全。

参考资料

《图解分库分表》

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
存储 缓存 监控
美团面试:说说OOM三大场景和解决方案? (绝对史上最全)
小伙伴们,有没有遇到过程序突然崩溃,然后抛出一个OutOfMemoryError的异常?这就是我们俗称的OOM,也就是内存溢出 本文来带大家学习Java OOM的三大经典场景以及解决方案,保证让你有所收获!
5691 0
美团面试:说说OOM三大场景和解决方案? (绝对史上最全)
|
网络协议 IDE Linux
mongoose使用详细 -- 如何通过mongoose搭建服务器
mongoose使用详细 -- 如何通过mongoose搭建服务器
2144 0
vue3 element-plus 实现表格数据更改功能
在 vue3 中使用 element-plus 实现表格数据更改功能,可以通过以下步骤实现:
1499 0
|
存储 SQL 数据可视化
7-TDengine集成Grafana实现日志数据可视化
7-TDengine集成Grafana实现日志数据可视化
1170 0
7-TDengine集成Grafana实现日志数据可视化
|
NoSQL 关系型数据库 MySQL
排行榜系统设计:高并发场景下的最佳实践
本文由技术分享者小米带来,详细介绍了如何设计一个高效、稳定且易扩展的排行榜系统。内容涵盖项目背景、技术选型、数据结构设计、基本操作实现、分页显示、持久化与数据恢复,以及高并发下的性能优化策略。通过Redis与MySQL的结合,确保了排行榜的实时性和可靠性。适合对排行榜设计感兴趣的技术人员参考学习。
1646 7
排行榜系统设计:高并发场景下的最佳实践
|
存储 SQL NoSQL
应用性能设计的圣杯:读写扩散的概念与实践
本文结合这三年作者在钉钉见到的应用架构,以及一些业界的实践分享,整理出一篇关于应用读写扩散设计的维基。
|
8月前
|
计算机视觉
YOLOv11改进策略【Neck】| GFPN 超越BiFPN 通过跳层连接和跨尺度连接改进v11颈部网络
YOLOv11改进策略【Neck】| GFPN 超越BiFPN 通过跳层连接和跨尺度连接改进v11颈部网络
1714 10
YOLOv11改进策略【Neck】| GFPN 超越BiFPN 通过跳层连接和跨尺度连接改进v11颈部网络
|
消息中间件 负载均衡 中间件
中间件冗余部署
【7月更文挑战第21天】
214 5
|
缓存 监控 安全
Spring AOP 详细深入讲解+代码示例
Spring AOP(Aspect-Oriented Programming)是Spring框架提供的一种面向切面编程的技术。它通过将横切关注点(例如日志记录、事务管理、安全性检查等)从主业务逻辑代码中分离出来,以模块化的方式实现对这些关注点的管理和重用。 在Spring AOP中,切面(Aspect)是一个模块化的关注点,它可以跨越多个对象,例如日志记录、事务管理等。切面通过定义切点(Pointcut)和增强(Advice)来介入目标对象的方法执行过程。 切点是一个表达式,用于匹配目标对象的一组方法,在这些方法执行时切面会被触发。增强则定义了切面在目标对象方法执行前、执行后或抛出异常时所
16740 4
|
项目管理
「软件项目管理」一文详解软件项目管理概述
该文章详细介绍了软件项目管理的关键概念、知识体系以及实施过程,涵盖了项目初始化、计划制定、执行控制到项目结束的全流程管理,并探讨了项目管理与过程管理在软件开发中的相互作用和应用。
「软件项目管理」一文详解软件项目管理概述