前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >海量交易订单查询没做“重试”,一哥们“喜提”P3故障!

海量交易订单查询没做“重试”,一哥们“喜提”P3故障!

作者头像
JAVA葵花宝典
发布2020-10-09 14:55:17
8200
发布2020-10-09 14:55:17
举报
文章被收录于专栏:JAVA葵花宝典JAVA葵花宝典

一、故事情节

以下情节根据真实事件改编!

由于现在PDD模式比较火,某大厂的一哥们,接到老板的需求,做一个拼团业务,具体的业务需求是这样的:

1、一个拼团活动有开始时间和结束时间;

2、某一商户下的某一商品当有第一个用户购买的时候,创建一个拼团单,后边用户在当前商户购买当前商品的时候,直接加入该拼团单(商户ID和商品ID决定一个拼团单);

3、拼团活动结束后使用定时任务判断是否成团,如果某一商品的拼团单购买的商品数量到达拼团的最低门槛,则拼团成功,否则拼团失败;

4、触发定时任务的时候,拿着所有的拼团单,查询每一个拼团单对应的交易单;

5、判断某一拼团单用户的下单数量是否达到最低门槛,决定拼团成功失败;

提示:画图工具使用的是OmniGraffle

因为知道,交易库数据量巨大,再加上业务刚上线,被交易那边限流,这哥们还特意进行了分页查询,每页查询50条数据,定时任务也采用主子任务的方式,拆分子任务处理拼团单,尽量减少每次查询交易单的数量。

业务刚上线的时候,前几天跑的好好的,定时任务执行完全OK!但是后面不知道运营咋推的,拼团单数量直接翻倍,每个拼团单交易单数量飙升!定时任务直接跑超时了,问题经查发现是交易单查询超时!直接导致拼团活动到达结束时间,拼团活动无法在约定的时间结束,无法结算,无法发货,造成资损XX元。后来通过定时任务手动重试解决查询超时问题。这哥们理所当然的"喜提"P3故障!

注意:

听哥们说,拼团活动结束的时候,定时任务处理的拼团单的数据量峰值大概有5W+,每一个拼团活动满足成团条件的最低成交数量是10个,需要查询交易单的数据量在50W左右,即使做了任务的拆分,每一个子任务只处理几百个拼团单,每一个拼团单只查询几十个交易单数据,但是在查询某一拼团交易成功的数量时,交易接口还是超时了(哥们公司规定,接口RT时间需要保证在50ms以内,查询交易单,还特意设置了RT时间为2000ms)

二、问题所在

我们先不去喷为什么要有这样的业务需求,为什么要在定时任务里批量的处理拼团是否成功?为什么不加缓存?为什么查询的订单量这么少就超时了?为什么???

从上述的场景中,得出的结论就是,关键的接口超时没有做好重试处理,数据量上升的时候,查询超时的问题不能够自动解决!

注意:

一般接口超时,基本重试1-2次之后,都是可以ok的!除非你调用的服务彻底挂了!这样可以扔到队列里边去定时执行,上述的业务,就是通过定时任务重试的方式。在服务长时间无法访问的时候,通过整体重试的方式避免业务无法跑下去!

当然接口超时,除了重试,还有其他的方案,这里我们只介绍重试方案,因为最简单了!

三、超时的几点问题

3.1、读超时

读超时,一个比较好的地方就是你只需要进行重试就可以简单的解决问题,因为你不会牵扯到数据的变更,因此重试的时候,无需保证数据访问的幂等;

3.2、写超时

写超时(增、删、改)的话就比较麻烦了,因为你无法知道,写超时了,数据库是否发生了变更:

- 数据写入成功,返回超时了,数据库已真实变更了这条数据;

- 数据未写入,请求超时了,数据库未发生变更;

上述两种情况,返回的错误码都是TimeOut,因此无法区分,这个时候,你就需要做好幂等处理了!

三、幂等处理的几个关键

关于幂等处理的几种方式,不是本文所要阐述的内容,有需要的可以参考:《高并发下的接口幂等性解决方案!

3.1、半幂等

例如:插入一条数据,调用服务A,A服务插入数据库的时候,根据主键冲突策略,发现已经已经存在了,直接返回错误,报已经存在主键了;

这种方式,服务A幂等做的不彻底,只是保证数据不会变更,但是通过返回错误来实现,这样的话,就需要调用方先进行一次查询操作,判断数据是否存在了,如果存在则不插入,如果不存在再调用A服务插入数据了;

3.2、全幂等

相对的,如果调用的服务A,在插入数据的时候,自己先查询一下数据是否已经存在,如果存在直接返回成功,如果不存在则执行插入操作,那么调用方就就直接执行插入操作就可以了,无需自己判断数据是否已经存在了,那么接口A就是全幂等的了;

3.3、幂等需要关注的几个问题

以下几点并不是需要注意问题的全部,欢迎大家留言补充!

3.3.1、服务的调用方和服务的提供方幂等键要保证一致,唯一性,并且不变性;

这个很好理解,例如:

- 服务的调用方以为调用方是按照用户身份证号做幂等的,但其实服务提供方是按照手机号做幂等的,这样就出现问题了;

- 服务的提供方前段时间还是用A做幂等键的,后边却用B了,说变就变的也是不可以的;

因此,服务调用方在调用服务之前一定要确定好服务提供方幂等键的设置;

3.3.2、调用方不能单纯的依靠查询来做幂等

例如:用户咔咔点击了两次,两个线程执行,同时执行插入操作,两个线程都先查询,结果某一时间点查询的数据都不存在,然后就执行插入操作了,就插入了两条数据;

