一种处理亿级聚合数据的方法

本文出自“淘系技术公众号”,为本人发表的文章

背景

在电商平台的架构体系中,商品数据是系统正常运转的基石,随着平台的发展,商品数据很容易突破亿级。在电商运营方面,平台通常需要举行各种大促,使用各种营销工具吸引消费者,因此需要对商品进行招商、选品、投放。

在大促招商后,平台会根据活动报名记录数据进行一系列的运营,活动报名记录通常根据某些维度进行了聚合,比如卖家聚合维度、活动聚合维度。对某一聚合维度的商品报名数据进行处理之前,首先需要获取这一聚合维度下面的所有数据。

如果数据量比较小,数据可以采用单库单表存储,获取某一聚合维度下面的所有数据也是比较简单的事情。但是,报名记录数据量通常非常大,需要采用分库分表存储数据,由于数据分散存储,获取分库分表数据将变得困难,特别是需要获取指定某一聚合维度的所有数据。比如,某个大促下面所有的报名记录有几千万条,因此不能使用单表存储,需要分库分表存储,这样就是导致获取某个大促的所有报名记录变得困难。

本文讲解方法的就是,如何高性能分布式地获取按维度聚合的分库分表数据。本文用大促报名记录模型来说明本文的方法,对于其他类似的模型也可以适用。

相关数据模型介绍

目前主流的数据存储方案还是MySql,虽然使用MySql存储大数量级数据需要进行复杂的水平拆分和垂直拆分,但是,MySql拥有优异的性能、灵活的索引方式等,这非常有利于复杂的业务需求。本文讨论基于Mysql数据库,存储引擎为Innodb,分库分表采用TDDL。为了更好的讲述本文的方法,接下来简单定义几个相关的数据模型,这些数据模型是大促技术体系中的一个缩影,能代表大多数业务的数据模型。

基础商品模型

基础商品模型描述的商品的基础属性,其他数据模型通过商品ID关联基础商品数据模型,基础商品模型通过卖家ID关联卖家模型,基础商品数据模型如下所示。基础商品数据量通常非常大,需要进行分库分表存储。对于基础商品表,通常查询方式会指定商品ID获取商品的基础信息,并且不会对该表有过多的查询方式,因此基础商品表通常使用商品ID作为分表键,即满足查询需求,又能够把数据均匀分布在所有分表中。

id                  bigint unsigned      主键ID
gmt_create          datetime             创建时间
gmt_modified        datetime             修改时间
item_id             bigint unsigned      商品ID
seller_id           bigint unsigned      商家ID
name                varchar              商品标题
price               bigint unsigned      商品价格
picture_url         varchar              商品图片url

活动数据模型

活动数据模型描述了一个活动的基础属性,活动数据模型如下所示。其中activity_id表示活动ID,major_id表示大促ID,一个大促下面存在多个活动,即一个大促ID可以关联多个活动ID,这种一对多的关系表示了它们之间的聚合关系,活动按照大促聚合维度进行了聚合。活动数据量通常不会非常大,因此通常采用单库单表存储。

id                  bigint unsigned      主键ID
gmt_create          datetime             创建时间
gmt_modified        datetime             修改时间
activity_id         bigint unsigned      活动ID
major_id            bigint unsigned      大促ID
name                varchar              活动名称
start_time          datetime             活动开始时间
end_time            datetime             活动结束时间

报名记录数据模型

报名记录数据模型描述了商品报名某个活动的报名信息,报名记录数据模型如下所示。一个商品可以报名多个活动,一个活动的报名记录可以由很多商家的商品报名组成。通过上面的数据模型,可以发现活动报名记录按照多种聚合维度进行了聚合,比如,卖家聚合维度、活动聚合维度、大促聚合维度。业务上需要对不同聚合维度的所有数据进行处理,因此就需要有一种方法能够高性能地获取某一聚合维度下的所有数据。

id                  bigint unsigned      主键ID
gmt_create          datetime             创建时间
gmt_modified        datetime             修改时间
activity_id         bigint unsigned      活动ID
item_id             bigint unsigned      商品ID
seller_id           bigint unsigned      卖家ID
activity_price      bigint unsigned      商品活动价格
aount               bigint unsigned      优惠商品件数
buyer_limit         bigint unsigned      买家限购件数

其他相关方法描述

