随着业务倒退,很多零碎须要经验服务拆分的过程。微服务化过程踩坑也是很失常的事。如果在服务拆分之前做好充分准备,能帮咱们少走很多弯路。本文次要从服务依赖,接口版本,隔离,数据统一等方面说说微服务化过程应该留神的点。
一、循环依赖问题
微服务化之后服务之间会存在各种依赖关系,不过依赖须要遵循肯定的规定,不能太随便。否则,就会呈现循环依赖的问题,而且会让调用关系变得盘根错节难于保护。上面是服务依赖的几条规定:
- 下层服务能够调用上层服务。
- 同级服务之间不能产生依赖关系,及不能产生调用关系。
- 上层服务不能调用下层服务。
- 服务之间的调用关系只能是单向的。
例如,在电商零碎里包含领取服务(Pay),库存服务(Inventory),订单服务(Order)。领取服务和库存服务属于根底服务,订单服务属于下层服务。领取服务和库存服务是同级的服务,他们之间不能存在调用关系。订单服务属于下层服务,订单服务能够调用领取服务和库存服务,然而领取服务和库存服务不能调用下层的订单服务。
假如咱们不论这些规定,让 Order 和 Pay 能够相互调用。这样就会产生循环依赖,Order 调用 Pay,Pay 也调用 Order,这样彼此都会依赖对方。
循环依赖导致哪些问题?
1.1 有限递归调用
如果,Order 调用 Pay 的 A 办法,Pay 调用 Order 的 B 办法。而后,A 办法里又调用了 Order 的 B 办法,B 办法里又调用了 Pay 的 A 办法。这样就会产生有限的递归调用,结果天然显而易见了。
Order {void B(){Pay.A();
}
}
Pay{void A(){Order.B();
}
}
1.2 部署依赖问题
假如 Order,Pay,Inventory 彼此之间都能够通过 API 相互调用。当 API 接口产生变更时,为了让其余服务可能失常调用,API 须要从新编译。如果 Order 和 Pay 的 API 都有变动,上线公布时就须要特地小心。为了保障公布胜利,就须要依据服务间 API 的依赖关系,具体思考先打包部署哪个服务,后打包部署哪个服务,才不至于公布失败。如果有更多的服务呢?比方 10 几个,梳理依赖关系都会把人搞疯的。
1.3 另外,循环依赖会让服务间的调用关系变得盘根错节,零碎难于保护。
二、接口版本兼容
一些初中级程序员往往会疏忽接口变更的问题,常常会因为接口变更导致线上问题。比方某个小型电商平台的订单服务调用领取服务的某个接口,产品忽然提了一个需要,这个需要须要在这个领取接口上加一个参数。开发这个需要的是个老手,他间接在原来的接口办法上实现了需要并加上了参数,联调测试通过后就公布上线了。后果刚上线订单服务就开始报错,因为办法变了,加了参数,订单服务找不到老的办法了。所以就会始终报错,直到订单服务上线为止。
所以咱们肯定要留神接口版本问题。咱们能够新加一个办法去重载老的办法,在新办法里实现新的性能,新办法的定义除了多一个参数外,其余的和老办法一样。也就是给老办法加了一个新版本。
这样在领取服务上线后,订单服务上线之前就不会报错了,因为老办法依然可用。订单服务上线后就间接切到了新版本的办法。
如果咱们服务框架选用的是 Dubbo,当一个接口的实现,呈现不兼容降级时,能够用 Dubbo 的版本号过渡,版本号不同的服务相互间不援用。
能够依照以下的步骤进行版本迁徙:
- 在低压力时间段,先降级一半提供者为新版本
- 再将所有消费者降级为新版本
- 而后将剩下的一半提供者降级为新版本
老版本服务提供者配置:
<dubbo:service interface="com.foo.BarService" version="1.0.0" />
新版本服务提供者配置:
<dubbo:service interface="com.foo.BarService" version="2.0.0" />
老版本服务消费者配置:
<dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" />
新版本服务消费者配置:
<dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" />
三、对于隔离的思考
3.1 数据隔离
实际上,服务化的其中一个根本准则就是数据隔离,不同服务应该有本人的专属数据库,而不应该共用雷同的数据库,数据拜访能够通过服务接口或者音讯队列的形式。
很多公司微服务化后,只做了代码工程的拆分,不同服务对应的数据依然寄存在同一个数据库中。这样做至多存在四个问题:
- 数据安全问题。他人的服务岂但能够拜访你的数据,而且还能批改和删除你的数据。
- 导致数据库连贯耗尽。一旦某个服务的开发者写了一个慢 SQL,并且这个服务也没有正当限度连接数。可能会消耗掉所有的数据库连贯,进而造成拜访雷同数据库的其余服务拿不到数据库连贯,无法访问数据库。
- 表关联查问。无奈防止其余服务的开发者,为了疾速上线某些需要。间接查问其余服务的表,或者跨服务做表关联查问。这样会造成服务间的耦合越来越重大。
- 表构造变动的影响。如果某个服务间接依赖于其余服务的数据,一旦表构造产生任何变动,比方批改表名或者字段。很可能会产生灾难性结果。
3.2 部署隔离
咱们常常会遇到秒杀业务和日常业务依赖同一个服务,以及 C 端服务和外部经营零碎依赖同一个服务的状况,比如说都依赖领取服务。而秒杀零碎的霎时访问量很高,可能会对服务带来微小的压力,甚至压垮服务。外部经营零碎也常常有批量数据导出的操作,同样会给服务带来肯定的压力。这些都是不稳固因素。所以咱们能够将这些独特依赖的服务分组部署,不同的分组服务于不同的业务,防止互相烦扰。
3.3 业务隔离
以秒杀为例。从业务上把秒杀和日常的售卖辨别开来,把秒杀做为营销流动,要参加秒杀的商品须要提前报名加入流动,这样咱们就能提前晓得哪些商家哪些商品要参加秒杀,能够依据提报的商品提前生成商品详情动态页面并上传到 CDN 预热,提报的商品库存也须要提前预热,能够将商品库存在流动开始前预热到 Redis,防止秒杀开始后大量拜访穿透到数据库。
四、数据一致性问题
做了微服务拆分后,还可能会呈现数据不统一的问题。比方领取服务中,领取状态产生变更后要告诉订单服务批改对应订单的状态。如果领取服务没有失常告诉到订单服务,或者订单服务接到告诉后没能失常解决告诉,就会导致领取服务的领取状态和订单服务的领取状态不统一,也就是数据会不统一。
那么如何防止数据不统一的问题产生呢?
咱们通常所说的服务间数据一致性,次要包含数据强一致性和最终一致性。对于强一致性,应用的业务场景很少,而且会有显著的性能问题。所以这里咱们次要探讨最终一致性。
个别咱们能够采纳如下几种形式来保障服务间数据的最终统一:
4.1 定时工作重试,同步调用接口
这种形式,采纳定时工作去扫表,每次定时工作扫描所有未胜利的记录,并发动重试。留神,要保障重试操作的幂等性。
这种形式的长处是:实现简略。毛病是:须要启动专门的定时工作,定时工作存在肯定的工夫距离,实时性会比拟差。而且同步接口调用的形式,耦合较重,有时无奈防止循环依赖的问题。
比方,Order 服务能够调用 Pay,Pay 做为根底服务不应该调用 Order。当 Pay 的某笔交易状态产生变更后,须要告诉 Order。如果采纳定时工作的形式就须要 Order 提供一个接口,定时工作扫描过程中同步调用这个接口去更新 Order 的订单状态。这样又违反了单向依赖的准则,造成了循环依赖。
4.2 异步音讯队列,发送事务型音讯
如上图,以电商下单流程为例。下单流程最初一步,告诉 WMS 捡货出库,是异步音讯走音讯队列。
public void makePayment() {orderService.updateStatus(OrderStatus.Payed); // 订单服务更新订单为已领取状态
inventoryService.decrStock(); // 库存服务扣减库存
couponService.updateStatus(couponStatus.Used); // 卡券服务更新优惠券为已应用状态
发送 MQ 音讯捡货出库;// 发送音讯告诉 WMS 捡货出库
}
按下面代码,大家不难发现问题!如果发送捡货出库音讯失败,数据就会不统一!有人说我能够在代码上加上重试逻辑和回退逻辑,发消息失败就重发,多次重试失败所有操作都回退。这样一来逻辑就会特地简单,回退失败要思考,而且还有可能音讯曾经发送胜利了,然而因为网络等问题发送方没失去 MQ 的响应。还有可能呈现发送方宕机的状况。这些问题都要思考进来!
幸好,有些音讯队列帮咱们解决了这些问题。比方阿里开源的 RocketMQ(目前曾经是 Apache 开源我的项目),4.3.0 版本开始反对事务型音讯(实际上早在奉献给 Apache 之前已经反对过事务音讯,起初被阉割了,4.3.0 版本从新开始反对事务型音讯)。
先看看 RocketMQ 发送事务型音讯的流程:
- 发送半音讯(所有事务型音讯都要经验确认过程,从而确定最终提交或回滚(摈弃音讯),未被确认的音讯称为“半音讯”或者“准备音讯”,“待确认音讯”)
- 半音讯发送胜利并响应给发送方
- 执行本地事务,依据本地事务执行后果,发送提交或回滚的确认音讯
- 如果确认音讯失落(网络问题或者生产者故障等问题),MQ 向发送方回查执行后果
- 依据上一步骤回查后果,确定提交或者回滚(摈弃音讯)
看完事务型音讯发送流程,有些读者可能没有齐全了解,不要紧,咱们来剖析一下!
问题 1:如果发送方发送半音讯失败怎么办?
半音讯(待确认音讯)是音讯发送方发送的,如果失败,发送方本人是晓得的并能够做相应解决。
问题 2:如果发送方执行完本地事务后,发送确认音讯告诉 MQ 提交或回滚音讯时失败了(网络问题,发送方重启等状况),怎么办?
没关系,当 MQ 发现一个音讯长时间处于半音讯(待确认音讯)的状态,MQ 会以定时工作的形式被动回查发送方并获取发送方执行后果。这样即使呈现网络问题或者发送方自身的问题(重启,宕机等),MQ 通过定时工作被动回查发送方根本都能确认音讯最终要提交还是回滚(摈弃)。当然出于性能和半音讯沉积方面的思考,MQ 自身也会有回查次数的限度。
问题 3:如何保障生产肯定胜利呢?
RocketMQ 自身有 ack 机制,来保障音讯可能被失常生产。如果生产失败(音讯订阅方出错,宕机等起因),RocketMQ 会把音讯重发回 Broker,在某个延迟时间点后(默认 10 秒后)从新投递音讯。
联合下面几个同步调用 hmily 残缺代码如下:
//TransactionListener 是 rocketmq 接口用于回调执行本地事务和状态回查
public class TransactionListenerImpl implements TransactionListener {
// 执行本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
记录 orderID,音讯状态键值对到共享 map 中,以备 MQ 回查音讯状态应用;return LocalTransactionState.COMMIT_MESSAGE;
}
// 回查发送者状态
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String status = 从共享 map 中取出 orderID 对应的音讯状态;
if("commit".equals(status))
return LocalTransactionState.COMMIT_MESSAGE;
else if("rollback".equals(status))
return LocalTransactionState.ROLLBACK_MESSAGE;
else
return LocalTransactionState.UNKNOW;
}
}
// 订单服务
public class OrderService{
//tcc 接口
@Hmily(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public void makePayment() {
1,更新订单状态为领取中
2,解冻库存,rpc 调用
3,优惠券状态改为应用中,rpc 调用
4,发送半音讯(待确认音讯)告诉 WMS 捡货出库 // 创立 producer 时这册 TransactionListenerImpl
}
public void confirmOrderStatus() {更新订单状态为已领取}
public void cancelOrderStatus() {复原订单状态为待领取}
}
// 库存服务
public class InventoryService {
//tcc 接口
@Hmily(confirmMethod = "confirmDecr", cancelMethod = "cancelDecr")
public void lockStock() {
// 防悬挂解决
if (分支事务记录表没有二阶段执行记录)
解冻库存
else
return;
}
public void confirmDecr() {确认扣减库存}
public void cancelDecr() {开释解冻的库存}
}
// 卡券服务
public class CouponService {
//tcc 接口
@Hmily(confirmMethod = "confirm", cancelMethod = "cancel")
public void handleCoupon() {
// 防悬挂解决
if (分支事务记录表没有二阶段执行记录)
优惠券状态更新为长期状态 Inuse
else
return;
}
public void confirm() {优惠券状态改为 Used}
public void cancel() {优惠券状态复原为 Unused}
}
如果执行到 TransactionListenerImpl.executeLocalTransaction 办法, 阐明半音讯曾经发送胜利了,也阐明 OrderService.makePayment 办法的四个步骤都执行胜利了,此时 tcc 也到了 confirm 阶段,所以在 TransactionListenerImpl.executeLocalTransaction 办法里能够间接返回 LocalTransactionState.COMMIT_MESSAGE 让 MQ 提交这条音讯,同时将该订单信息和对应的音讯状态保留在共享 map 里,以备确认音讯发送失败时 MQ 回查音讯状态应用。
起源:二马读书
申明:文章取得作者受权在 IDCF 社区公众号(devopshub)转发。优质内容共享给思否平台的技术伙伴,如原作者有其余思考请分割小编删除,致谢。
玩乐高,学麻利,规模化麻利联合作战沙盘之「乌托邦打算」,12 月 25-26 日登陆深圳,将“多团队麻利协同”基因内化在研发流程中,为规模化晋升研发效力保驾护航!!🏰⛴公众号回复“乌托邦”可加入