这个时候,就需要加锁处理了或者根据主键冲突策略等方式判断幂等了;

3.1、3.2中举例还是有瑕疵的,大家注意!

3.3.3、调用方幂等键唯一了,但是其他数据却变了,业务做好处理,具体业务具体分析

这种情况很常见,例如:服务提供方约定以手机号作为幂等键,但是服务的调用方第一次插入数据的时候,手机号是A,其他数据是B,第二次调用的时候,手机号是A,其他数据确是C,那服务提供方到底让不让你插哪?这个就需要根据具体的业务做分析了,如果业务决定,让你插,你就插,不让你插就不能插了!

3.3.4、幂等键跟随数据做好持久化,做到“有据可依”,禁止幂等键纯内存拼接

这个很好理解,举个例子吧:

插入一条数据,拼接了一个幂等键ABC,你如果不做持久化,数据存储不包含ABC三个字段,那么你下次如何判断数据是否已经存在哪?

3.3.5、消息幂等处理的几个关键

消息幂等是一个比较复杂的场景,因为消息可能存在的无序性、重复性、延迟,都增加了幂等处理的复杂性,其中重复性则是幂等的时候需要重点考虑的;

1、重复性

例如:交易系统存在下单、支付、发货行为,交易系统如果多次消费同一笔定金支付成功消息时,由于幂等问题可能导致很多问题:

一般,我们在发送交易消息的时候,会把 "订单的状态和订单ID" 作为消息体的一部分,然后在接收到消息的时候,根据消息的类型判断是不是下单消息,以及判断当前订单的状态是否是”用户下单“,这样在消息不重复消费的时候,是没有问题的。

如果出现上述情况,用户下单消息重复消费,在接收到用户支付消息的时候订单状态已经被修改为已支付,但是由于用户下单消息重复消费,消息体是没有变化的(状态没有发生变化),就又修改订单状态为待支付状态了,这里显然是不对的。

我们应该做:

我们应该在接收到消息的时候,根据订单ID去数据库查询一下订单此时的状态,然后根据当前的状态判断下一步的操作,并且消息处理的时候还要加锁哦!加锁的维度可以是订单ID!防止并发的时候,出现3.3.2中的情况!

因此,不要把可变值作为幂等的条件,加锁查询订单最新的状态!

2、无序性

保证消息的顺序消费是比较复杂的,并且成本也很高,一般我们可以根据不同的业务判断消息消费的顺序性的;

例如:用户下单消息=>用户支付消息,顺序的行为是这样消费的。但无序的时候,我们可能先接收到”用户支付消息“然后才会接收到”用户下单消息“。

如果你的业务在接收用户下单消息做的处理不影响主链路的话,则可以直接先处理”用户支付消息“,当在收到”用户下单消息“的时候,查询订单的状态已经变为”已支付“,则直接把消息幂等掉,返回true,结束消息的消费。

但是,如果你的”用户下单消息“有重要的逻辑,必须先消费了之后,才可以消费”用户支付消息“,那我们就需要特别注意了!根据查询出来的订单状态进行判断,判断是否已经消费了”用户下单消息“,当先接收到”用户支付消息“的时候,消息直接重发就可以了,等消费了”用户下单消息“之后,再消费”用户支付消息“。

3.3.5、定时任务幂等处理的几个关键

定时任务的幂等需要解决的主要问题就是”重复性“,和消息的重复消费问题大致相同,需要根据查询最新的状态进行业务的处理,这里不做过多说明;

四、如何实现优雅的重试

Show代码不是目的,讲到底才是真谛!

4.1、为什么进行重试

我们依赖的外部服务对于调用者来说一般都是不可靠的,尤其是在网络环境比较差的情况下,网络抖动很容易导致请求超时等异常情况,这时候我们就需要使用失败重试策略重新调用服务提供方的接口来获取数据。

一般网络抖动引起的接口超时,基本重试1-2次之后,都是可以ok的!

重试常见的一种方式是使用定时任务重试,例如某次操作失败,记录下来,当定时任务再次启动,则将数据放到定时任务的方法中,重新跑一遍,最终直至得到想要的结果为止。

缺点就是,依赖于定时任务工具的特性,重试机制相对简单,一般只能实现支持Cron表达式每隔多长时间进行重试;

无论是基于定时任务的重试机制,还是我们自己写的简单的重试器,缺点都是重试的机制太单一,而且实现起来不优雅,我们很难解决在什么条件下需要重试、什么条件下需要结束重试、重试等待的时间,很难监控整个重试的过程等等问题。

4.2、如何实现优雅的重试?

代码是很枯燥的,授人以鱼不如授人以渔,今天就给大家介绍两款工具:Guava-retrying和Spring-retry,让你实现优雅的重试!

4.2.1、Guava-retrying

Guava-retrying是基于谷歌的核心类库Guava的重试机制实现。

GitHub地址:https://github.com/rholder/guava-retrying

4.2.2、Spring-retry

Spring Retry则看起来更舒服了!

GitHub地址:https://github.com/spring-projects/spring-retry

有兴趣的小伙伴可以具体了解一下,这里不再赘述!

六、归纳总结

经过上述一个案例引出了重试的各种需要考虑的问题,以及重试常见的工具,这里提醒大家一定要正确的进行重试!重要接口,上线前记得压测,做好重试处理,做好监控报警配置,不要感觉自己的业务小,没有流量,一但出现问题,你就只能"喜提"故障了!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 JAVA葵花宝典 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档