在进入本文主题之前,先描述下我了解到的解决该问题的相关方法。一些相关的方法通常采用商品ID作为活动报名记录表的分表键,这样数据可以比较均匀地分布到所有的分表中。对某一聚合维度的数据进行分布式获取时可以采用以下三种方案。

全表扫描方法

该方法思想很简单,只需对所有分表进行扫描,对每个分表进行处理时,可以采用索引提升获取数据的性能。这种方式实现简单,但是无法充分发挥分布式架构的性能,整体性能较低。

横向分片方法

  1. 根据聚合维度信息遍历所有分表得到每个分表的最大主键ID和最小主键ID,下图中min1表示分表1的最小主键ID、max1表示分表1的最大主键ID;
  2. 预估数据总量,数据总量等于每个分表中max-min相加的总和,如下图中count的计算公式;
  3. 根据预估数据总量count和固定每页大小划分成多个小任务进行分布式处理,下图中假设每页间隔大小为10,任务划如下图所示;
  4. 对每个小任务进行处理,每个小任务中包含主键范围,根据主键范围依次与每个表的最小主键和最大主键进行对比,判断该任务主键范围落在哪个表中,最后根据主键范围及其他条件获取数据。

缺点

  1. 对于数据分布稀疏的聚合维度,该方法会导致预估数据总量偏大很多,分批任务数量非常大,实际获取的数据可能很少,获取数据效率低;
  2. 该方案需要频繁重复的查询每个表中的最小和最大主键,获取数据的性能不高,容易造成数据库连接池满问题;
  3. 该方案分片处理方式实现复杂、不易于理解。

纵向分片方法

  1. 根据聚合维度信息遍历所有分表得到每个分表的最大主键ID和最小主键ID,下图中min1表示分表1的最小主键ID、max1表示分表1的最大主键ID;
  2. 预估数据总量,数据总量等于所有分表中的最大主键减最小主键,如下图中count的计算公式;
  3. 根据预估数据总量count和固定每页大小划分成多个小任务进行分布式处理,下图中假设每页间隔大小为10,任务划如下图所示;
  4. 对每个小任务进行处理,每个小任务中包含主键范围,遍历所有分表判断任务主键范围是否落在最小主键和最大主键之间,从符合条件的表中根据主键范围及其他条件获取数据。
    在这里插入图片描述

缺点

  1. 对于数据分布密集的聚合维度,该方法会导致预估数据总量偏小很多,分片任务数量较少,每个任务包含大量数据,导致获取数据性能不高;
  2. 同样,该方案需要频繁重复的查询每个表中的最小和最大主键,获取数据的性能不高,容易造成数据库连接池满问题;
  3. 该方案分片处理方式实现复杂、不易于理解。

聚合维度降维方法

有了以上准备知识,本节开始讨论本文的方法。本文提出的方法采用聚合维度降维的思想,找到一条合适的降维路径将高聚合维度降低到低聚合维度,然后对最低聚合维度对数据进行分片处理。降维和分片的作用就是将大任务拆分成小任务,然后将小任务通过消息中间件进行分布式处理。

分库分表的设计

分片键的选择

分库分表的设计与业务相关性比较大,本文讨论一种比较常见的场景,适用于大多数的业务。分片键的选择直接影响数据分布、获取方式、获取的性能。分片键的选择有两大因素需要考虑,一个是数据获取方式,另一个是数据分布。数据获取方式需要根据具体的业务进行判断,比如,商品详情展示会根据商品ID查询基础商品表,圈品操作会根据活动ID或者卖家ID获取所有相关的商品。另外,数据需要比较均匀地分布到所有的分表中,这样才能保证不会因为数据倾斜导致获取数据时的性能问题。

基础商品表的分片键的选择有商品ID或者卖家ID。对于基础商品表,通常查询方式会指定商品ID获取商品的基础信息,并且不会对该表有过多的查询方式。因此,基础商品表采用商品ID作为分片键比较合适,根据商品ID对分表总数取模可以得到分表的位置。采用商品ID作为分片键可以比较均匀地将数据分布在所有表中。

