公众号后台回复“
学习
”,免费获取精品学习资料
扫描下方海报
试听
本文来源:crossoverJie
前言
之前不少人问我“能否分享一些分库分表相关的实践”,其实不是我不分享,而是真的经验不多🤣;和大部分人一样都是停留在理论阶段。
不过这次多少有些可以说道了。
先谈谈背景,我们生产数据库随着业务发展量也逐渐起来;好几张单表已经突破
亿级
数据,并且保持每天 200+W 的数据量增加。
而我们有些业务需要进行关联查询、或者是报表统计;在这样的背景下大表的问题更加突出(比如一个查询功能需要跑好几分钟)。
可能很多人会说:为啥单表都过亿了才想方案解决?其实不是不想,而是由于历史原因加上错误预估了数据增长才导致这个局面。总之原因比较复杂,也不是本次讨论的重点。
临时方案
由于需求紧、人手缺的情况下,整个处理的过程分为几个阶段。
第一阶段应该是去年底,当时运维反应
MySQL
所在的主机内存占用很高,整体负载也居高不下,导致整个 MySQL 的吞吐量明显降低(写入、查询数据都明显减慢)。
为此我们找出了数据量最大的几张表,发现大部分数据量在7/8000W 左右,少数的已经突破一亿。
通过业务层面进行分析发现,这些数据多数都是用户产生的一些
日志型数据
,而且这些数据在业务上并不是强相关的,甚至两三个月前的数据其实已经不需要实时查询了。
因为接近年底,尽可能的不想去动应用,考虑是否可以在运维层面缓解压力;主要的目的就是把单表的数据量降低。
原本是想把两个月之前的数据直接迁移出来放到备份表中,但在准备实施的过程中发现一个大坑。
表中没有一个可以排序的索引,导致我们无法快速的筛选出一部分数据!这真是一个深坑,为后面的一些优化埋了个地雷;即便是加索引也需要花几个小时(具体多久没敢在生产测试)。
如果我们强行按照时间进行筛选,可能查询出 4000W 的数据就得花上好几个小时;这显然是行不通的。
于是我们便想到了一个大胆的想法:这部分数据是否可以直接不要了?
这可能是最有效及最快的方式了,和产品沟通后得知这部分数据真的只是日志型的数据,即便是报表出不来今后补上也是可以的。
于是我们就简单粗暴的做了以下事情:
这样新的数据就写到了新表,同时业务上也是使用的这个数据量较小的新表。
虽说过程不太优雅,但至少是解决了问题同时也给我们做技术改造预留了时间。
分表方案
之前的方案虽说可以缓解压力,但不能根本解决问题。
有些业务必须得查询之前的数据,导致之前那招行不通了,所以正好我们就借助这个机会把表分了。
我相信大部分人虽说没有做过实际做过分表,但也见过猪跑;网上一搜各种方案层出不穷。
我认为最重要的一点是要结合实际业务找出需要 sharding 的字段,同时还有上线阶段的数据迁移也非常重要。
时间
可能大家都会说用 hash 的方式分配得最均匀,但我认为这还是需要使用历史数据的场景才用哈希分表。
而对于不需要历史数据的场景,比如业务上只查询近三个月的数据。
这类需求完成可以采取时间分表,按照月份进行划分,这样改动简单,同时对历史数据也比较好迁移。
于是我们首先将这类需求的表筛选出来,按照月份进行拆分,只是在查询的时候拼接好表名即可;也比较好理解。
哈希
刚才也提到了:需要根据业务需求进行分表策略。
而一旦所有的数据都有可能查询时,按照时间分表也就行不通了。(也能做,只是如果不是按照时间进行查询时需要遍历所有的表)
因此我们计划采用
hash
的方式分表,这算是业界比较主流的方式就不再赘述。
采用哈希时需要将
sharding
字段选好,由于我们的业务比较单纯;是一个物联网应用,所有的数据都包含有物联网设备的唯一标识(IMEI),并且这个字段天然的就保持了唯一性;大多数的业务也都是根据这个字段来的,所以它非常适合来做这个
sharding
字段。
在做分表之前也调研过
MyCAT
及
sharding
-
jdbc
(现已升级为
shardingsphere
),最终考虑到对开发的友好性及不增加运维复杂度还是决定在 jdbc 层 sharding 的方式。
但由于历史原因我们并不太好集成
sharding
-
jdbc
,但基于
sharding
的特点自己实现了一个分表策略。
这个简单也好理解:
int index = hash(sharding字段) % 分表数量 ;
select xx from 'busy_'+index where sharding字段 = xxx;
其实就是算出了表名,然后路由过去查询即可。
只是我们实现的非常简单:修改了所有的底层查询方法,每个方法都里都做了这样的一个判断。
并没有像
sharding
-
jdbc
一样,代理了数据库的查询方法;其中还要做
SQL
解析-->
SQL
路由-->执行
SQL
-->合并结果
这一系列的流程。
如果自己再做一遍无异于重新造了一个轮子,并且并不专业,只是在现有的技术条件下选择了一个快速实现达成效果的方法。
不过这个过程中我们节省了将 sharding 字段哈希的过程,因为每一个 IMEI 号其实都是一个唯一的整型,直接用它做 mod 运算即可。
还有一个是需要一个统一的组件生成规则,分表后不能再依赖于单表的字段自增了;方法还是挺多的:
-
比如时间戳+随机数可满足大部分业务。
-
UUID,生成简单,但没法做排序。
-
雪花算法统一生成主键ID。
大家可以根据自己的实际情况做选择。
业务调整
因为我们并没有使用第三方的 sharding-jdbc 组件,所有没有办法做到对代码的低侵入性;每个涉及到分表的业务代码都需要做底层方法的改造(也就是路由到正确的表)。
考虑到后续业务的发展,我们决定将拆分的表分为 64 张;加上后续引入大数据平台足以应对几年的数据增长。
这里还有个小细节需要注意:分表的数量需要为 2∧N 次方,因为在取模的这种分表方式下,即便是今后再需要分表影响的数据也会尽量的小。
再修改时只能将表名称进行全局搜索,然后加以修改,同时根据修改的方法倒推到表现的业务并记录下来,方便后续回归测试。
当然无法避免查询时利用非 sharding 字段导致的全表扫描,这是所有分片后都会遇到的问题。
因此我们在修改分表方法的底层查询时同时也会查看是否有走分片字段,如果不是,那是否可以调整业务。
比如对于一个上亿的数据是否还有必要存在按照分页查询、日期查询?这样的业务是否真的具有意义?
我们尽可能的引导产品按照这样的方式来设计产品或者做出调整。
但对于报表这类的需求确实也没办法,比如统计表中某种类型的数据;这种我们也可以利用多线程的方式去并行查询然后汇总统计来提高查询效率。
有时也有一些另类场景:
比如一个千万表中有某一特殊类型的数据只占了很小一部分,比如说几千上万条。