常见的数据同步/集成场景多发生于不同的存储系统、不同的存储格式,如从 mysql 同步数据至数仓、excel 或 csv 导入数据库中,但是众多数据同步解决方案很少涉及从 http 接口同步数据。
如淘宝、拼多多等电商平台平台,平台内部不同团队之间的数据打通,很多的开源数据集成工具可以满足大部分场景。但是像三方卖家入驻电商平台后,就需要在电商平台注册自研应用,通过开放平台提供的接口打通自研应用与电商平台的数据壁垒,准确及时地获取到商品、订单、物流、售后以及评价等实时变更数据,而无法实现数据库 binlog 或消息队列等形式的数据同步。
本系列文章描述在对接快手、淘宝等开放平台同步商品、详情,订单、售后单等数据的一些经验总结和实践方案。
数据同步接口
根据数据量和实时性的不同需求,开放平台数据接口查询接口总共有 2 种形式:
-
详情接口。
-
详情接口又分为单详情接口和批量详情接口。如查询店铺信息,单详情接口只支持根据单个店铺 id 获取店铺信息,而批量详情接口可以同时传入 20 个店铺 id,在需要获取 100 个店铺信息的情况下,前者需要发起 100 次调用,后者只需要调用 5 次。
-
单实体和多实体。如获取店铺详情,使用店铺 id 即可获取,而对于 sku 库存详情,需要同时传入 sku id 和仓库 id。对于 sku 库存,需要同时遍历 sku 和仓库,才能获取完整的 sku 库存数据。
-
-
列表接口,或者分页接口。比如店铺下的商品列表接口,仓库列表,平台类目列表等。
-
增量接口。支持时间范围查询的分页接口。比如订单列表接口,根据订单修改时间分页查询。
而数据同步的难点也在于对支持时间范围查询的分页接口准确、及时地同步数据,下面也主要介绍增量接口的几种实现。
增量接口
1.普通形式
根据时间范围进行分页请求的基本实现,请求参数中带有开始结束时间加上分页参数。
请求参数和响应结果如下:
{"startTime": "2021-11-11 00:00:00","endTime": "2021-11-12 00:00:00","pageIndex": 1,"pageSize": 50
}[{...},{...},{...}
]
请求接口时需要携带时间范围和分页信息,而接口响应为数据列表,响应不包含分页信息,判断是否请求下一页,需要根据当前响应数据列表 size 是否等于请求参数中的 pageSize。github 的 list-commits 就是此种类型。
2.响应包含分页信息
普通形式接口响应是不包含分页信息,github 的 list-commits 接口获取分页信息需要一些特殊的技巧。
在管理后台类型的 web 页面中经常可以看到支持分页的列表页,这种页面接口的返回数据一般如下:
{"startTime": "2021-11-11 00:00:00","endTime": "2021-11-12 00:00:00","pageIndex": 1,"pageSize": 50
}{"dataCount": 10000,"pageIndex": 1,"pageSize": 50,"datas": []
}
接入方在判断是否有一页的时候,可以判断 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 就是一种减少翻页消耗的手段。
请求参数和响应结果如下:
{"startTime": "2021-11-11 00:00:00","endTime": "2021-11-12 00:00:00","pageIndex": 1,"pageSize": 50
}{"hasNext": true,"pageIndex": 1,"pageSize": 50,"datas": []
}
对于关系型数据库如 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 after
:select * from table where id > 2000000 limit 50
。
cursor 就是用来实现 search after 的一种方式:取 datas
的最后一条数据的 timestame + id 作为 cursor,交互时第一次请求时 cursor
为空不传入,之后每次请求传入响应中 cursor
值,直到 cursor
返回一个特殊标识,分页结束。
请求参数和响应结果如下:
{"startTime": "2021-11-11 00:00:00","endTime": "2021-11-12 00:00:00","cursor": "1639487400913_5","pageSize": 50
}{"cursor": "1639487400918_10","pageSize": 50,"datas": []
}
开放平台接口对 cursor
进行解析构建 select 语句:
select *
from table
where id > ${id}
and time >= ${timestame}
and time < ${endTime}
order by time, id
limit ${pageSize}
快手开放平台大量使用采用此种形式,如获取订单列表v2。
接口常见限制
开放平台为了保护接口安全,会对接口进行一定限制:
-
开始时间结束时间相差不能过大以避免深翻页。比如不能超过 7 天
-
只允许同步近期数据。比如订单类只允许同步 90 天内订单
-
减少每次请求数据量。比如限制每页数据最多 100 条
-
接口限流。限制接口请求并发和 qps