JoeCao / JoeCao.github.io

543 stars 69 forks source link

多研究些架构,少谈些框架(3)-- 微服务和事件驱动 #5

Open JoeCao opened 7 years ago

JoeCao commented 7 years ago

多研究些架构,少谈些框架(1) -- 论微服务架构的核心概念 多研究些架构,少谈些框架(2)-- 微服务和充血模型 多研究些架构,少谈些框架(3)-- 微服务和事件驱动

2017-6-16 曹祖鹏

接上篇,我们采用了领域驱动的开发方式,使用了充血模型,享受了他的好处,但是也不得不面对他带来的弊端。这个弊端在分布式的微服务架构下面又被放大。

事务一致性

事务一致性的问题在Monolithic下面不是大问题,在微服务下面却是很致命,我们回顾一下所谓的ACID原则

在单体服务和关系型数据库的时候,我们很容易通过数据库的特性去完成ACID。但是一旦你按照DDD拆分聚合根-微服务架构,他们的数据库就已经分离开了,你就要独立面对分布式事务,要在自己的代码里面满足ACID。
对于分布式事务,大家一般会想到以前的JTA标准,2PC两段式提交。我记得当年在Dubbo群里面,基本每周都会有人询问Dubbo啥时候支撑分布式事务。实际上根据分布式系统中CAP原则,当P(分区容忍)发生的时候,强行追求C(一致性),会导致(A)可用性、吞吐量下降,此时我们一般用最终一致性来保证我们系统的AP能力。当然不是说放弃C,而是在一般情况下CAP都能保证,在发生分区的情况下,我们可以通过最终一致性来保证数据一致。

例: 在电商业务的下订单冻结库存场景。需要根据库存情况确定订单是否成交。 假设你已经采用了分布式系统,这里订单模块和库存模块是两个服务,分别拥有自己的存储(关系型数据库),

DIFF

在一个数据库的时候,一个事务就能搞定两张表的修改,但是微服务中,就没法这么做了。 在DDD理念中,一次事务只能改变一个聚合内部的状态,如果多个聚合之间需要状态一致,那么就要通过最终一致性。订单和库存明显是分属于两个不同的限界上下文的聚合,这里需要实现最终一致性,就需要使用事件驱动的架构。

事件驱动实现最终一致性

事件驱动架构在领域对象之间通过异步的消息来同步状态,有些消息也可以同时发布给多个服务,在消息引起了一个服务的同步后可能会引起另外消息,事件会扩散开。严格意义上的事件驱动是没有同步调用的。

例子: 在订单服务新增订单后,订单的状态是“已开启”,然后发布一个Order Created事件到消息队列上

2-placeorder

库存服务在接收到Order Created 事件后,将库存表格中的某sku减掉可销售库存,增加订单占用库存,然后再发送一个Inventory Locked事件给消息队列

3-InvetoryLock

订单服务接收到Inventory Locked事件,将订单的状态改为“已确认”

OrderChange

有人问,如果库存不足,锁定不成功怎么办? 简单,库存服务发送一个Lock Fail事件, 订单服务接收后,把订单置为“已取消”。

好消息,我们可以不用锁!事件驱动有个很大的优势就是取消了并发,所有请求都是排队进来,这对我们实施充血模型有很大帮助,我们可以不需要自己来管理内存中的锁了。取消锁,队列处理效率很高,事件驱动可以用在高并发场景下,比如抢购。

是的,用户体验有改变,用了这个事件驱动,用户的体验有可能会有改变,比如原来同步架构的时候没有库存,就马上告诉你条件不满足无法下单,不会生成订单;但是改了事件机制,订单是立即生成的,很可能过了一会系统通知你订单被取消掉。 就像抢购“小米手机”一样,几十万人在排队,排了很久告诉你没货了,明天再来吧。如果希望用户立即得到结果,可以在前端想办法,在BFF(Backend For Frontend)使用CountDownLatch这样的锁把后端的异步转成前端同步,当然这样BFF消耗比较大。

