从淘宝“已买到的宝贝”开始,聊聊数据(库)异构的应用

余梦

我们经常在淘宝上买东西,当我们进入我的淘宝->已买到的宝贝的页面时,我们程序猿大神们是否会敏锐的想到这个表面上看似乎很简单的订单查询功能是如何实现的呢?我们知道,淘宝每天产生的订单量是非常大的,一段时间累积的订单量将非常巨大,在订单的查询上,已经远远不是那些数据量很小的系统的查询那么简单了,而且一个用户既可以是买家又可能是卖家,而且要按各种不同条件去查询,如何高效快速的查询买家的订单、卖家的订单?这个问题非常值得我们好好去思考下。

本人没在淘宝工作过,对于淘宝“我的订单”模块实现方案也是通过网上学习以及自己思考得来的,而且淘宝的技术架构及实现方案不断的随着需求变化而在完善,可能和我下面讲到的有很多不同的地方,这里目的不是讨论淘宝订单查询模块的实现,而在于我接下来说到的数据(库)异构,这个可以解决很多业务复杂问题的思路。


什么是数据异构?

把数据按需(数据结构、存取方式、存取形式)异地构建存储。

常见应用场景

分库分表中有一个最为常见的场景,为了提升数据库的查询能力,我们都会对数据库做分库分表操作。比如订单库,随着点我达订单量的快速增长,订单量累计数据也非常大,业务团队已经对其做过分表分库(城市id对应一个shardx值,按照shardx取模进行分片)。因为我们的订单都属于同城订单,商家的订单也只局限在同城,相比淘宝京东这类订单查询会简单一些。很多时候,开始的时候我们是按照订单ID维度去分库分表,那么后来的业务需求想按照商家维度去查询,比如我想查询某一个商家下的所有订单,就非常麻烦。这个时候通过数据异构就能很好的解决此问题。如下图所示:

总结起来大概有以下几种场景:

  1. 数据库镜像
  2. 数据库实时备份
  3. 搜索构建(比如分库分表后的多维度数据查询)
  4. 业务缓存刷新
  5. OLTP导致的相关数据变化以及重要业务消息

上面大概介绍了数据异构的概念和应用场景,那我们就回到上面提到的淘宝订单查询的问题上来。前面我提到,这个查询其实比你页面看到的要复杂很多,如果仅仅是通过普通的分表分库,一旦后面数据量太大涉及跨库跨表查询,这个查询时间开销也是不小的。我这里先谈下淘宝订单拆分设计的大概思路(不一定完全正确或者和淘宝采用的方案是一样的):

淘宝是根据订单号做拆分的,而下单中有两个维度,买家和卖家,对订单做拆分之后,必须还是可以通过买家,卖家方便的查询着两个维度的数据。该怎么办呢?这里留个疑问。我们假设淘宝拆分规则如下,淘宝将订单表拆分到20个mysql库中,而在每个库中又将订单表横向拆分为50份,相当于将一个表拆分为1000份。拆分之后事务会分散到1000套表中,这必然会大大增加并发的事务处理能力。那么问题来了,经过拆分之后如何保证买家卖家快速的查询其下的订单呢?最好的办法是保证买家,卖家下的订单在一张表中,如何保证呢?淘宝的做法是将买家的id取模后放到订单号中。对于20台服务器,每台服务器50张表只需要2位买家或卖家id的后2位数字就可以准确定位到具体的库和表。订单号中同时存在买家id的最后2位和卖家id的最后2位。分别在订单号的倒数第3,4位数和最后两位数。假定买家id为123456789,那么在订单号中的最后两位就是89,通过89对20取模就可以定位到具体的库上,通过对50取模就可以定位到具体的表上。这样买家在查询其订单时就可以通过其id获得其订单所在库以及表,就可以方便有效的查询买家订单了。这里会带来另外一个问题,卖家查询订单时怎么办?前面我们已经提到卖家和买家被分成两个不同的维度来做表设计,卖家查询时不是直接查订单表,而是通过卖家维度的表来做查询。卖家维度的表的插入,更新是通过在订单插入时发一个消息来通知插入的。即使这样做了库,表的拆分,依然会有问题。淘宝在双11时的一天的交易量非常巨大,这样几个月过去后,这些拆分后的表中的数据量也会达到很大的一个量,处理速度就会下降。淘宝的做法是把三个月之前的老数据迁移到其他库中,这样就避免了数据量增大导致的系统响应时间降低的问题。但是会带来另外一个问题,用户在查询订单时需要同时查两个库,一个是历史数据表,另一个是近期数据表;这个问题无可避免,就是通过查询两次解决。也许有人会想到拆分之后对全数据做统计会有问题。如果在拆分后的表上做统计,是肯定会有问题的。怎么做呢?其实很简单,把数据迁移到别的库中去做统计。表做拆分可以大大的提高TPS,但是也会带来一些问题,需要通过可靠的消息通知机制通知其他模块做非核心处理的事情,需要通过高效的搜索系统保证搜索数据的及时更新。淘宝订单的规则据我观察也变化过几次,比如现在,你可以看到你的订单倒数第三第四位两个数字就是你的id后两位。比如这张图 (我看了下我的淘宝订单,现在的后四位规则就是如图所示,这个截图来自淘宝内部培训ppt) 当然还有按不同查询条件去异构数据,这样,针对特定条件的查询就会简单很多。上面提到的策略也许现在已经有了很大调整,只是为了说明如何通过良好的数据异构设计去解决这类问题。上面这个例子,就是典型的数据异构去解决实际业务场景中那些原本很复杂的问题,包括很多场景下的ETL,大数据计算处理后汇总统计生成的数据,这类都属于数据异构的范畴(个人的理解,理解得不对请包涵),其目的就是把原本复杂的东西更简单的呈现出来,中间的过程对你来说透明。我突然想到计算机领域某个大神说的那句话:"Any problem in computer science can be solved by anther layer of indirection" 。这些拆分的表、或者间接生成的表,和这里的中间层是不是有点像?接下来谈下关于数据异构到哪里去的问题。