报名记录表的分片键的选择有活动ID、商品ID、卖家ID。首先,可以排除活动ID作为分片键的选择,因为根据活动ID进行分库分表显然会导致数据倾斜严重。如果选择商品ID作为分表键,数据可以比较均匀分布到所有表中,但是根据聚合维度查询所有报名记录数据会出现性能问题,因为同一聚合维度的数据会分散到所有表中,获取数据时需要对所有表进行遍历,并且比较困难进行分布式处理。如果选择卖家ID作为分表键,同一个卖家的数据会聚合到同一个分表中,这样有利于对同一个卖家的数据进行获取,获取数据的效率也会非常高,但是在数据分布方面,个别超级卖家会拥有几十万以上的商品,特别是天猫超市卖家拥有超过2千万的商品,这些超级卖家都是电商平台自营的。

自定义分表键

从上面的分析可以知道,报名记录表采用商品ID或卖家ID作为分表键都不是非常合适的选择。如果同一聚合维度的数据分散到很多表中会导致获取性能不高,如果同一聚合维度的数据过度聚合到同一分表中会导致数据倾斜,同样也会导致获取性能不高。

本文介绍的方法,其中一个核心思想就是寻找一个合适的聚合维度将这个维度的数据聚合在同一分表中,既不会导致数据倾斜,有能够提高获取数据的性能。通过上面的分析可以知道卖家维度是比较合适的维度,但是,超级卖家会导致数据倾斜。因此,本文选择在卖家维度的基础上进行改进,采用自定义维度作为分表键,即在报名记录数据模型中增加一列sharding_key作为分表键。

对于普通卖家,自定义分表键sharding_key等于卖家ID。对于超级卖家,预先设置好该超级卖家存储的虚拟分表,比如,一个超级卖家所有的数据均匀分布到0-15号虚拟分表中,在业务逻辑层根据商品ID作为虚拟分表键对虚拟分表总数取模得到虚拟分表的序号,自定义分表键shardingKey就等于“#{卖家ID}_#{虚拟分表序号}”。流程图如下:
yuque_diagram.jpg

分布式获取的设计

某一聚合维度的报名记录数据总数可能超过亿级,如此庞大的数据总量如何高性能的获取,采用分布式方案是非常合适的。因此,需要将包含亿级数据的大任务拆分成小任务,然后将小任务分发给集群中所有的机器执行,本文采用消息中间件进行分发任务。

聚合维度的定义

数据之间具有一定的关联关系,多个数据可以与同一个标识相关联,而这多个数据就是根据同一个标识进行了聚合,我们把这个标识定义为一个聚合维度。

比如,一个卖家聚合维度相关的商品数据都属于同一个卖家,一个活动聚合维度相关的报名记录数据都报名了这一个活动,一个大促聚合维度相关的商品数据都参与了这个大促下面的某一个活动,一个自定义分表键聚合维度相关的报名记录数据都与这一自定义分表键相关。

最低聚合维度的定义

根据某一聚合维度可以直接在数据库具体某张表中获取到该聚合维度相关的所有数据,我们称这个聚合维度为最低聚合维度。因此,报名记录表中自定义分表键聚合维度属于最低聚合维度,因为根据一个自定义分表键聚合的报名记录数据都存储在同一个物理分表中,根据自定义分表键可以定位到具体的物理表。

聚合维度的降维

聚合维度之间也具有一定的关系,高聚合维度可以由低聚合维度组成。比如,在报名记录相关的数据模型中,一个大促聚合维度由多个活动聚合维度组成,一个活动聚合维度由多个卖家聚合维度组成,一个卖家聚合维度由一个或者多个自定义分片键聚合维度组成。从上面的关系可以得知,高聚合维度可以不断降维直到最低聚合维度,其实也就是,一个高聚合维度的大任务可以通过降维方式分解成许多最低聚合维度的小任务。一个大促聚合维度的降维过程如下所示:

如何实现高性能的降维也是比较关键的点,影响降维性能的核心应该是获取聚合维度组成部分的过程。大促ID与活动ID的关系存在活动表中,活动表是单库单表,因此只需要建立大促ID+活动ID的组合索引,查询大促ID相关的所有活动ID时就可以通过覆盖索引的方式获取,覆盖索引方式的查询性能很高。活动与报名卖家的关系存储在报名记录表中,报名记录表是根据自定义分片键作为分片键的分库分表,同样在报名记录表中建立活动ID+卖家ID的组合索引,查询活动ID关联的所有卖家ID时需要遍历所有物理表利用覆盖索引的方式获取,虽然需要遍历所有物理表,但是可以通过多线程处理,且使用覆盖索引查询方式的性能很高,因此获取活动相关卖家ID性能同样很高。而卖家ID与自定义分表键之间的关系属于配置关系,获取卖家相关的自定义分表键属于内存计算,性能非常高。

