需要注意的是——本文不是真正的讨论如何做煎饼果子。
相信南方人都吃过煎饼果子——一种裹着生菜和火腿肠的鸡蛋煎饼。虽然好吃,其制作过程却也不比汉堡包简单,让我们一起来拆解下吧。
图 1 ——煎饼果子拆解
从上图(这个?没有卷起来)我们可以看到,一个煎饼果子分为下面的几个部分:
- 最下面的是鸡蛋煎饼——鸡蛋是打在煎饼上的
- 中间是一层生菜——若干
- 在上面是一根火腿肠——只有一根
最后把它卷起来,套上食品塑料袋,就是煎饼果子了,如下图:
看到如此美味的煎饼果子,是不是很想自己做来吃吃,甚至是去开个店,专门卖煎饼果子呢?当上CEO,迎娶白富美,这都不是梦啊~~。那么,下面就让我们一起看看如果做一个煎饼果子吧。
如何做?
1. 准备材料
煎饼果子需要下面几种食材:
- 面粉——我们需要做煎饼,
- 鸡蛋N枚——取决于你要做的?的数量,一般一个煎饼果子,使用一枚?
- 生菜若干——平均一个煎饼果子,使用1到2片生菜,
- 火腿肠N根——一般,都会加上火腿肠,可选,一般一个煎饼果子,一根火腿肠。
准备好了原材料,让我们来做煎饼果子吧,
2. 制作步骤
2.1 使用平底锅,将高筋面粉,调匀,然后变成煎饼,如下所示:
2.2 待煎饼7分熟之后,将鸡蛋打在煎饼上,并刮匀,如下所示:
2.3 放上生菜若干(1-2片),如下所示:
现在送脆饼
2.4 放上火腿肠 1根 并卷起来
2.5 从中间纵向断开,如下所示:
制作步骤回顾:
从上面的图解中我们可以看到,流程是这样的:
- 面粉 + 水 -> 面浆;面浆 + 烘焙 -> 煎饼
- 煎饼 + 鸡蛋 + 烘焙 -> 鸡蛋煎饼
- 鸡蛋煎饼 + 生菜 -> 带有生菜的鸡蛋煎饼
- 带有生菜的鸡蛋煎饼 + 火腿肠 -> 带有生菜和火腿肠的鸡蛋煎饼
- 带有生菜和火腿肠的鸡蛋煎饼 + 卷曲 + 切断 -> 煎饼果子
我们对上面的步骤进行化简:
- 面粉 -> 煎饼
- 煎饼 + 鸡蛋 -> 鸡蛋煎饼
- 鸡蛋煎饼 + 生菜 + 火腿肠 -> 煎饼果子
我们对上面的步骤继续化简
面粉 -> 煎饼 -> 鸡蛋 -> 鸡蛋煎饼 -> 生菜 -> 火腿肠 -> 煎饼果子
下面让我们使用代码先来模拟下,验证怎么样才可以高效的做出煎饼果子吧。
N 种制作方式
一个人(傻等)
面粉 -> 煎饼 -> 鸡蛋 -> 鸡蛋煎饼 -> 生菜 -> 火腿肠 -> 煎饼果子
实现代码:
让我们运行上面的代码,其输出结果如下:
开始计时
好吃的煎饼果子勒
耗时:9 秒
如果我们偶尔做一次,其实还好啦,满足,不过作为有梦想的程序员,我们要快点,这样才有竞争力啊,对,快,更快。
一个人(手脚利索,异步做)
这就是我们牛逼的异步的方式了,即基于EventLoop/CSP的方式,事情都是一个人做,这个好了接着做下一个,在做煎饼的时候,不会等待煎饼烤熟。而是回去洗菜、准备火腿肠等。
多请三个帮手
这个时候,我们就会想,是不是多招聘几个人呢?掐指一算,对,招聘4个人吧:
这里我们,招聘了4个人,如下所示:
private static ExecutorService executorService = Executors.newFixedThreadPool(4);
人手多了,让我们来看看效果吧:
开始计时
好吃的煎饼果子勒
耗时:5 秒
是的,我们变快了~~~,只需要 5秒,我们投入了这么大的人力成本不就是要,客户第一,让顾客更加快速地买到煎饼果子么?
做点创新(换汤不换药)
上面的方式稍显老套,作为弄潮儿,最新的技术,搞搞搞,瞧,我们的透明后厨,简洁着呢:
通过上面的方式,真的好简洁,一套一套的,让我们来看看效果吧:
开始计时
好吃的煎饼果子勒
耗时:5 秒
额,竟然还是这样?
再进一步(成本优化/一个人当两个人用)
感觉这个人有点多,生意也不够好,还是换方式搞吧,某某某,你帮帮他吧,某某某,你也别闲着,把XXX也搞了。
不行,这样不行,一定要从根本上解决问题,对,我们来梳理一下。
开店成功需要几个要素?!
- 成本节约,在等待的时候,可以帮忙做点别的事情。
- 结构优化,效率提升,建立完整的上下行监管机制,消息顺达。
- 精简语义,使用专用词沟通,减少自然语言表意不明。
- 客户第一,提高服务质量,尽快返回结果,不要超出购买者的能够忍受的最长等待时间。
所以,对我们的例子使用Actor模型来进行建模,然后应用CQRS来减少语义。当然,这里我们的事件并没有持久化,算是部分实现。
-
Actor模型 + Facade门面模式:
其中,使用 Ask 模式,我们接驳了传统的门店服务,以及基于Actor 模型的店员。而对于我们的店员,我们又使用了Actor模型以及监管机制,同时使用 CQRS 来对命令和事件进行分离。
其中我们有命令:
在我们的门店服务内部,将会对这些命令进行处理,并且产生相应的事件。
再和门店的接驳处,使用了Ask模式:
而对于老板/店长来说,他肯定是自己不处理的,所以他将任务,派发给了煎饼果子大侠
,即:
注意,上面我们又一次使用了Ask模式,而非FSM。
这项任务就到达了我们的煎饼果子大侠了,他的任务可重了,因为他需要等待的东西有:
- 鸡蛋煎饼
- 洗好的生菜
- 撕开好的火腿肠
所以:我们的煎饼果子大侠会分别和,鸡蛋煎饼太郎、生菜小二哥以及火腿肠大叔形成依赖关系,并且会依赖于他们的结果,才可以做出一个完整的煎饼果子:
当收到老板的命令的时候,他将任务进行了拆解,分发给了和他合作的其他店员:
这里,我们搭配使用了Ask模式和Aggregator模式,将从各个部分收集到的结果,进行汇总,并产生了最终的美味的煎饼果子。
需要注意的是,我们在这里,并没有看到煎饼是怎么来的,而是直接看到的是鸡蛋煎饼,这就是DDD中的领域分层和依赖了:
我们的鸡蛋煎饼太郎和煎饼西施之间是一个强依赖关系:
这里,我们看到了一个奇怪的地方,即不对称的超时设置。因为太郎对西施特别好,所以顶住压力,不管怎么催他,他都会给西施说,别着急,慢慢来。
这就引发一个问题了,不正确的超时设置,可能让消费者非常不耐烦,本来消费者已经等了30s了,结果你对他说,哎呀,我们的鸡蛋煎饼太郎太忙了,然后顾客灰溜溜的走了,丢失了大量的潜在客户。
即——不合适的超时配置,会造成服务质量的下降。
让我们来把店开起来,并且提供服务吧
现在店开起来了,让我们来看一看运行结果吧。
开始计时
老板,煎饼果子来一个
大侠,做个煎饼果子
太郎,做鸡蛋煎饼
小二哥,切生菜
大叔,撕火腿肠
西施,做煎饼
太郎:我在做鸡蛋煎饼
大侠:我在开始做煎饼果子了
老板:大侠已经做好了啊?!
好吃的煎饼果子勒
耗时:5 秒
喔,完美的组织
架构和模式应用!当然,我们这里没有对Event进行持久化,这一点儿是不利于回溯的,同时也没有应用断路器模式,以及还有多处不合理的超时配置。
上面的这些方式,都让我们不难想,如何让客户更少的等待,如何提供更好的服务,如果我们的心更加大,如果我们要把店面做大,甚至要开连锁店,或者开煎饼果子工厂呢?
技术,不是给业务以限制,而是助力其想象。
Reactive-Stream/反应式流的方式
如果,我们想要将这个模式,复制到更多的场景,甚至开一条生产,N调生产线,如果我们要这些生产线能实现智能的调控,达到最小的资源占用,来达到最大的效力,那么我们应该怎么做呢?
对于这个问题,在2013年到2014年,业界也在思考,后来几经波折,想到了一种基于流的拓扑描述的方式。下面就让我们使用反应式流的方式,来实现上面的业务吧。
首先来一个不太清晰的例子,这个例子中,我们使用了Akka-Stream——一个ReactiveStream的实现。
在上图中,我们对抽象进行了下面几点改进:
- 我们的消费请求,被抽象为了一个流,这个流,类似于我们做的供给侧改革,使用消费请求,来指导我们的生产。
- 我们的店员,不再是基本的店员了,把他们想成持续提供煎饼,鸡蛋煎饼,生菜,和撕开的火腿肠,以及煎饼果子的流,即生产线。
- 我们可以动态地控制生产速度,如果消费者多,就在能力满足的情况下,尽量地多生产,在某项能力不能满足目前需求的情况下,就进行复制这些服务,对其进行复制,以提高更搞的生产力。比如,做煎饼是个比较缓慢的动作,那么我们可以再加一条生产线,这个生产线专门生产煎饼。
- 有了这个流,我们发现,煎饼的生产和鸡蛋煎饼的生产,总是强依赖的,那么我们可以将他们部署到临近的生产线,减少成本。
- 同上,我们发现鸡蛋煎饼和生菜以及火腿肠的产生,也是最终要进行合并使用,那么我们也可把这三种生产线,排布在一起,这样降低了将这三种材料,运输到煎饼果子大哥的时间。
- 如果我们发现煎饼果子大哥这个只做煎饼的过程很慢了,这个时候我们可以只在这个流程节点上,进行复制,从而加快煎饼果子的卷曲和打包过程。
- 我们如果一个发货窗口发不过来,我们可以多开两个门店/物流发货窗口。
- 我们可以使用类似于由仓库直接发货的方式,将生产好的热腾腾的煎饼果子,直接交给购买者,而不用通过我们的店长或者店面,在消息模式中,这叫做Forward模式。
- 即——我们描述了依赖,而再有了这样的依赖之后,基于我们的需求和供应信息。我们可以处处都进行复制流程,复制处理节点,从而做到最优化地让热腾腾的煎饼果子到达用户的手里。
- 当然,如果我们实在是,实在是不能再扩展生产线了,那么我们就会进行回压,回压的时候,我们在最前面,就会让客户等待,或者在满足SLA的情况下,进行一些策略,但是,我们整体的服务质量,依然是那么得好,RS,就是双向的流,控制流,数据流。
即——类似于下面的结构:
我们将生产好的结果,直接递交给了消费者。
即——可以做到图中的任意一个方框内的结构拓扑,都是可以单独地进行复制,和调控的,甚至是智能地进行调控:)。智慧物流,我们也有智慧服务。
分别使用Akka-Stream、RxJava2以及Flux来实现
有了上面的实现,我们可以使用现在的一些语义化的工具来进行描述,比如:
好了,我们再看一种:
然后,我们再看一种:
然后仔细一下对比:
我们都需要煎饼Flow/Source,而且从煎饼Flow变成了鸡蛋煎饼Flow/Source
我们都还需要生菜Flow/Source,以及火腿肠Flow/Source。
我们从鸡蛋煎饼Flow/Source 、生菜Flow/Source以及火腿肠Flow/Source,构建了一条煎饼果子的Flow/Source。如果我们把Flow/Source看做生产线,喔喔,我们根据三个现有的Flow/Source,构建了一条煎饼果子的Flow/Source。
最后,我们只从里面拿了一个煎饼果子出来:)
上面我们还有一点,就是异步和并发怎么控制?我如何并发动做一些事情呢?
或者
非常简单,如果我们想要同步的呢?去掉红框中的部分就好了。
也就是说,我们描绘了整个服务的编排,然后我们便可以方便地对任意特定的子拓扑进行优化了。开辟新的生产线,或者通过复制某个处理的过程来对某个流程进行并发执行,以提高其生产效率,当然在真的扩不了的情况下,保护我们的系统,保障我们SLA。
那么,我们如何不断地获取我们的美味煎饼果子呢?早上喜欢吃煎饼果子的人太多,都要疯掉了,好,请看:
或者
剩下的留作练习:)
只要我们的请求不断,我们的
就会持续的产生结果。多么简单直接,而且非常的优雅。复用这样的模式,开连锁店,开工厂,智能化的工业生产,人生巅峰不是梦。
拓展思考?
基于这个例子,我们可以看到,如果我们的服务代码,通过上面的方式来编写,那么势必更加地简洁优雅,而且我们也具备了更细腻的控制力,并且也有了更高屋建瓴的拓扑、思维建模以及全局优化的可能。同时,我们的服务,都是由反应式流中流转的信号量进行驱动的,从而实现了动态的推拉结合——对,没错,控制论中的知识:)
我相信,通过面向流的编程,从数据first,切换服务编排,请求/响应拓扑first的思维,将会大大地提高我们对链路的理解能力。同时,有了这些工具,我们常见的反应式设计模式,都可以非常方便地应用,并完全可以结合FP以及DDD的一些思路,打造更加清晰、明了、性能优异的系统。而且,我们仅仅描述了服务的编排,从而产生拓扑,而剩下的事情,只需要动动手指,也许手指都不需要动:),这一切,都将会有下一代的架构来智能地保证。
小结
从上面,大家已经看到了,我们的编程模式,是如何一步一步地从传统的方式,变成我们的Reactive 化的方式。通过Reactive 的方式,我们可以方便的实现服务编排,有了服务编排之后,我们可以做更多的事情,服务将会是更加智能化的,而非一成不变,提升了客户体验、资源利用率,并降低了资源的浪费。
原文发布时间为:2018-02-07
本文作者:虎鸣
本文来自云栖社区合作伙伴“淘宝技术”,了解相关信息可以关注“淘宝技术”微信公众号