• 电商API接口——实现数据同步的实时数据封装接口


    常见的数据同步/集成场景多发生于不同的存储系统、不同的存储格式,如从 mysql 同步数据至数仓、excel 或 csv 导入数据库中,但是众多数据同步解决方案很少涉及从 http 接口同步数据。

    如淘宝、拼多多等电商平台平台,平台内部不同团队之间的数据打通,很多的开源数据集成工具可以满足大部分场景。但是像三方卖家入驻电商平台后,就需要在电商平台注册自研应用,通过开放平台提供的接口打通自研应用与电商平台的数据壁垒,准确及时地获取到商品、订单、物流、售后以及评价等实时变更数据,而无法实现数据库 binlog 或消息队列等形式的数据同步。

    本系列文章描述在对接快手、淘宝等开放平台同步商品、详情,订单、售后单等数据的一些经验总结和实践方案。

    数据同步接口

    根据数据量和实时性的不同需求,开放平台数据接口查询接口总共有 2 种形式:

    • 详情接口。

      • 详情接口又分为单详情接口和批量详情接口。如查询店铺信息,单详情接口只支持根据单个店铺 id 获取店铺信息,而批量详情接口可以同时传入 20 个店铺 id,在需要获取 100 个店铺信息的情况下,前者需要发起 100 次调用,后者只需要调用 5 次。

      • 单实体和多实体。如获取店铺详情,使用店铺 id 即可获取,而对于 sku 库存详情,需要同时传入 sku id 和仓库 id。对于 sku 库存,需要同时遍历 sku 和仓库,才能获取完整的 sku 库存数据。

    • 列表接口,或者分页接口。比如店铺下的商品列表接口,仓库列表,平台类目列表等。

    • 增量接口。支持时间范围查询的分页接口。比如订单列表接口,根据订单修改时间分页查询。

    而数据同步的难点也在于对支持时间范围查询的分页接口准确、及时地同步数据,下面也主要介绍增量接口的几种实现。

    增量接口

    1.普通形式

    根据时间范围进行分页请求的基本实现,请求参数中带有开始结束时间加上分页参数。

    请求参数和响应结果如下:

    1. {
    2.  "startTime""2021-11-11 00:00:00",
    3.  "endTime""2021-11-12 00:00:00",
    4.  "pageIndex"1,
    5.  "pageSize"50 
    6. }
    7. [
    8.   {...},
    9.   {...},
    10.   {...}
    11. ]

    请求接口时需要携带时间范围和分页信息,而接口响应为数据列表,响应不包含分页信息,判断是否请求下一页,需要根据当前响应数据列表 size 是否等于请求参数中的 pageSize。github 的 list-commits 就是此种类型。

    2.响应包含分页信息

    普通形式接口响应是不包含分页信息,github 的 list-commits 接口获取分页信息需要一些特殊的技巧。

    在管理后台类型的 web 页面中经常可以看到支持分页的列表页,这种页面接口的返回数据一般如下:

    1. {
    2.  "startTime""2021-11-11 00:00:00",
    3.  "endTime""2021-11-12 00:00:00",
    4.  "pageIndex"1,
    5.  "pageSize"50 
    6. }
    7. {
    8.  "dataCount"10000,
    9.  "pageIndex"1,
    10.  "pageSize"50,
    11.  "datas": []
    12. }

    接入方在判断是否有一页的时候,可以判断 pageIndex * pageSize 是否大于 dataCount 来决定是否继续请求。

    这种响应结果,接入方可以根据 dataCount 和 pageSize 计算得出 maxPageIndex,因此分页请求形式可以从第一页请求到最后一页,也可以从最后一页请求到第一页。

    一般情况下,开发者都会直接从第一页开始请求,直到最后一页,什么情况下才会出现不得不从最后一页请求呢?当出现修改时间陷阱的时候。

    修改时间陷阱

    开放平台往往也会根据业务需求,提供不同类型的时间范围,比如订单场景,有订单创建时间,修改时间,支付时间,出库时间等。一般情况下,订单创建时间、支付时间、出库时间等一旦确定就不会再改变,修改时间为每次订单发生状态变更如支付、发货、确认收货等都会将修改时间置为当前时间。如果使用不会变动的订单创建时间等来同步,一旦订单发生变更,如出现退货退款,这些时间是无法感知到订单实时变动的,而使用修改时间,无论是订单创建、还是用户支付、卖家发货,一旦订单有变更,都会反映到修改时间,从而可以实时地获取订单状态变更。

    对于数据接入者来说来说,最喜欢的时间类型为修改时间,根据修改时间同步可以确保数据发生修改后实时地同步过来。

    因为订单状态每次发生变更,修改时间都会变动。如果正好处在同步时间区间内的订单发生了变更,会存在一个丢数据的陷阱:必须按照修改时间降序进行同步,原因如下:

    假设有订单数据在某个时间范围内从左向右按照修改时间升序排序,效果如下:

    1 2 3 4 5    6 7 8 9 10    11 12 13 14 15    16 17 18 19 20
    

    在同步这个时间范围内,如果 20 条数据没有发生任何修改,分页拉取第一页 1 ~ 5,第二页 6 ~ 10 等可以确保数据不会漏。

    但是在拉取第一页 1 ~ 5 后,数据 5 发生修改,其余数据不变,拉取第二页时再按照修改时间升序排序,此时排序效果如下:

    1 2 3 4 6    7 8 9 10 11    12 13 14 15 16    17 18 19 20
    

    可以看到数据 6 被归到第一页中,之后拉取第二页时会从数据 7 开始,数据 6 被遗漏。

    按照修改时间同步,需要分页请求多次才能同步完成时,中间数据发生修改会导致数据排序顺序发生变动,从而造成数据遗漏。

    处理方式也很简单,从最后一页向前拉即可。

    同样是上面的 20 条数据,在同步完最后一页数据 16 ~ 20 条后,数据 5 发生变更,同步倒数第二页时,同步到的数据为 12 ~ 16,逐步推进分页,在最后一次请求中同步到数据 1 ~ 6。

    倒着拉虽然不会出现数据遗漏的情况,但是可以观察到数据 16 是被重复拉取的,接入方需要做好数据更新的幂等性。

    数据接入时需确定时间类型,会不会发生变动,以及排序顺序:

    • 开放平台根据修改时间升序排序时,接入方需要从后向前翻页;

    • 开放平台根据修改时间降序排序时,接入方需要从前往后翻页。

    3.翻页优化:取消 dataCount ,使用 hasNext

    对于分页接口,普遍存在深翻页潜在风险。开放平台在实现分页接口时,会尽量减少深翻页的影响或者直接规避深翻页,使用 hasNext 代替 dataCount 就是一种减少翻页消耗的手段。

    请求参数和响应结果如下:

    1. {
    2.  "startTime""2021-11-11 00:00:00",
    3.  "endTime""2021-11-12 00:00:00",
    4.  "pageIndex"1,
    5.  "pageSize"50 
    6. }
    7. {
    8.  "hasNext"true,
    9.  "pageIndex"1,
    10.  "pageSize"50,
    11.  "datas": []
    12. }

    对于关系型数据库如 mysql,返回结果中的 dataCount 和 datas 无法在一条 sql 中查询获取,需分别执行一次 count 和 select limit offset 请求才能获取 dataCount 和 datas

    接口响应包含 dataCount 字段的目的是为了方便接入方判断何时中止分页请求,开放平台使用 boolean 类型的 hasNext 字段,帮助接入方判断是否到达最后一页。

    开放平台该如何确定 hasNext 的值呢?执行 select 语句时设置 size 为 pageSize + 1,如果查询语句返回数据量为 pageSize + 1,存在下一页,否则不存在。

    接口不返回 dataCount 字段,可以避免 count 查询,极大提高接口性能。

    淘宝开放平台接口大量采用此种形式,如查询卖家已卖出的增量交易数据(根据修改时间)。

    使用 hasNext 时需要开放平台根据修改时间降序排列,因为接入方无法实现从后向前翻页。

    4.翻页优化:取消 pageIndex,使用 cursor

    分页接口普遍惧怕深翻页,深分页会拖累接口响应时间,对数据库造成较大压力,带来潜在的系统崩溃风险,而开放平台保证稳定性必须小心处理深分页。

    深分页 sql 的问题在于过深的排序:select * from table limit 50 offset 1000000

    一般解决深分页的思路是使用 search afterselect * from table where id > 2000000 limit 50

    cursor 就是用来实现 search after 的一种方式:取 datas 的最后一条数据的 timestame + id 作为 cursor,交互时第一次请求时 cursor 为空不传入,之后每次请求传入响应中 cursor 值,直到 cursor 返回一个特殊标识,分页结束。

    请求参数和响应结果如下:

    1. {
    2.  "startTime""2021-11-11 00:00:00",
    3.  "endTime""2021-11-12 00:00:00",
    4.  "cursor""1639487400913_5",
    5.  "pageSize"50 
    6. }
    7. {
    8.  "cursor""1639487400918_10",
    9.  "pageSize"50,
    10.  "datas": []
    11. }

    开放平台接口对 cursor 进行解析构建 select 语句:

    1. select * 
    2. from table 
    3. where id > ${id} 
    4. and time >= ${timestame} 
    5. and time < ${endTime} 
    6. order by time, id
    7. limit ${pageSize} 

    快手开放平台大量使用采用此种形式,如获取订单列表v2。

    接口常见限制

    开放平台为了保护接口安全,会对接口进行一定限制:

    • 开始时间结束时间相差不能过大以避免深翻页。比如不能超过 7 天

    • 只允许同步近期数据。比如订单类只允许同步 90 天内订单

    • 减少每次请求数据量。比如限制每页数据最多 100 条

    • 接口限流。限制接口请求并发和 qps

  • 相关阅读:
    指针和数组关系-从例题的角度分析
    基于C++和C#窗体实现各种无损压缩算法并进行比较分析
    06【数据库的约束】
    弹性IP(弹性公网IP)和固定IP的区别
    Flutter 基本概念
    【点云上采样】最近邻插值上采样算法
    SpringBoot实践(三十四):Gradle的使用
    【软考经验分享】软考-中级-嵌入式备考
    C++编程常见错误及处理
    Redis主从复制的核心原理
  • 原文地址:https://blog.csdn.net/onebound_linda/article/details/134073409