最低聚合维度的数据分片

得到最低聚合维度之后,我们可以直接获取到商品ID,但是,由于一个最低聚合维度相关的商品数据也有很多,为了提升商品数据获取性能,我们需要继续对数据进行分片,将一个小任务拆分成更多的小任务,充分利用集群分布式处理的能力。总体的数据分片思路如下所示:
yuque_diagram (2).jpg

最大最小主键ID获取

获取最大和最小主键ID的最简单方式是通过max和min函数,但最好不要使用这种方式,因为这种方式会因为访问数据行数过多导致性能问题。在上面讲述聚合维度的降维中提到需要建立活动ID+卖家ID的组合索引,其实可以将该索引改进一下,变成活动ID+卖家ID+自定义分表键的组合索引。这样便可以通过覆盖索引查询最大最小主键ID,性能更高,其中查询最大主键ID的SQL如下所示。

SELECT id FROM table WHERE activity_id = #{activityId} AND seller_id = #{sellerId} 
AND sharding_key = #{shardingKey} ORDER BY id DESC LIMIT 1
数据分片与获取

最低聚合维度相关的所有报名记录数据都在同一个物理表中,但这并不代表最低聚合维度相关的所有报名记录按主键ID连续分布在同一个物理表中,实际情况往往是数据非连续分散在物理表中,且有时分散范围很大。通过求主键ID的最大和最小值可以预估数据总量为最大值-最小值。如果按照预估数据总量和固定任务大小方式进行划分任务,当数据分布非常分散时,将产生大量的任务,实际其中大部分任务不包含任何报名记录数据。

固定任务大小方式划分任务
假设某最低聚合维度总共包含200条数据,最小主键ID(minId)=1000、最大主键ID(maxId)=10000000,预估数据总量为maxId-minId=9999000,每个任务固定分配500大小的数据,即需要划分9999000/500=19998个任务,第一任务的主键ID范围为[1000,1500),最后一个任务的主键ID范围为[9999500,10000000]。

因此我们需要使用一种合理的算法对数据进行分片和获取,避免因为任务划分过多而导致性能下降,避免因为单个任务包含过多的数据而导致处理性能下降。

首先,我们采用动态任务大小方式进行数据分片,任务的大小会根据预估数据总量、集群机器总数、数据分布稀疏系数、初始任务大小按照一定的算法计算出来。算法计算流程如下:
yuque_diagram (3).jpg

然后,在数据获取方面,由于无法根据任务的主键ID范围判断任务实际包含的数据总数,为了避免一次性获取大量数据导致慢sql,我们需要采用主键ID作为游标的方式分批获取数据,流程图如下:
yuque_diagram (4).jpg

效果

