微服务化后,这4点一定要注意 | IDCF
来源:二马读书
随着业务发展,很多系统需要经历服务拆分的过程。微服务化过程踩坑也是很正常的事。如果在服务拆分之前做好充分准备,能帮我们少走很多弯路。本文主要从服务依赖,接口版本,隔离,数据一致等方面说说微服务化过程应该注意的点。
一、循环依赖问题
微服务化之后服务之间会存在各种依赖关系,不过依赖需要遵循一定的规则,不能太随意。否则,就会出现循环依赖的问题,而且会让调用关系变得错综复杂难于维护。
下面是服务依赖的几条规则:
上层服务可以调用下层服务。 同级服务之间不能产生依赖关系,及不能产生调用关系。 下层服务不能调用上层服务。 服务之间的调用关系只能是单向的。
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的版本号过渡,版本号不同的服务相互间不引用。
可以按照以下的步骤进行版本迁移:
在低压力时间段,先升级一半提供者为新版本。 再将所有消费者升级为新版本。 然后将剩下的一半提供者升级为新版本。
新版本服务提供者配置:
老版本服务消费者配置:
新版本服务消费者配置:
三、关于隔离的考虑
3.1 数据隔离
实际上,服务化的其中一个基本原则就是数据隔离,不同服务应该有自己的专属数据库,而不应该共用相同的数据库,数据访问可以通过服务接口或者消息队列的方式。
很多公司微服务化后,只做了代码工程的拆分,不同服务对应的数据仍然存放在同一个数据库中。这样做至少存在四个问题:
数据安全问题。别人的服务不但可以访问你的数据,而且还能修改和删除你的数据。 导致数据库连接耗尽。一旦某个服务的开发者写了一个慢SQL,并且这个服务也没有合理限制连接数。可能会消耗掉所有的数据库连接,进而造成访问相同数据库的其他服务拿不到数据库连接,无法访问数据库。 表关联查询。无法避免其他服务的开发者,为了快速上线某些需求。直接查询其他服务的表,或者跨服务做表关联查询。这样会造成服务间的耦合越来越严重。 表结构变化的影响。如果某个服务直接依赖于其他服务的数据,一旦表结构发生任何变化,比如修改表名或者字段。很可能会产生灾难性后果。
四、数据一致性问题
做了微服务拆分后,还可能会出现数据不一致的问题。比如支付服务中,支付状态发生变更后要通知订单服务修改对应订单的状态。如果支付服务没有正常通知到订单服务,或者订单服务接到通知后没能正常处理通知,就会导致支付服务的支付状态和订单服务的支付状态不一致,也就是数据会不一致。
public void makePayment() {
orderService.updateStatus(OrderStatus.Payed); //订单服务更新订单为已支付状态
inventoryService.decrStock(); //库存服务扣减库存
couponService.updateStatus(couponStatus.Used); //卡券服务更新优惠券为已使用状态
发送MQ消息捡货出库;//发送消息通知WMS捡货出库
}
发送半消息(所有事务型消息都要经历确认过程,从而确定最终提交或回滚(抛弃消息),未被确认的消息称为“半消息”或者“预备消息”,“待确认消息”)。 半消息发送成功并响应给发送方。 执行本地事务,根据本地事务执行结果,发送提交或回滚的确认消息。 如果确认消息丢失(网络问题或者生产者故障等问题),MQ向发送方回查执行结果。 根据上一步骤回查结果,确定提交或者回滚(抛弃消息)。
//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
}
}