上一章介绍了有界上下文集成的方法,其中上下文通信主要有消息和rpc调用,这一章主要介绍消息的应用
1.消息传递的基础
使用消息传递与传统、非分布式面向对象的应用程序不同,最显著的区别消息是异步的,不能采用集中式组件负责传递所有消息,容易引起单点故障,每个发送或者接收组件上有不同代理,上下文只与代理连接,一般采用的是至少传递一次的策略+对消息进行幂等校验使用。
消息模型有两种
- 事件(发布订阅):应用广泛一个发布可以增加多个订阅,加入新的订阅者对发布者来说不用修改代码。
- 存储转发(命令):发送方将消息放在一个队列存储,消费方读取处理成功则删除,否则放回重试,实现一对一通信,比如亚马逊的SQS。
消息的使用会带来用户体验的改变,比如原来同步等待就能得到结果,但是后面可能需要再次给用户发送推送、通知告知具体结果。采用消息可以破除大的事务,这样相当于选择最终一致性
。
2.构建一个电子商务应用程序
用NServiceBus构建一个电子商务应用程序,NServiceBus提供了消息能力支持,就类似于各种MQ一样。
2.1 系统设计
- 领域驱动设计
了解系统构建的价值,投入时间与领域专家构建通用语言,将一些隐式的概念变的显示,可以从识别领域中的重要事件开始(事件风暴),这一点和消息传递应用程序有很多相似之处。
- 领域事件
提炼显示环境中发生的事件,让事件成为通用语言的一部分,然后组织起来以便了解如何组合形成完整的业务用例。可以用组件图的形式可视化呈现出来。
image.png image.png- 上下文之间使用消息通信。
- 与外部支付提供程序之间使用RPC通信。
- 每个上下文可以选择不同的技术,包括数据库。
- 每个上下文不共享数据库。
提供的程序代码是C#的,大概流程如下:
-
网站将接收到用户创建订单的参数,包含user_id、product_id,shipping_type通过
PlaceOrder
类型的命令消息发送给销售
,返回给用户稍后会邮件通知结果。 -
销售
会将PlaceOrder
里面的参数解析,并将订单信息、计算订单实际金额以后写入数据库,然后发送OrderCreated
事件消息。
-
-
-
结算
收到事件消息OrderCreated
后,会RPC调用信用卡服务进行扣款(这里理解为同步调用),结算内部会有一个自己用的消息,包含订单和支付状态,会将这个消息再次异步发送给结算
用,结算
根据这个消息的状态决定下一步处理。
- 成功:发送
PaymentAccepted
事件消息 - 失败: 发送
PaymentReject
事件消息。
-
运输
收到事件消息OrderCreated
,会创建运单(此处后续版本新增一个address_id
以方便用户支持多个地址)
-
-
运输
收到PaymentAccepted
后,RPC调用第三方运输程序执行真正的配送,同样也创建一个自己用的内部消息,保存第三方结果,类似结算
.
1.成功: 发送shipSuccess
事件消息,销售通知成功
.
2.失败:发送shipFail
事件消息,结算
执行退款、销售通知成功
这个流程对比现实有不合理之处
- 配送地址用的地址id,其实应该用id对应的详细快照.
- 订单价格会发生变化用户看到的可能不是最新的,其实实际应该是创建订单前应该再次计算价格,如果价格和之前的有变化应该提醒用户,并刷新界面。
但是依然可以通过这个例子去体验一下采用消息机制实现上下文通信的流程和关键点。
-
命令格式的消息有局限性,一对一,比如事件消息用途广。谁使用谁定义,比如
PlaceOrder
是销售
定义的。 -
数据一致性问题:比如支付失败了,订单需要流转到一个特定的状态。但是中间可能会有短暂不一致,在这个案例来说创建订单和支付是一起完成的,类似于自动支付,如果按照传统理解这应该是一个事务,如果支付失败事务回滚,那订单就应该不存在,采用消息模式就应该在支付失败的时候发送支付失败消息,让订单状态标记为支付失败并通知用户。
-
有界上下文需要存储所有自己用的数据。
在创建shiporder的时候需要address具体信息,假设address是存在用户中心
里面的,这个时候不希望根据address_id去调用用户中心
去查询,而是由用户中心
发布用户地址变化消息,运输
服务把它存起来,这样降低耦合了,避免依赖用户中心
,通过接收消息,存储数据这是一种降低耦合的方式,一般成为数据异构。
这一点存疑,实际应该不这么用,一般都是创建订单的时候就查询
用户中心
得到具体地址信息,后面的消息发送的都是地址信息而不是address_id,如果确实要支持修改地址,应该在运行修改的情况下修改订单的地址然后,在同步给运单系统。
- 时序问题
消息是很难保证有序的,比如如果上面采取了监听用户创建地址并存储数据的方案,有可能出现创建运单消息先到,创建地址消息后到的问题,这样就查询不到地址信息,这种情况下可以采用消息重试,但是如果用户是修改地址信息,这样就拿不到最新的地址数据,所以说很多方案都有优劣,要综合考虑成本和受益。
- 消息版本
消息的结构难免会有改动,为了包装兼容升级可以加入版本号,先判断版本号,然后再根据版本号解析里面具体的数据。
- 通过消息让外部http或者rpc变得可控
比如上面案例里面的真正调用第三方支付或者运输系统的时候,如果当时第三方系统故障了,可以通过内部的消息重试来解决。
- 把所有内容放在UI里面
每个上下文如果有需要公开的数据通过定义对外公开的API,在网页用AJAX调用去渲染数据,不用单一而复杂的API是为了提高加载速度、服务降级,比如市场服务
出问题的时候,依然期望可以得到不包含优惠的产品,这里推荐由客户端进行编排请求调用.
这个不是绝对的,一般客户端请求的是网关服务,网关服务会调用其他的微服务完成业务功能,网关这一层需要支持服务降级,比如产品列表希望展示用户当前可享受优惠的最低购买价格,计价服务出问题的时候应该做服务降级,不应该导致产品列表失败,总体来说具体有谁编排调用需要看UI功能的相关程度。
网友评论