当前位置: 首页 > 科技观察

微服后,这几点一定要注意

时间:2023-03-17 11:59:11 科技观察

本文转载自微信公众号《二玛读书》,作者二玛读书。转载本文请联系尔玛读书公众号。随着业务的发展,很多系统都需要经历服务拆分的过程。在微服务的过程中踩坑也是很正常的。如果我们在服务拆分之前做好充分的准备,可以帮助我们少走很多弯路。本文主要从服务依赖、接口版本、隔离、数据一致性等方面讲微服务过程中应该注意的点。循环依赖问题微服务之后,服务之间会存在各种依赖关系,但是依赖关系需要遵循一定的规则,不能太随意。否则会出现循环依赖的问题,调用关系会变得复杂,难以维护。下面是服务依赖的几个规则:1.上层服务可以调用下层服务。2、同级服务之间不能有依赖关系,也不能有调用关系。3、下层服务不能调用上层服务。4、服务之间的调用关系只能是单向的。例如,电子商务系统包括支付服务(Pay)、库存服务(Inventory)和订单服务(Order)。支付服务和库存服务是基础服务,订单服务是上层服务。支付服务和库存服务是同级服务,它们之间不能有调用关系。订单服务属于上层服务。订单服务可以调用支付服务和库存服务,但是支付服务和库存服务不能调用上层订单服务。假设我们忽略这些规则,让Order和Pay互相调用。这样就会产生循环依赖,Order调用Pay,Pay也调用Order,这样就会互相依赖。循环依赖会导致哪些问题?1.无限递归调用。如果Order调用Pay的A方法,Pay调用Order的B方法。然后在A方法中调用Order的B方法,在B方法中调用Pay的A方法。这会产生无限的递归调用,后果不言而喻。Order{voidB(){Pay.A();}}Pay{voidA(){Order.B();}}2.部署依赖问题假设Order、Pay、Inventory可以通过API相互调用。当API接口发生变化时,需要重新编译API才能正常调用其他服务。如果Order和Pay的API发生了变化,在线发布时需要特别注意。为了保证发布成功,需要根据服务间的API依赖关系,仔细考虑先打包部署哪些服务,后打包部署哪些服务,以免发布失败。如果有更多的服务呢?比如10个以上,整理依赖会让人抓狂。3.另外,循环依赖会使服务之间的调用关系复杂化,使系统难以维护。接口版本兼容。一些初中级的程序员往往会忽略接口变化的问题,经常因为接口的变化而导致上线问题。比如一个小电商平台的订单服务调用了支付服务的某个接口,产品突然提了一个需求。这个需求需要在支付接口上加一个参数。开发这个需求的人是一个新手。他直接在原来的接口方法上实现了需求,增加了参数。联调测试通过后发布上线。结果订单服务一上线就开始报错,因为改了方法加了参数,订单服务找不到老的方法。所以它会一直报错,直到订单服务上线。所以一定要注意接口版本问题。我们可以添加一个新方法来重载旧方法,并在新方法中实现新功能。新方法的定义与旧方法相同,只是多了一个参数。也就是在旧方法上加一个新版本。这样付款服务上线后,订单服务上线前就不会报错了,因为老方法还是可以的。订单服务上线后,直接切换到新版本的方法。如果我们的服务框架使用Dubbo,当某个接口的实现不兼容升级时,可以使用Dubbo的版本号进行过渡,不同版本号的服务之间不会相互引用。版本迁移可以按以下步骤进行:1.在低压期,先将一半的提供者升级到新版本2.然后将所有消费者升级到新版本3.然后将剩下的一半提供者升级到新版本老版本服务提供者配置:新版本服务提供者配置:新版本服务消费者配置:隔离注意事项数据隔离:其实服务的基本原则之一就是数据隔离。不同的服务应该有自己的专用数据库。数据访问可以通过服务接口或消息队列,而不是共享同一个数据库。很多公司只是在微服务之后拆分了代码工程,不同服务对应的数据仍然存储在同一个数据库中。这样做至少存在四个问题:1、数据安全问题。其他人的服务不仅可以访问您的数据,还可以修改和删除您的数据。2、导致数据库连接耗尽。一旦某个服务的开发者写了一个慢SQL,并且该服务没有合理限制连接数。可能会消耗掉所有的数据库连接,导致访问同一个数据库的其他服务无法获取数据库连接,无法访问数据库。3.表关联查询。为了快速推出某些要求,其他服务的开发人员无法避免。直接查询其他服务的表,或者跨服务进行表关联查询。这会导致服务之间的耦合越来越严重。4.表结构变化的影响。如果一个服务直接依赖于其他服务的数据,一旦表结构发生变化,比如修改表名或者字段。很可能会有灾难性的后果。部署隔离:我们经常会遇到秒杀业务和日常业务依赖同一个服务,C端服务和内部操作系统依赖同一个服务的情况,比如都依赖支付服务。但是,秒杀系统的瞬时流量非常大,可能会给服务带来巨大的压力,甚至不堪重负。内部操作系统也经常有批量导出数据的操作,也会给业务带来一定的压力。这些都是不稳定的因素。因此,我们可以将这些相互依赖的服务分组部署,不同的分组服务于不同的业务,避免相互干扰。业务隔离:以秒杀为例。从商业角度看,秒杀区别于日常销售,将秒杀作为一种营销活动。想要参与秒杀的商品需要提前报名参加活动,这样我们就可以提前知道哪些商家、哪些商品会参与秒杀。商品详情静态页面提前生成并上传到CDN进行预热。上报的商品库存也需要提前预热。可以在活动开始前将商品库存预热到Redis,防止秒杀开始后大量访问渗透到数据库中。数据一致性问题微服务拆分后,也可能会出现数据不一致的问题。例如在支付服务中,支付状态发生变化后,必须通知订单服务修改对应订单的状态。如果支付服务没有正常通知订单服务,或者订单服务收到通知后未能处理通知,会导致支付服务的支付状态与订单服务的支付状态不一致,即数据会不一致。那么如何避免数据不一致的问题呢?我们通常所说的服务间的数据一致性主要包括数据强一致性和最终一致性。对于强一致性,业务场景用的少,会有明显的性能问题。所以这里主要讨论最终一致性。一般我们可以通过以下方式来保证服务间数据的最终一致性:重试定时任务,同步调用接口,使用定时任务扫表,为每个定时任务扫描所有不成功的记录,并发起重试尝试。注意必须保证重试操作的幂等性。这种方法的优点是实现简单。缺点是需要启动一个专门的定时任务,而且定时任务有一定的时间间隔,所以实时性会比较差。而且调用同步接口的方法耦合度高,有时无法避免循环依赖的问题。例如,Order服务可以调用Pay,但作为基础服务的Pay不应该调用Order。当Pay的交易状态发生变化时,需要通知Order。如果使用定时任务方式,Order需要提供一个接口,在定时任务扫描过程中同步调用该接口,更新Order的订单状态。这就违背了单向依赖的原则,形成了循环依赖。异步消息队列,发送事务性消息如上图所示,以电商下单流程为例。订货流程的最后一步是通知WMS去仓库取货,这是一条异步消息,经过消息队列。publicvoidmakePayment(){orderService.updateStatus(OrderStatus.Payed);//订单服务更新订单为已付款状态inventoryService.decrStock();//库存服务扣除库存couponService.updateStatus(couponStatus.Used);//卡券service更新优惠券为已使用状态,发送MQ消息出库提货;//发送消息通知WMS去仓库取货}根据上面的代码,大家不难发现问题所在!如果出库提货消息失败,会导致数据不一致!有人说我可以在代码中加入重试逻辑和回滚逻辑。如果消息发送失败,将重新发送。如果多次重试失败,将回滚所有操作。这样逻辑会特别复杂,要考虑fallback失败,也有可能消息已经发送成功,但是发送方因为网络等问题没有得到MQ的响应。也可能出现发送方宕机的情况。这些问题一定要注意!幸运的是,一些消息队列帮助我们解决了这些问题。比如阿里开源的RocketMQ(目前是Apache开源项目),4.3.0版本开始支持事务消息(其实在贡献给Apache之前就已经支持事务消息,后来被阉割了,4.3.0版本开始以支持事务性消息)。先看RocketMQ发送事务性消息的过程:1.发送半消息(所有事务性消息都要经过确认过程,才能确定最终提交还是回滚(放弃消息)。未确认的消息称为“半消息”或“准备”message","messagetobeconfirmation")2、半消息发送成功并响应发送方3、执行本地事务,根据本地事务的执行结果发送commit或rollback确认消息4、如果确认消息丢失(网络问题或生产者故障等),MQ将执行结果5回检给发送者,并根据上一步的检查结果确认提交或回滚(放弃消息)。看完事务性消息发送流程,可能有的读者还不是很理解,没关系,我们来分析一下!问题一:如果发送方发送半条消息失败怎么办?半消息(待确认消息)由消息发送者发送。如果失败,发送方知道并可以相应地处理。问题2:如果发送端在执行本地事务后没有发送确认消息通知MQ提交或回滚消息(网络问题、发送端重启等),怎么办?没关系,当MQ发现一条消息在中途长时间处于该消息的状态(消息待确认),MQ会主动以计划任务的形式回查发送者,并获取执行结果发件人。这样即使网络出现问题或者发送方自身出现问题(重启、宕机等),MQ也可以通过定时任务主动检查发送方,基本确认消息是提交还是回滚(弃)。当然,出于性能和半消息堆积的考虑,MQ本身也会对checkback的次数进行限制。问题三:如何保证消费成功?RocketMQ本身有ack机制保证消息可以正常消费。如果消费失败(消息订阅者错误、宕机等),RocketMQ会重新将消息回传给Broker,并在一定延迟时间(默认10秒)后重新投递消息。结合上面对hmily的几个同步调用,完整代码如下://TransactionListener是一个rocketmq接口,用于回调执行本地事务和查询状态publicclassTransactionListenerImplementsTransactionListener{//执行本地事务@OverridepublicLocalTransactionStateexecuteLocalTransaction(Messagemsg,Objectarg){记录orderID,消息状态键值对ReturnLocalTransactionState.COMMIT_MESSAGE;}//查询发送方状态@OverridepublicLocalTransactionStatecheckLocalTransaction(MessageExtmsg){Stringstatus=从共享映射中获取orderID对应的消息状态;if("commit".equals(status))returnLocalTransactionState.COMMIT_MESSAGE;elseif("rollback".equals(status))returnLocalTransactionState.ROLLBACK_MESSAGE;elsereturnLocalTransactionState.UNKNOW;}}//订单服务publicclassOrderService{//tcc接口@Hmily(confirmMethod="confirmOrderStatus",cancelMethod="cancelOrderStatus")publicvoidmakePayment(){1、更新订单状态为付款中2、冻结库存、调用rpc3、更改优惠券状态为使用中、调用rpc4、发送一半消息(待确认)通知WMS取货出库//创建生产者时,本卷TransactionListenerImpl}publicvoidconfirmOrderStatus(){更新订单状态为已付款}publicvoidcancelOrderStatus(){恢复订单状态为待支付}}//库存服务publicclassInventoryService{//tcc接口@Hmily(confirmMethod="confirmDecr",cancelMethod="cancelDecr")publicvoidlockStock(){//防挂处理if(branchtransaction记录表中没有二段执行记录)冻结库存elsereturn;}publicvoidconfirmDecr(){确认扣除库存}publicvoidcancelDecr(){解除冻结库存}}//卡券服务publicclassCouponService{//tccinterface@Hmily(confirmMethod="confirm",cancelMethod="cancel")publicvoidhandleCoupon(){//反挂处理如果(分支交易记录表没有二段执行记录)优惠券状态为更新为临时状态Inuseelsereturn;}publicvoidconfirm(){优惠券状态变为已使用}publicvoidcancel(){优惠券状态恢复为未使用}}如果执行了TransactionListenerImpl.executeLocalTransaction方法,则表示半消息有蜜蜂n发送成功,也代表OrderService.makePayment方法的四步执行成功。此时tcc也已经到了confirm阶段,所以在TransactionListenerImpl.executeLocalTransaction方法中,可以直接返回LocalTransactionState.COMMIT_MESSAGE让MQ提交这条消息,并在sharedmap中保存订单信息和对应的消息状态,这样当确认消息发送失败时,MQ可以回查消息状态。3.采用TCC、SAGA、Seata等框架