数据异构的方向

上图只是比较常见的几种数据去向,还有很多其他NOSQL数据库可能会去做存储,还有些最终会存储在数据仓库中。在日常业务开发中大致可以分为以上几种数据去向,DB-DB这种方式,一般常见于分库分表后,聚合查询的时候,比如我们按照订单ID去分库分表,那么这个时候我们要按照用户ID去查询,查询这个用户下面的订单就非常不方便了,所以我们就可以用数据库异构的方式,重新按照用户ID的维度来分一个表,像在上面常见应用场景中介绍的那样。把数据异构到redis、elasticserach、solr、mongodb中去要解决的问题跟按照多维度来查询的需求差不多。这些存储都有聚合的功能。当然同时也可以提高查询性能,应对大访问量,比如redis这种银弹(银弹一词出自《人月神话》)。

数据异构的常用方法

replication 这个很简单就是将数据库A,全部拷贝一份到数据库B,这样的使用场景是离线统计跑任务脚本的时候可以。缺点也很突出,不适用于持续增长的数据。 标记同步 这个是业务场景比较简单的时候,理想情况下数据不会发生改变,比如日志数据,这个时候可以去标记,比如时间戳,这样当发生故障的时候还可以回溯到上一次同步点,开始重新同步数据。 BINLOG方式 通过实时的订阅mysql的binlog日志,消费到这些日志后,重新构建数据结构插入一个新的数据库或者是其他存储比如es、solr等等。订阅binlog日志可以比较好的能保证数据的一致性。 MQ方式 业务数据写入DB的同时,也发送MQ一份,也就是业务里面实现双写。这种方式比较简单,但也很难保证数据一致性,对简单的业务场景可以采用这种方式。

使用Binlog,MQ进行数据异构

现在开源的订阅binlog的日志组件,使用比较广泛的是阿里的canal。其基于mysql数据库binlog的增量订阅和消费组件((点我达大数据团队结合公司业务开发了一个redis数据订阅组件,目的类似))。由于canal服务器目前读取的binlog事件只保存在内存中,并且只有一个canal客户端可以进行消费,所以如果需要多个消费客户端,可以引入MQ。如下图虚线框部分。我们还需要确保全量对比来保证数据的一致性(canal+mq的重试机制基本可以保证写入异构库之后的数据一致性),这个时候可以有一个全量同步WORKER程序来保证数据一致性(保险起见) 上图是一个典型的进行数据异构的方式,目前点我达业务团队中就有在类似的用法。

写在最后:

本文主要叙述了数据异构的使用场景,方法。这里面涉及到的RocketMQ以及Canal并没有深入分析。根据数据异构的定义,将数据异地构建存储,我们可以应用的地方就非常多,文中说的分库分表之后按照其它维度来查询的时候,我们想脱离DB直接用缓存比如redis来抗量的时候。数据异构这种方式都能够很好的帮助我们来解决诸如此类的问题。当然数据异构也会带来一些额外的副作用,比如数据的大量冗余(相对)会带来更大的存储(消耗)压力,异构数据带来的数据同步、清洗、一致性等问题等。我们最终的目的是,通过良好的数据异构去解决一些技术手段很难处理的问题。