专栏名称: 51CTO技术栈
有趣 | 有料 | 有内涵,为您提供最优质的内容,愿我们一起悦享技术,成就人生。
目录
相关文章推荐
51好读  ›  专栏  ›  51CTO技术栈

进来抄作业:一次完美的分库分表实践!

51CTO技术栈  · 公众号  · 程序员  · 2020-12-28 18:03

正文

送福利啦

关注 鸿蒙技术社区 ,回复 【鸿蒙】 价值 399元 的鸿蒙 开发板套件 (数量有限,先到先得) ,还可以 免费下载 鸿蒙 入门资料


👇 扫码 立刻关注 👇

专注开源技术,共建鸿蒙生态


几年前我曾经服务过的一家电商公司,随着业务增长我们每天的订单量很快从 30 万单增长到了 100 万单,订单总量也突破了一亿。


图片来自 Pexels


当时用的 MySQL 数据库。根据监控,我们的每秒最高订单量已经达到了 2000 笔(不包括秒杀,秒杀 TPS 已经上万了。秒杀我们有一套专门的解决方案,详见 《秒杀系统设计~亿级用户》 )。


不过,直到此时,订单系统还是单库单表,幸好当时数据库服务器配置不错,我们的系统才能撑住这么大的压力。


业务量还在快速增长,再不重构系统早晚出大事,我们花了一天时间快速制定了重构方案。


重构?说这么高大上,不就是分库分表吗?的确,就是分库分表。不过除了分库分表,还包括管理端的解决方案,比如运营,客服和商务需要从多维度查询订单数据,分库分表后,怎么满足大家的需求?


分库分表后,上线方案和数据不停机迁移方案都需要慎重考虑。为了保证系统稳定,还需要考虑相应的降级方案。


为什么要分库分表?


当数据库产生性能瓶颈:IO 瓶颈或 CPU 瓶颈。两种瓶颈最终都会导致数据库的活跃连接数增加,进而达到数据库可承受的最大活跃连接数阈值。


终会导致应用服务无连接可用,造成灾难性后果。可以先从代码,SQL,索引几方面进行优化。如果这几方面已经没有太多优化的余地,就该考虑分库分表了。


IO 瓶颈


第一种:磁盘读 IO 瓶颈。


由于热点数据太多,数据库缓存完全放不下,查询时会产生大量的磁盘 IO,查询速度会比较慢,这样会导致产生大量活跃连接,最终可能会发展成无连接可用的后果。


可以采用一主多从,读写分离的方案,用多个从库分摊查询流量。或者采用分库+水平分表(把一张表的数据拆成多张表来存放,比如订单表可以按 user_id 来拆分)的方案。


第二种:磁盘写 IO 瓶颈。


由于数据库写入频繁,会产生频繁的磁盘写入 IO 操作,频繁的磁盘 IO 操作导致产生大量活跃连接,最终同样会发展成无连接可用的后果。


这时只能采用分库方案,用多个库来分摊写入压力。再加上水平分表的策略,分表后,单表存储的数据量会更小,插入数据时索引查找和更新的成本会更低,插入速度自然会更快。


CPU 瓶颈


SQL 问题: 如果 SQL 中包含 join,group by,order by,非索引字段条件查询等增加 CPU 运算的操作,会对 CPU 产生明显的压力。


这时可以考虑 SQL 优化,创建适当的索引,也可以把一些计算量大的SQL逻辑放到应用中处理。


单表数据量太大: 由于单张表数据量过大,比如超过一亿,查询时遍历树的层次太深或者扫描的行太多,SQL 效率会很低,也会非常消耗 CPU。这时可以根据业务场景水平分表。


分库分表方案


分库分表主要有两种方案:

①利用 MyCat,KingShard 这种代理中间件分库分表。


好处是和业务代码耦合度很低,只需做一些配置即可,接入成本低。缺点是这种代理中间件需要单独部署,所以从调用连路上又多了一层。


而且分库分表逻辑完全由代理中间件管理,对于程序员完全是黑盒,一旦代理本身出问题(比如出错或宕机),会导致无法查询和存储相关业务数据,引发灾难性的后果。


如果不熟悉代理中间件源码,排查问题会非常困难。曾经有公司使用 MyCat,线上发生故障后,被迫修改方案,三天三夜才恢复系统。CTO 也废了!


②利用 Sharding-Jdbc,TSharding 等以 Jar 包形式呈现的轻量级组件分库分表。


缺点是,会有一定的代码开发工作量,对业务有一些侵入性。好处是对程序员透明,程序员对分库分表逻辑的把控会更强,一旦发生故障,排查问题会比较容易。


稳妥起见,我们选用了第二种方案,使用更轻量级的 Sharding-Jdbc。

做系统重构前,我们首先要确定重构的目标,其次要对未来业务的发展有一个预期。这个可以找相关业务负责人了解,根据目标和业务预期来确定重构方案。


例如,我们希望经过本次重构,系统能支撑两年,两年内不再大改。业务方预期两年内日单量达到 1000 万,相当于两年后日订单量要翻 10 倍。