没办法,产品经理不接受,产品经理说用户的体验必须是没有库存就不会生成订单,这个方案会不断的生成取消的订单,他不能接受,怎么办?那就在订单列表查询的时候,略过这些cancel状态的订单吧,也许需要一个额外的视图来做。我并不是一个理想主义者,解决当前的问题是我首先要考虑的,我们设计微服务的目的是本想是解决业务并发量。而现在面临的却是用户体验的问题,所以架构设计也是需要妥协的:( 但是至少分析完了,我知道我妥协在什么地方,为什么妥协,未来还有可能改变。

多个领域多表Join查询

限界上下文(Bounded Context)和数据耦合

除了多领域join的问题,我们在业务中还会经常碰到一些场景,比如电商中的商品信息是基础信息,属于单独的BC,而其他BC,不管是营销服务、价格服务、购物车服务、订单服务都是需要引用这个商品信息的。但是需要的商品信息只是全部的一小部分而已,营销服务需要商品的id和名称、上下架状态;订单服务需要商品id、名称、目录、价格等等。这比起商品中心定义一个商品(商品id、名称、规格、规格值、详情等等)只是一个很小的子集。这说明不同的限界上下文的同样的术语,但是所指的概念不一样。 这样的问题映射到我们的实现中,每次在订单、营销模块中直接查询商品模块,肯定是不合适,因为

下图一个下单场景分析,在电商系统中,我们可以认为会员和商品是所有业务的基础数据,他们的变更应该是通过广播的方式发布到各个领域,每个领域保留自己需要的信息。 5-MessageDriven

保证最终一致性

最终一致性成功依赖很多条件

6-localtranscation

方案的优势是使用了本地数据库的事务,如果Event没有插入成功,那么订单也不会被创建;线程扫描后把event置为已发送,也确保了消息不会被漏发(我们的目标是宁可重发,也不要漏发,因为Event处理会被设计为幂等)。 缺点是需要单独处理Event发布在业务逻辑中,繁琐容易忘记;Event发送有些滞后;定时扫描性能消耗大,而且会产生数据库高水位隐患;

我们稍作改进,使用数据库特有的MySQL Binlog跟踪(阿里的Canal)或者Oracle的GoldenGate技术可以获得数据库的Event表的变更通知,这样就可以避免通过定时任务来扫描了

8-improved

不过用了这些数据库日志的工具,会和具体的数据库实现(甚至是特定的版本)绑定,决策的时候请慎重。

使用Event Sourcing 事件溯源

事件溯源对我们来说是一个特别的思路,他并不持久化Entity对象,而是只把初始状态和每次变更的Event记录下来,并在内存中根据Event还原Entity对象的最新状态,具体实现很类似数据库的Redolog的实现,只是他把这种机制放到了应用层来。 虽然事件溯源有很多宣称的优势,引入这种技术要特别小心,首先他不一定适合大部分的业务场景,一旦变更很多的情况下,效率的确是个大问题;另外一些查询的问题也是困扰。 我们仅仅在个别的业务上探索性的使用Event Souring和AxonFramework,由于实现起来比较复杂,具体的情况还需要等到实践一段时间后再来总结,也许需要额外的一篇文章来详细描述

以上是对事件驱动在微服务架构中一些我的理解,文章部分借鉴了Chris Richardson的Blog,https://www.nginx.com/blog/event-driven-data-management-microservices/ 在此我向他表示致谢 。

版权说明

本文采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。

关注我

微信公众号 qrcode_for_8

benyVip commented 7 years ago

看不懂这个图哦

chinleo commented 7 years ago

经验少了导致理解不够深刻。

xiongshihu commented 7 years ago

意犹未尽,期待事件溯源实践后的总结。

flydream commented 7 years ago

不错,以前有实现过一个大规模订单履行平台,基本类似的架构,DDD + EventDriven,思想和原理基本一致

wanyouzhu commented 7 years ago

很酷!通过引入中间状态来解决并发和一致性问题,高! 这种方式唯一一点不够完美(也许只有理想主义才会这样想)的地方就是反过来影响产品的设计,要告诉用户订单提交并不意味着下单成功

weey007 commented 7 years ago

很赞,期待博主提供相关的 sample

sunnykaka commented 7 years ago

实际上我们在项目中使用了ElasticSearch作为专门的查询视图,效果很不错

具体是如何使用ElasticSearch来实现查询视图, 能介绍下具体做法或者设计思路吗.

WallenHan commented 7 years ago

追着一些转载的公众号来的,谢谢前辈的一系列分享,我自己在做开发的时候思考过类似东西,一直没有理清思路茅塞顿开。这很棒 (๑•̀ㅂ•́)و✧

soulmz commented 7 years ago
wx20170906-162151 2x

图少了... 求补源

JoeCao commented 7 years ago

@sunnykaka 看最新的一篇《CQRS初探》

quntatatic commented 6 years ago

”使用CountDownLatch这样的锁把后端的异步转成前端同步” 这里不太明白。 在分布式环境下,后端通过异步消息通知前端库存是否扣减成功,请问前端如何实现同步等待和唤醒?

sndwow commented 6 years ago

你好,看了文章学到很多,谢谢。 不过对于“限界上下文(Bounded Context)和数据耦合”这块有些不明白,例如商品的价格,在订单、营销模块都要进行适当的冗余,那么在进行价格更新的时候势必也会按需进行更新,工作量会激增。且在创建订单的时候,价格应该要到商品去取,假如商品宕机,那么订单是无法创建的,这种情况怎么处理?且微服之间是否可以相互进行调用,还是交由APIGATEWAY调用各个微服?

JoeCao commented 6 years ago

@sndwow

sndwow commented 6 years ago

@JoeCao 感谢解惑。

darkhaxe commented 6 years ago

有一点看不明白,如题主大大叙述的 在订单冗余商品模块的信息,避免api查询,而使用mq异步更新 但是在下单时,假如异步通知滞后了,岂不是拿不到最新的商品数据??

JoeCao commented 6 years ago

@darkhaxe 是的,是有这个风险。这也是需要在架构上权衡的。

supercolor007 commented 6 years ago

使用本地事务

还是以上面的订单扣取信用的例子

想问一下第三点,在写表的同时发送消息应该也可以吧

JoeCao commented 6 years ago

@supercolor007 , 要确保写数据库Event表事务结束了,再发消息。这样才能做到一致性。

jiangx1010 commented 6 years ago

曾经也懵懵懂懂实践过DDD,在开发时有一个想法一直在脑子里,就是代码一定是要反应业务的,而不是做数据库的搬运工。所以在做一个促销类的系统时,改变了一下设计的思路,从对象和内存的角度出发去设计系统,没有采用传统的“数据库驱动设计”,和这篇文章的思路类似,也是从数据库中初始化库存对象,然后业务逻辑都在库存对象中,针对库存的锁定或者消费都采用队列和事件。 无意看到这篇文章有一种豁然开朗的感觉,作者对DDD的实践每一个环节都有清晰的思路,希望作者有更多关于DDD的分享。

devpage commented 5 years ago

https://github.com/devpage/walker 分布式事务, 和alibaba/fescar 一个处理逻辑

li-daqian commented 5 years ago

@JoeCao 请假一个问题

请求是在队列当中排队,消费者的是不是采用单线程模型来规避并发问题,那吞吐量会变低吧? 如果消费者采用线程池来消费消息,那还是会有并发问题的。

sunxyz commented 5 years ago

@soulmz 多研究些架构,少谈些框架(3)-- 微服务和事件驱动

sunxyz commented 5 years ago

你好 大神,看完了这十几篇文章收获很多, 但在落地ddd的时候总是会遇到许多问题与疑惑.

就以博客系统举例 (参照wordpress) 这一块怎么设计与划分比较好?

这是我的设计:

1 . 起初是划分了三个领域 :

2. 二个领域: 后面在实现的时候想到订单和订单项的关系和文章和评论的关系比较接近, 然后就把文章与评论合并成文章域 变成了两个域:

但实现的时候发现这样很别扭

举个例子 :

两个用户同时评论, 然后就会有概率存在只保存了一条的可能性. 伪代码:

class PostApplicationService{
  void addComment(Long postId, String comment){
    Post post = postRepository.findById(postId)
    post.addComment(comment)
    postRepository.save(post)
  }
}

这一块我的想法是在post增加一个版本号 ,但是感觉这样 反而没有设计 一个CommentRepository 来的更方便, 这一块是不是我将领域划分错了? 还是 其他什么我没意识到的问题?

然后我就进行了改造

addComment(comment) {
  comment.setPost(this)
  commentMapper.save(comment);
}

但总感觉很别扭

ddd 对于功力尚薄的我确实不好落地. 更多的是 将贫血模型 变得 充血一些. 我想大多数人大多数情况下就在这止步了 .

JoeCao commented 5 years ago

@sunxyz 聚合

聚合是一组相关的领域对象,拥有一致性边界,使得边界内的类与对象图的其他部分“断开连接”,其目的是要确保业务规则在领域对象的各个生命周期都得以执行。

  1. 每个聚合有一个根和一个边界,边界定义了一个聚合内部有哪些实体或值对象,聚合边界内保证业务不变性(invariant),根是聚合内的某个实体;
  2. 聚合内部的对象之间可以相互引用,但是聚合外部如果要访问聚合内部的对象时,必须通过聚合根开始导航,绝对不能绕过聚合根直接访问聚合内的对象,也就是说聚合根是外部可以保持 对它的引用的唯一元素;
  3. 聚合内除根以外的其他实体的唯一标识都是本地标识,也就是只要在聚合内部保持唯一即可,因为它们总是从属于这个聚合的;
  4. 聚合根负责与外部其他对象打交道并维护自己内部的业务规则;
  5. 聚合内部的对象可以保持对其他聚合根的引用;
  6. 删除一个聚合根时必须同时删除该聚合内的所有相关对象,因为他们都同属于一个聚合,是一个完整的概念;

例子1 : 销售订单(Order)/订单明细(OrderLineItem)

分析一下电商领域中常见的销售订单(Order)/订单明细(OrderLineItem):对于一张销售订单来说,订单明细是不可缺少的,否则就不成其为销售订单。因此,销售订单和订单明细之间的关系是一种固定的不可变(invariant)的关系,就像《领域驱动设计》一书中所讲的汽车与轮胎之间的关系那样,汽车少了轮胎就不成其为汽车了。反过来看,订单明细也离不开销售订单,这很简单,因为明细订单明细是描述销售订单的一个不可或缺的部分。于是,在这个例子中,销售订单和订单明细两个对象毋庸置疑的必须在一个聚合中,是强关联生命周期,其中聚合根为销售订单,其中包含一条或多条订单明细的实体,聚合及其实体间的关系可以用下图表示:

55925FD4-F162-43E7-83FC-1DA3261B630A

例子2:论坛的主题(Post)和回复(Reply)

对于论坛主题(Post)/回复(Reply)之间的关系,首先从限界上下文划分,他们应该是一个上下文的,都是和论坛相关。论坛的主题是可以脱离回复单独存在的(一个主题可以没有任何人对其进行回复),而回复却不能脱离主题(没有主题的回复是没有意义的)。所以这个限界上下文存在两个聚合:第一个聚合是以主题(Post)为聚合根;另一个聚合是以回复(Reply)为聚合根,其中包含了对主题(Post)对象引用。其关系可以如下表示 B4900569-D646-4C3F-A1B5-F13F37AF8317

例子3:客户(Customer)/ 销售订单(Order)

在电商中客户(Customer)和销售订单(Order)的情况更特殊一些。客户是可以独立存在的,即使他没有任何的交易行为,而订单却不能脱离客户。但是这两个的业务关系却比较松散,客户除了订单还有很多业务属性,我们在实际设计中,常常把这两个松散耦合的对象拆为两个限界上下文:第一个限界上下文中包含了客户(Customer)为聚合根的聚合;另一个限界上下文是以订单(Order)为聚合根,Order包含了其中的OrderLineItem之类的实体,同样也包含了对用户(Customer)的引用(By Id),这里面应用就不能直接通过对象了,否则这个聚合就特别大。其关系可以如下表示:

651049D5-98E6-4D0D-B4BB-8A450960E97D

JoeCao commented 5 years ago

@sunxyz 对象的关系确定,是否在一个聚合、限界上下文中,是内联(inline)为值对象还是引用还是只保留id?这个最关键的是通过业务场景分析他们的生命周期是否一致。

sunxyz commented 5 years ago

@JoeCao 恩恩 大神 .

是否在一个聚合、限界上下文中 这些如何更好的判断 ?

看了大神举得生动的例子,结合自身的理解谈一下, 大神看看是否理解有误:

就以第三个例子为例:

如果一个或多个属性或对象是某个对象创建时必须的(耦合度很紧密)那就放在一个聚合内(该对象是聚合根) 然后聚合根是访问该聚合内信息的唯一方法并且也是修改该聚合的唯一途径.

1.耦合度紧密程度和创建时必要性(不可分割性)是划分聚合的一个很重要的因素.

在wordpress 这个例子中 文章和用户(是两个松散耦合的对象) 两个聚合通过id来关联 ,这个我想没有太大的疑问; 文章和评论的松散程度则没有订单和订单明细耦合那样紧密,也不像用户和订单耦合那样 松散 ,这种情况下拆分成两个聚合或是一个聚合则要看访问评论的方式,如果评论有成千上万条那么松散的偶尔会更好一些 ,如果评论条数很少那么设计为一个聚合也是可以的 , 我个人的看法是 如果1没有问题那么设计成为两个聚合会更好一些.

2.当耦合度不是那么紧密也不是那么松散可以设计成一个聚合或多个聚合, 和具体场景有关, 如果数据量不是那么大一个聚合会更好些.

----补充-----

早上又想了一下关于聚合这一块,想起了UML类图关系中的组合和聚合:

耦合程度相对松散的是聚合 ,相对紧密的是组合, 聚合是一种较弱形式的对象包含(一个对象包含另一个对象)关系。较强形式是组合(Composition).

聚合(Aggregation)

聚合是一种特殊的关联(Association)形式,表示两个对象之间的所属(has-a)关系。所有者对象称为聚合对象,它的类称为聚合类;从属对象称为被聚合对象,它的类称为被聚合类。例如,一个公司有很多员工就是公司类Company和员工类Employee之间的一种聚合关系。被聚合对象和聚合对象有着各自的生命周期,即如果公司倒闭并不影响员工的存在。

public class Company {
    private List<Employee> employees;
}

public class Employee {
    private String name;   
}

组合(Composition)

聚合是一种较弱形式的对象包含(一个对象包含另一个对象)关系。较强形式是组合(Composition). 在组合关系中包含对象负责被包含对象的创建以及生命周期,即当包含对象被销毁时被包含对象也会不复存在。例如一辆汽车拥有一个引擎是汽车类Car与引擎类Engine的组合关系。下面是组合的一些例子。

(1)通过成员变量初始化

public class Car {
    private final Engine engine = new Engine();       
}

class Engine {
    private String type;
}

(2)通过构造函数初始化

public class Car {
    private final Engine engine;  

    public Car(){
       engine  = new Engine();
    }
}
public class Engine {
    private String type;
}

3.组合要在一个限界上下文中; 聚合可以不在一个限界上下文中.

参考资料: UML类图中的六大关系:关联、聚合、组合、依赖、继承、实现

galaio commented 4 years ago

图挂了。。。楼主有空补一下么?感谢

caperea commented 4 years ago

@soulmz 多研究些架构,少谈些框架(3)-- 微服务和事件驱动

这里的图也是挂的😂文章质量很高,希望楼主有空把图修补一下👍

caperea commented 4 years ago

应该是图床挂了,域名已经无法解析了:pic.youdetan.com