以旧换新项目中的微服务实践

微服务在近几年比较火,公司也在使用微服务架构。我在最近参与开发的以旧换新项目中也进行了一些微服务相关的实践,在这里总结一下遇到的问题和收获。

项目简介

该项目主要是针对印度商城,用户用旧手机换取优惠券,抵扣购买新手机的部分费用。流程如下图所示,用户在web端输入手机IMEI信息,后端调用cashify的接口获取报价及报价id,或者用app进行检测(使用cashify提供的SDK)获取报价和报价id,再调用以旧换新服务生成以旧换新券。用户下单的时候可以使用券,调用支付组成服务以及订单组的接口完成交易,物流组负责发货相关功能,实际配送由第三方物流公司Ekart完成。Ekart将新手机派送给用户,并回收用户的旧手机,回收时会进行质检,没问题了会把旧手机派送给cashify,cashify把旧手机的钱打给我们。
以旧换新流程图

我主要负责和cashify对接接口、商城后端生成以旧换新券流程中的相关接口以及以旧换新券服务。以旧换新券服务使用公司的SOA框架,其它项目通过thrift调用以旧换新券服务。

使用微服务的原因

以旧换新券被做成了一个单独的服务,主要有一下几点好处:

  • 首先,比较安全。作为独立的服务,可以限制只有内网的ip才能访问,不对外暴露;此外,由于使用thrift协议,需要使用SDK才能请求相应的接口,很大程度上避免了恶意攻击;

  • 其次,以旧换新券服务的代码和数据库都部署在独立的机器上,即使该服务挂了,也不会影响商城其它的功能。当遇到流量比较大的情况时,可以单独给服务加机器,提高机器的使用率;

  • 再者,可以降低业务的耦合度,避免单体应用代码过于庞大臃肿的情况。

遇到的问题和解决方法

服务与商城后端解耦不够充分

最开始我考虑的是,以旧换新券后续可能还有别的生成途径,不一定要通过请求cashify的接口获取报价来生成,于是我就把请求cashify的逻辑都放到商城后端来做,请求完之后再调以旧换新券服务生成券,后来发现这样做存在很大的问题:在PC或m站,用户输入手机的IMEI号来生成以旧换新券,商城后端得先调用服务判断该IMEI号是否已经生成了券,生成的券是否已经过期,如果没有生成过券或者生成的券已经过期了就会调用cashify的接口获取报价和报价ID等信息,并根据这些信息再次调用服务生成券,而服务中生成券逻辑需要再次查询数据库判断是否满足生成券的条件,相当于商城和服务中有重复的代码。于是将调用cashify接口的逻辑迁移到服务中进行,使服务和商城后端不再耦合。

其实最开始的思路是有问题的,即使以旧换新券还有别的生成途径,这些和生成券相关的逻辑都应该放到一起,充分和商城代码解耦。

并发情况下数据不一致的问题

上面提到过,在PC或m站生成券的时候会先根据IMEI号判读是否满足生成券的条件,如果满足就会调用cashify的接口获取报价和报价ID并生成券,记录在数据库中。但是当两个相同的IMEI号同时请求时就会有问题,这两个请求同时去查数据库发现该IMEI号满足生成券的条件,就都去请求cashify的接口,这时cashify会生成两个报价ID,最终数据库保存的是先生成的那个报价ID,而最新生成的报价ID才是有效的,这就导致在优惠券使用之后订单组根据报价ID回调cashify的时候不成功。

最终解决方法是在redis里设置一个过期时间为30秒的key,标记某个IMEI在30秒之内是否已经请求过这个接口,如果请求过,直接返回请求过于频繁的信息,保证同一个IMEI不会同时请求cashify的接口生成两个报价ID;再就是根据日志修复之前有问题的数据;另外,之所以正常情况下(不是刷接口)也会出现两个相同的IMEI号同时请求,是因为用户在点击生成按钮之后发现没有反应,或者是手抖,连续点了两下,所以前端也需要进行相应的优化,在用户点击一次之后,显示一个类似loading的效果,不让用户继续点。

抢购流量大对以旧换新券服务造成压力

付款页面会调以旧换新券服务获取当前用户可用的券,抢购的时候流量比较大,服务接口的错误率比较高,直接导致有些用户付不了款。解决的方法如下:

  • 每次用户生成以旧换新券的时候都在redis里加个key标记该用户有以旧换新券,该key的过期时间和券的过期时间一致。在付款页面先通过redis里的标记判断该用户是否有以旧换新券,有才会去请求服务,没有直接不请求,这样就将没有以旧换新券的流量都挡在了外面,极大的减轻了服务的压力。需要说明的一点是,用户使用券的时候也不去改这个标记,因为用户可能有多张券,维护这个标记的成本很高,就算用户只有不可用的券,大不了这部分用户的流量都打到服务上,压力也不会很大。
  • 在付款页面将调用以旧换新券列表的接口降级,即使接口有问题也不报错,不影响付款流程的进行。

接口幂等

退款的时候为了保证整个退款流程走通,券的退回接口可能会被调用多次,因此退回接口设置了幂等,即使某张券已经退回成功,如果再次请求退回该张券,还是返回成功(不需要修改数据库)。支付组成服务在调以旧换新券服务的退回接口时,可能由于网络原因没有接收到正确的返回结果(实际上已经退回成功),这时会重试三次,如果还是失败,就交给消息队列处理,由于接口设置了幂等,最终调用方会收到退回成功的返回结果。

错误号和Log ID

商城和服务中的错误号最好是统一的,二者之间不要有冲突,这样排查问题的时候更方便。对于同一个请求,商城和服务中用同一个Log ID来记录日志,便于追踪问题(这点项目之前的逻辑已经是这样做的)。