使用本方案的产品在实际应用场景中,根据聚合维度获取数据的速度能够达到50000 QPS,在忽略业务处理耗时的情况下,该方案获取数据的速度可以达到更高。后续会在“在线数据圈选引擎”中继续进行压测验证。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要做一个千万级的实时数据采集与聚合,您需要考虑以下几个方面: 1. 选择合适的数据存储技术:根据实时数据采集和聚合的需求,您需要选择一个高效的数据存储技术,例如NoSQL数据库(如MongoDB、Cassandra)或分布式文件系统(如Hadoop HDFS、Apache Spark)等。这些技术可以处理大量数据并支持分布式计算,使您能够轻松地扩展数据存储和处理能力。 2. 设计可扩展的架构:要支持千万级的实时数据采集和聚合,您需要设计一个可扩展的架构。这意味着您需要将系统分为多个组件,并将这些组件部署在多个节点上,以便能够快速添加新的节点以满足扩展需求。此外,您还需要确保您的架构具有高可用性和容错性,以便在节点故障时能够自动恢复。 3. 选择合适的消息队列:为了处理大量的实时数据,您需要选择一个高效的消息队列,例如Kafka、RabbitMQ等。这些消息队列可以帮助您在系统各个组件之间传递数据,并提供数据缓存和异步处理功能,从而提高整个系统的性能。 4. 实时数据处理聚合:在处理大量实时数据时,您需要选择一种高效的数据处理聚合方法。您可以选择使用流处理技术(如Apache Flink、Apache Storm、Spark Streaming等)来实现实时数据聚合和分析,从而快速处理大量数据。 5. 监控和调试:要确保系统正常运行,您需要对系统进行监控和调试。您可以使用一些监控工具(如Prometheus、Grafana等)来监视系统性能,并使用日志分析工具(如ELK Stack)来诊断问题和优化性能。 总之,要实现一个千万级的实时数据采集和聚合系统,需要综合考虑多个方面,包括数据存储技术、架构设计、消息队列、实时数据处理聚合技术、监控和调试等。 ### 回答2: 要做一个千万级的实时数据采集与聚合,首先我们需要明确以下几个关键步骤。 1. 架构设计:一个可扩展的架构是非常重要的。可以采用分布式系统的架构,使用多台服务器进行数据采集与聚合。例如,可以使用消息队列(如Kafka)来接收实时数据,并将数据发送给多个数据处理节点进行并行处理聚合。 2. 数据采集:选择合适的数据采集方式,根据实际需求采集所需的数据。可以使用爬虫技术、API接口等方式进行数据采集。在数据采集过程中,要考虑高效的并发请求和数据的稳定性。 3. 数据存储:选择适当的数据存储方式,以支持大量数据的存储和高效的数据访问。可以使用分布式数据库(如HBase、Cassandra)或者NoSQL数据库(如MongoDB)进行数据存储。 4. 数据聚合:在数据处理节点中,使用合适的算法和技术进行数据聚合。例如,可以使用流处理框架(如Spark Streaming、Flink)对实时数据进行处理聚合,并将结果存储在数据库中。 5. 实时性保障:为了保证实时性,可以使用数据分区和分片的方式,将数据划分为多个小部分进行并行处理。同时,合理设置数据处理节点的数量和资源分配,以确保数据能够及时地处理聚合。 6. 监控与优化:建立合适的监控系统,及时发现和解决系统中的问题。通过监控数据采集与聚合的速度、延迟和数据准确性等指标,进行系统性能优化和调整。 总之,要实现千万级的实时数据采集与聚合,需要有一个可扩展的架构设计,合理选择数据采集和存储方式,使用适当的算法和技术进行数据聚合,同时保障实时性和数据准确性,并进行监控和优化。 ### 回答3: 要实现千万级的实时数据采集和聚合,可以按照以下步骤进行: 1. 确定需求和设计目标:明确要采集和聚合数据类型、规模和实时性要求,以及系统的响应时间和可用性要求。 2. 架构设计:采用分布式系统架构,包括数据源采集、数据传输、数据存储和实时计算四个模块。数据源采集模块负责从各种数据源获取数据,并进行初步清洗和处理数据传输模块负责将采集到的数据传输到存储和计算节点;数据存储模块负责存储数据,并提供高速读取和写入的能力;实时计算模块负责对存储的数据进行实时聚合和计算。 3. 数据采集和传输:根据数据类型选择合适的采集方式,如API接口、数据流、日志文件等。使用高效的数据传输方案,如消息队列或分布式文件系统,保证数据的可靠传输和低延迟。 4. 数据存储:选择合适的存储系统,如分布式数据库或列存储数据库,以满足千万级数据存储和读写的性能要求。使用数据分片和分区技术来提高存储系统的扩展性和并发性。 5. 实时计算:采用流式计算框架,如Apache Flink、Apache Storm等,进行实时数据聚合和计算。利用窗口和聚合函数来实现对数据的实时统计和计算,并将结果存储和输出。 6. 数据质量和监控:建立数据质量监控系统,包括数据完整性、一致性和准确性的监控和验证。通过监控指标和报警系统,及时发现和处理数据异常和故障。 7. 可伸缩性和高可用性:采用水平扩展的方式,根据需求增加存储和计算节点。保证系统的高可用性,使用集群技术和备份机制,防止单点故障导致整个系统不可用。 8. 性能优化和调优:通过合理的分区和索引策略、数据压缩和缓存等方式来提高系统的性能和效率。定期进行性能测试和调优,保证系统在高负载和大数据场景下的稳定和可靠性。 通过以上步骤,可以实现一个千万级的实时数据采集与聚合系统,满足实时性要求,并保证数据的准确性和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值