根据上面的数据,我们分成了 16 个数据库。按日订单量 1000 万来算,每个库平均的日订单量就是 62.5 万(1000 万/16),每秒最高订单量理论上在 1250 左右( 2000*(62.5/100) )。


这样数据库的压力基本上是可控的,而且基本不会浪费服务器资源。


每个库分了 16 张表,即便按照每天 1000 万的订单量,两年总单量是 73 亿(73 亿=1000 万*365*2),每个库的数据量平均是 4.56 亿(4.56 亿=73 亿/16),每张表的数据量平均是 2850 万(2850 万=4.56 亿/16)。


可以看到未来两到三年每张表的数据量也不算多,完全在可控范围。


分库分表主要是为了用户端下单和查询使用,按 user_id 的查询频率最高,其次是 order_id。


所以我们选择 user_id 做为 sharding column,按 user_id 做 hash,将相同用户的订单数据存储到同一个数据库的同一张表中。


这样用户在网页或者 App 上查询订单时只需要路由到一张表就可以获取用户的所有订单了,这样就保证了查询性能。


另外我们在订单 ID(order_id)里掺杂了用户 ID(user_id)信息。


简单来说,order_id 的设计思路就是,将 order_id 分为前后两部分,前面的部分是 user_id,后面的部分是具体的订单编号,两部分组合在一起就构成了 order_id。


这样我们很容易从 order_id 解析出 user_id。通过 order_id 查询订单时,先从 order_id 中解析出 user_id,然后就可以根据 user_id 路由到具体的库表了。


另外,数据库分成 16 个,每个库分 16 张表还有一个好处。16 是 2 的 N 次幂,所以 hash 值对 16 取模的结果与 hash 值和 16 按位“与运算”的结果是一样的。


我们知道位运算基于二进制,跨过各种编译和转化直接到最底层的机器语言,效率自然远高于取模运算。


有读者可能会问,查询直接查数据库,会不会有性能问题?是的。所以我们在上层加了 Redis,Redis 做了分片集群,用于存储活跃用户最近 50 条订单。


这样一来,只有少部分在 Redis 查不到订单的用户请求才会到数据库查询订单,这样就减小了数据库查询压力,而且每个分库还有两个从库,查询操作只走从库,进一步分摊了每个分库的压力。


有读者可能还会问,为什么没采用一致性 hash 方案?用户查询最近 50 条之前的订单怎么办?请继续往后看!


管理端技术方案


分库分表后,不同用户的订单数据散落在不同的库和表中,如果需要根据用户 ID 之外的其他条件查询订单。


例如,运营同学想从后台查出某天 iphone7 的订单量,就需要从所有数据库的表中查出数据然后在聚合到一起。


这样代码实现非常复杂,而且查询性能也会很差。所以我们需要一种更好的方案来解决这个问题。


我们采用了 ES(ElasticSearch)+HBase 组合的方案,将索引与数据存储隔离。


可能参与条件检索的字段都会在 ES 中建一份索引,例如商家,商品名称,订单日期等。所有订单数据全量保存到 HBase 中。


我们知道 HBase 支持海量存储,而且根据 Rowkey 查询速度超快。而 ES 的多条件检索能力非常强大。可以说,这个方案把 ES 和 HBase 的优点发挥地淋漓尽致。


看一下该方案的查询过程: 先根据输入条件去 ES 相应的索引上查询符合条件的 Rowkey 值,然后用 Rowkey 值去 HBase 查询,后面这一步查询速度极快,查询时间几乎可以忽略不计。


如下图:

该方案,解决了管理端通过各种字段条件查询订单的业务需求,同时也解决了商家端按商家 ID 和其他条件查询订单的需求。如果用户希望查询最近 50 条订单之前的历史订单,也同样可以用这个方案。


每天产生数百万的订单数据,如果管理后台想查到最新的订单数据,就需要频繁更新 ES 索引。在海量订单数据的场景下,索引频繁更新会不会对 ES 产生太大压力?


ES 索引有一个 segment(片段)的概念。ES 把每个索引分成若干个较小的 segment 片段。


每一个 segement 都是一个完整的倒排索引,在搜索查询时会依次扫描相关索引的所有 segment。


每次 refresh(刷新索引) 的时候,都会生成一个新的 segement,因此 segment 实际上记录了索引的一组变化值。由于每次索引刷新只涉及个别 segement 片段,更新索引的成本就很低了。


所以,即便默认的索引刷新(refresh)间隔只有 1 秒钟,ES 也能从容应对。


不过,由于每个 segement 的存储和扫描都需要占用一定的内存和 CPU 等资源,因此 ES 后台进程需要不断的进行 segement 合并来减少 segement 的数量,从而提升扫描效率以及降低资源消耗。


MySQL 中的订单数据需要实时同步到 Hbase 和 ES 中,那么同步方案是什么?







请到「今天看啥」查看全文


推荐文章
教你看穿男人的心  ·  撒娇是门学问,教你如何玩转男人心!
8 年前
汽车咨询中心网  ·  一张图带你看清ADAS产业链
8 年前