图片来自Pexels我入职第一天就经历了一次生产事故。运维同学告诫,线上MySQL负载压力大,直接压缩了主库MySQL。坠毁(第一天不是一个好兆头)。运维同学紧急进行了主从切换。在事后查找生产事故的原因时,发现MySQL雪崩的主要原因是慢查询导致的。把慢查询的SQL导出后,项目经理直接说这个MySQL优化功能要交给新人!我连忙打开跳板机查看。五亿级别!这狗屎肯定是历史问题积累到现在造成的。项目经理直接把这个坑甩给我了。我心想,难道我不能通过试用期吗????好在身经百战,迅速与项目经理和老同事沟通了解业务场景,才发现现在的情况是这样的。我所在的公司主要是做IM社交系统的。这张5亿级别的数据表是随从表,也俗称粉丝表。一些大V或者网红,拥有百万以上的粉丝是很常见的。.A跟随B之后会产生一条记录,B跟随A也会产生一条记录。很长时间才达到今天的数据规模。项目经理慢慢对我说,这个优化你别着急,先把方案拿出来!一万只草泥马在我心里擦肩而过,这给了我一块不好啃的骨头。看来是想试探一下自己的能力深浅。根据我之前的经验,当单表数据达到500W左右时,就要考虑分表了。常见的表分区方案无非是散列取模或范围分区。不过这次的数据表拆分迁移过程的难点在于两个方面:数据的平滑过渡,以及单表数据在不停机的情况下逐步迁移。(老板说:敢宕机,每分钟损失几千元,KPI直接给你扣负分)数据分区,用hash还是range?(暂时不会用一些分库分表的中间件,无奈)先说hash:一般情况下,我们都是拿userid取模,直接把数据插入模中就行了,这是简单粗暴的。但是如果user_id=128和user_id=257是仿照128的话,他们都对应user_attention_1表,而且恰好都是粉丝过百万的网红,所以两个人很容易填满数据表。当其他用户再次输入数据时,user_attention_1表无疑会变成一个大表。这是一个典型的数据热点问题,这个方案可以通过。有同学说user_id和fans_id可以合并取模分配,我也考虑过这个问题。这样虽然数据分布均匀,但是会有一个致命的问题就是查询问题(因为目前没有像MongoDB和DB2这样的高性能查询DB,也没有数据同步,考虑到工作量还是查询现有的分布表中的数据)。比如业务场景中经常用到的查询是我关注那些人,那些人关注我,那么我们的查询代码可能会这样写://我关注谁select*fromuser_attentionwhereuser_id=#{userId}//谁关注了meselect*fromuser_attentionwherefans_id=#{userId}我们把user_id和fans_id的组合hash之后,如果我要查询我关注了谁,谁关注了我,那么我会搜索128张表得到结果。这太恶心了,绝对不可取,而且考虑到以后的扩容至少会影响一半的数据,实在不好用,这个方案PASS。接下来说一下range:Range看起来也很简单。当用户id在一定范围内时,会路由到一张表中。比如userid=128,那么范围[0,10000]对应表user_attention_0,直接插入数据即可。但是这样也会造成热点数据问题。似乎简单的水平分区已经不够用了。这个方案也是可以通过的,所以还是得另辟蹊径。经过夜以继日的努力和深思熟虑,我得出了三种解决方案:第一种方案:范围+一致性哈希环组合(哈希环节点10000)什么是哈希环?看这里:我想采用这个方案主要是因为它容易扩展,影响范围小,只涉及哈希环上的单个节点。数据迁移简单,每次扩容只需要在新增节点和后端节点之间交换数据即可。查询范围小,部分表分区根据range和hash的关系进行检索。大意是先根据user_id划分range,但是range之后,我后面对应的可能不是table,而是hashring。每个范围区域对应自己的一组环,我们可以根据实际情况进行扩容。比如[1,10000]范围内只有2个大V,那么我们可以分三张表。预留1500万数据容量。[10001,20000]有4个网红大V,哈希环上给出了实际的4张表。我们的userid可以按顺时针顺序放在第一个物理表中,数据就会存储起来。凡事有利有弊,计划要结合工时。只有在实际可行性和技术审查后才能决定。我们也列出了缺点:设计复杂,需要增加rangearea和hashring的关系。系统修改较多,查询关系复杂,增加了路由表的概念。虽然尽量把用户数据分配到一个区域,但是查询谁关注我,我关注谁的逻辑还是比较复杂。第二种方案:range+hash取模(hash取模300)其实更容易理解。它是范围+哈希模数的简单组合。先range到某个范围,然后在这个范围内hash取模找到对应的表存储。这个方案比方案一简单,但是方案一的问题也存在,也有扩展数据,影响范围大的问题。但是,实现起来要简单得多。从查询的角度,可以根据不同的场景控制模数的大小范围,每个分区的哈希模数根据实际情况采用不同的值。最后一个解决方案:rangeuserId分区是我认为最可靠和可实施的解决方案。它看起来像第二种解决方案,但更具体一些。首先定义一个中间关系表user_attention_routing。我们会做一个用户范围和路由到哪个表的关系,按照范围区间搜索,把已有的数据组合成某个大V,或者网红数据量比较大的,我们就路由形成自己的表格。数据大概是这样的。比如user_id=256就是一个大V,所以建议单独建一个表。查询范围时,首先检查是否有自己对应的路由表。而其他分散的用户仍然被路由到一个统一的表。这时候有同学会说数据不统一。我也是这么想的,但是想要绝对均匀分布基本是不可能的。只能相对做。尽量分发一些大V,不要占用公共资源。当某个人突然变成大V的时候,这个人就会被单独分开,这个过程会不断进化,保证数据的平衡。而经过这样的处理,原来的很多关联查询并没有太大的变化,只要在数据迁移之后动态修改所有原来的user_attentions即可(使用mybatis拦截器就可以处理)。PS:其实分析实际业务场景的关注数据,大多来自于那些碎片化的用户。分表计划首先是这样确定的,接下来还有一个问题就是查询问题。上面说了,很多业务查询无非就是谁关注我,我关注谁。如果继续用之前的://我关注了谁select*fromuser_attentionwhereuser_id=#{userId}//谁关注了我select*fromuser_attentionwherefans_id=#{userId}这样的方案,当我想查看我的粉丝是谁时,悲剧了,我还是要检索全表,根据fansid找到我所有的粉丝,因为这个表只记录了我关注的人的数据。考虑到这类问题,我决定重新设计数据存储形式,用空间换时间的思路。原来的处理方式是在用户关注对方时生成一条记录。目前的处理方式是用户A在关注用户B时写入两条数据,通过字段区分关系。如果user_attention表是这样的:用户1关注用户2后生成两条数据,state(1代表我关注,0代表我被关注,2代表我们有关联),采用这条数据后存储方法,我所有的查询都可以从user_id开始,而不是反向推fans_id。在数据库索引设计方面,考虑user_id,fans_id,state和user_id,state的结构就可以了。是不是感觉很简单?虽然数据存储量增加了,但是查询起来方便多了。分表和查询的问题解决了,最后一步就是考虑数据迁移的过程,这个也很重要。失败了,自己的KPI会被扣(一步一步)!数据迁移最需要考虑的问题是时效性。迁移程序是必不可少的。如何正常运行生产环境?迁移脚本离线运行数据交互不影响吗?答案是经典套路数据的双写,因为旧数据并没有一下子迁移到新表,还保留着user_attention产生的数据。在生成旧表数据的同时,根据路由规则,直接保存到新表中。离线迁移程序,多开几台服务器运行比较慢,但是一定要控制数据量,不要占用IO,影响生产环境。线下模拟和演练也是必不可少的,谁也不能保证能不能行。有什么问题?迁移脚本和user_id、fans_id的在线唯一索引就够了。在某些极端情况下,数据会存储在新表中并写入数据,但旧表中的数据并没有更新。可以做好版本号控制和日志记录就可以了,都是比较简单的。当新表数据与旧表完全同步后,我们可以将系统中所有影响旧表查询的语句都改成新表查询,验证没有问题。如果没有问题,你终于可以幸福了!truncatetableuser_attention;杀死这5亿条数据的定时炸弹。好了,今天的分享到此结束。看来我不仅能熬过试用期,还能熬过试用期。更别提下班回家接娃了??。作者:二线城市程序员TOM编辑:陶佳龙来源:转载自公众号JavaTom
