做煎饼果子的N种方式——From Sequential to Reactive

简介:

需要注意的是——本文不是真正的讨论如何做煎饼果子。

相信南方人都吃过煎饼果子——一种裹着生菜和火腿肠的鸡蛋煎饼。虽然好吃,其制作过程却也不比汉堡包简单,让我们一起来拆解下吧。

797cef7819db469706a072d80ed67f6096340d94

图 1 ——煎饼果子拆解

从上图(这个?没有卷起来)我们可以看到,一个煎饼果子分为下面的几个部分:

  1. 最下面的是鸡蛋煎饼——鸡蛋是打在煎饼上的
  2. 中间是一层生菜——若干
  3. 在上面是一根火腿肠——只有一根

最后把它卷起来,套上食品塑料袋,就是煎饼果子了,如下图:

0b11334f83b044d7b91d0f478047c3e46ebae370

看到如此美味的煎饼果子,是不是很想自己做来吃吃,甚至是去开个店,专门卖煎饼果子呢?当上CEO,迎娶白富美,这都不是梦啊~~。那么,下面就让我们一起看看如果做一个煎饼果子吧。

如何做?


1. 准备材料

煎饼果子需要下面几种食材:

  • 面粉——我们需要做煎饼,
  • 鸡蛋N枚——取决于你要做的?的数量,一般一个煎饼果子,使用一枚?
  • 生菜若干——平均一个煎饼果子,使用1到2片生菜,
  • 火腿肠N根——一般,都会加上火腿肠,可选,一般一个煎饼果子,一根火腿肠。

ccdaa0942e908b4d689347b5d7f29520b62c7ee8

准备好了原材料,让我们来做煎饼果子吧,

2. 制作步骤


2.1 使用平底锅,将高筋面粉,调匀,然后变成煎饼,如下所示:

29e8cff476bba3fcabfc3adebd28b7b85a718908

2.2 待煎饼7分熟之后,将鸡蛋打在煎饼上,并刮匀,如下所示:

6b80d069bc58331c95bb40332f7d258ae66ca503

2.3 放上生菜若干(1-2片),如下所示:

6e91b2be11b6963cc643865e594b5b1b3b0535f5

现在送脆饼

2.4 放上火腿肠 1根 并卷起来

15acd580492b27dab4f300d2038b36d79c25a7fe

2.5 从中间纵向断开,如下所示:

56ed6dae7ab295fd9907b155db5a13e5acfaeb7b

制作步骤回顾:

从上面的图解中我们可以看到,流程是这样的:

  1. 面粉 + 水 -> 面浆;面浆 + 烘焙 -> 煎饼
  2. 煎饼 + 鸡蛋 + 烘焙 -> 鸡蛋煎饼
  3. 鸡蛋煎饼 + 生菜 -> 带有生菜的鸡蛋煎饼
  4. 带有生菜的鸡蛋煎饼 + 火腿肠 -> 带有生菜和火腿肠的鸡蛋煎饼
  5. 带有生菜和火腿肠的鸡蛋煎饼 + 卷曲 + 切断 -> 煎饼果子

我们对上面的步骤进行化简:

  1. 面粉 -> 煎饼
  2. 煎饼 + 鸡蛋 -> 鸡蛋煎饼
  3. 鸡蛋煎饼 + 生菜 + 火腿肠 -> 煎饼果子

我们对上面的步骤继续化简

面粉 -> 煎饼 -> 鸡蛋 -> 鸡蛋煎饼 -> 生菜 -> 火腿肠 -> 煎饼果子

下面让我们使用代码先来模拟下,验证怎么样才可以高效的做出煎饼果子吧。

N 种制作方式


一个人(傻等)

面粉 -> 煎饼 -> 鸡蛋 -> 鸡蛋煎饼 -> 生菜 -> 火腿肠 -> 煎饼果子

实现代码:

f560bbce5a51c987d338f8bb73119c626055bec6

让我们运行上面的代码,其输出结果如下:

开始计时
好吃的煎饼果子勒
耗时:9

如果我们偶尔做一次,其实还好啦,满足,不过作为有梦想的程序员,我们要快点,这样才有竞争力啊,对,快,更快。

一个人(手脚利索,异步做)

这就是我们牛逼的异步的方式了,即基于EventLoop/CSP的方式,事情都是一个人做,这个好了接着做下一个,在做煎饼的时候,不会等待煎饼烤熟。而是回去洗菜、准备火腿肠等。

多请三个帮手

这个时候,我们就会想,是不是多招聘几个人呢?掐指一算,对,招聘4个人吧:

e453a7556d0da863fa79eba9e603d3ccd5923ca0

这里我们,招聘了4个人,如下所示:

private static ExecutorService executorService = Executors.newFixedThreadPool(4);

人手多了,让我们来看看效果吧:

开始计时
好吃的煎饼果子勒
耗时:5

是的,我们变快了~~~,只需要 5秒,我们投入了这么大的人力成本不就是要,客户第一,让顾客更加快速地买到煎饼果子么?

做点创新(换汤不换药)

上面的方式稍显老套,作为弄潮儿,最新的技术,搞搞搞,瞧,我们的透明后厨,简洁着呢:

1b505e7029552c209822145a8569c759c11f40f4

通过上面的方式,真的好简洁,一套一套的,让我们来看看效果吧:

开始计时
好吃的煎饼果子勒
耗时:5

额,竟然还是这样?

再进一步(成本优化/一个人当两个人用)

感觉这个人有点多,生意也不够好,还是换方式搞吧,某某某,你帮帮他吧,某某某,你也别闲着,把XXX也搞了。

不行,这样不行,一定要从根本上解决问题,对,我们来梳理一下。

开店成功需要几个要素?!

  1. 成本节约,在等待的时候,可以帮忙做点别的事情。
  2. 结构优化,效率提升,建立完整的上下行监管机制,消息顺达。
  3. 精简语义,使用专用词沟通,减少自然语言表意不明。
  4. 客户第一,提高服务质量,尽快返回结果,不要超出购买者的能够忍受的最长等待时间。

所以,对我们的例子使用Actor模型来进行建模,然后应用CQRS来减少语义。当然,这里我们的事件并没有持久化,算是部分实现。

  1. Actor模型 + Facade门面模式:

dbc31d1043ff4c918d6e53e9cf62cd60fb705574

其中,使用 Ask 模式,我们接驳了传统的门店服务,以及基于Actor 模型的店员。而对于我们的店员,我们又使用了Actor模型以及监管机制,同时使用 CQRS 来对命令和事件进行分离。

其中我们有命令:

a3d12a5e246ac4b27258c93fd4861da147bfd5ab

在我们的门店服务内部,将会对这些命令进行处理,并且产生相应的事件。

23668d079f45e046cb46ed5e49434fcdd0ff6354

再和门店的接驳处,使用了Ask模式:

97913fc90f7dc04c46433ecc292756f6302e1562

而对于老板/店长来说,他肯定是自己不处理的,所以他将任务,派发给了煎饼果子大侠,即:

689da1d072aa946cb47a094107f2897b428663b4

注意,上面我们又一次使用了Ask模式,而非FSM。

这项任务就到达了我们的煎饼果子大侠了,他的任务可重了,因为他需要等待的东西有:

  1. 鸡蛋煎饼
  2. 洗好的生菜
  3. 撕开好的火腿肠

所以:我们的煎饼果子大侠会分别和,鸡蛋煎饼太郎、生菜小二哥以及火腿肠大叔形成依赖关系,并且会依赖于他们的结果,才可以做出一个完整的煎饼果子:

71e538d5fb3eb35faa4de776e351c756b5616dbe

当收到老板的命令的时候,他将任务进行了拆解,分发给了和他合作的其他店员:

2f03b3f91d75b566cf15ce500fcc2114a0209eb1

这里,我们搭配使用了Ask模式和Aggregator模式,将从各个部分收集到的结果,进行汇总,并产生了最终的美味的煎饼果子。

需要注意的是,我们在这里,并没有看到煎饼是怎么来的,而是直接看到的是鸡蛋煎饼,这就是DDD中的领域分层和依赖了:

我们的鸡蛋煎饼太郎和煎饼西施之间是一个强依赖关系:

05a679abfdfb1900487f3680b88138b07cd4740d

这里,我们看到了一个奇怪的地方,即不对称的超时设置。因为太郎对西施特别好,所以顶住压力,不管怎么催他,他都会给西施说,别着急,慢慢来。

这就引发一个问题了,不正确的超时设置,可能让消费者非常不耐烦,本来消费者已经等了30s了,结果你对他说,哎呀,我们的鸡蛋煎饼太郎太忙了,然后顾客灰溜溜的走了,丢失了大量的潜在客户。

即——不合适的超时配置,会造成服务质量的下降。

让我们来把店开起来,并且提供服务吧

cf16061126c2e45ded88057b3b387e36e515221b

现在店开起来了,让我们来看一看运行结果吧。

开始计时
老板,煎饼果子来一个
大侠,做个煎饼果子
太郎,做鸡蛋煎饼
小二哥,切生菜
大叔,撕火腿肠
西施,做煎饼
太郎:我在做鸡蛋煎饼
大侠:我在开始做煎饼果子了
老板:大侠已经做好了啊?!
好吃的煎饼果子勒
耗时:5

喔,完美的组织

架构和模式应用!当然,我们这里没有对Event进行持久化,这一点儿是不利于回溯的,同时也没有应用断路器模式,以及还有多处不合理的超时配置。

上面的这些方式,都让我们不难想,如何让客户更少的等待,如何提供更好的服务,如果我们的心更加大,如果我们要把店面做大,甚至要开连锁店,或者开煎饼果子工厂呢?

技术,不是给业务以限制,而是助力其想象。

Reactive-Stream/反应式流的方式


如果,我们想要将这个模式,复制到更多的场景,甚至开一条生产,N调生产线,如果我们要这些生产线能实现智能的调控,达到最小的资源占用,来达到最大的效力,那么我们应该怎么做呢?

对于这个问题,在2013年到2014年,业界也在思考,后来几经波折,想到了一种基于流的拓扑描述的方式。下面就让我们使用反应式流的方式,来实现上面的业务吧。

首先来一个不太清晰的例子,这个例子中,我们使用了Akka-Stream——一个ReactiveStream的实现。

f4c53accc889fbe6302e28193b533a1d0c94bbd3

在上图中,我们对抽象进行了下面几点改进:

  1. 我们的消费请求,被抽象为了一个流,这个流,类似于我们做的供给侧改革,使用消费请求,来指导我们的生产。
  2. 我们的店员,不再是基本的店员了,把他们想成持续提供煎饼,鸡蛋煎饼,生菜,和撕开的火腿肠,以及煎饼果子的流,即生产线。
  3. 我们可以动态地控制生产速度,如果消费者多,就在能力满足的情况下,尽量地多生产,在某项能力不能满足目前需求的情况下,就进行复制这些服务,对其进行复制,以提高更搞的生产力。比如,做煎饼是个比较缓慢的动作,那么我们可以再加一条生产线,这个生产线专门生产煎饼。
  4. 有了这个流,我们发现,煎饼的生产和鸡蛋煎饼的生产,总是强依赖的,那么我们可以将他们部署到临近的生产线,减少成本。
  5. 同上,我们发现鸡蛋煎饼和生菜以及火腿肠的产生,也是最终要进行合并使用,那么我们也可把这三种生产线,排布在一起,这样降低了将这三种材料,运输到煎饼果子大哥的时间。
  6. 如果我们发现煎饼果子大哥这个只做煎饼的过程很慢了,这个时候我们可以只在这个流程节点上,进行复制,从而加快煎饼果子的卷曲和打包过程。
  7. 我们如果一个发货窗口发不过来,我们可以多开两个门店/物流发货窗口。
  8. 我们可以使用类似于由仓库直接发货的方式,将生产好的热腾腾的煎饼果子,直接交给购买者,而不用通过我们的店长或者店面,在消息模式中,这叫做Forward模式。
  9. 即——我们描述了依赖,而再有了这样的依赖之后,基于我们的需求和供应信息。我们可以处处都进行复制流程,复制处理节点,从而做到最优化地让热腾腾的煎饼果子到达用户的手里。
  10. 当然,如果我们实在是,实在是不能再扩展生产线了,那么我们就会进行回压,回压的时候,我们在最前面,就会让客户等待,或者在满足SLA的情况下,进行一些策略,但是,我们整体的服务质量,依然是那么得好,RS,就是双向的流,控制流,数据流。

即——类似于下面的结构:

06b1f3aa0ea64832eca644a5d1a395aed2935182

我们将生产好的结果,直接递交给了消费者。

即——可以做到图中的任意一个方框内的结构拓扑,都是可以单独地进行复制,和调控的,甚至是智能地进行调控:)。智慧物流,我们也有智慧服务。

分别使用Akka-Stream、RxJava2以及Flux来实现

有了上面的实现,我们可以使用现在的一些语义化的工具来进行描述,比如:

d2d32803f75b2653710d687f46d6d6ed00d7ee7f

好了,我们再看一种:

b6ff2c5bef182a3e2d96e630069a458718dfa0a7

然后,我们再看一种:

52857bfdeca1fabd84b39e733f57e9c7d5f5eb59

然后仔细一下对比:

9bee133cd26e489003d8314cb0ae1e361519f78e

我们都需要煎饼Flow/Source,而且从煎饼Flow变成了鸡蛋煎饼Flow/Source

62bb0a0c7323ffb86a23251b3b6980b693269fd3

我们都还需要生菜Flow/Source,以及火腿肠Flow/Source。

a1d671567d428a7aef6515ea0e392806e672b98d

我们从鸡蛋煎饼Flow/Source 、生菜Flow/Source以及火腿肠Flow/Source,构建了一条煎饼果子的Flow/Source。如果我们把Flow/Source看做生产线,喔喔,我们根据三个现有的Flow/Source,构建了一条煎饼果子的Flow/Source。

最后,我们只从里面拿了一个煎饼果子出来:)

13146de5b19eb2387b4c5951ccc523cc24e59ae8

上面我们还有一点,就是异步和并发怎么控制?我如何并发动做一些事情呢?

ec66ed25cbd5588ad118e7dc1ca7806807b2b7aa

或者

77939629da7ce39147664d5db68e5f2c8da5d074

非常简单,如果我们想要同步的呢?去掉红框中的部分就好了。

也就是说,我们描绘了整个服务的编排,然后我们便可以方便地对任意特定的子拓扑进行优化了。开辟新的生产线,或者通过复制某个处理的过程来对某个流程进行并发执行,以提高其生产效率,当然在真的扩不了的情况下,保护我们的系统,保障我们SLA。

那么,我们如何不断地获取我们的美味煎饼果子呢?早上喜欢吃煎饼果子的人太多,都要疯掉了,好,请看:

fea4069be1a116b5f918cd6dd39528a307d61ac5

或者

剩下的留作练习:)

只要我们的请求不断,我们的

4e6b20b8b2673e4521c10a7622850caec108d5de

就会持续的产生结果。多么简单直接,而且非常的优雅。复用这样的模式,开连锁店,开工厂,智能化的工业生产,人生巅峰不是梦。

拓展思考?

基于这个例子,我们可以看到,如果我们的服务代码,通过上面的方式来编写,那么势必更加地简洁优雅,而且我们也具备了更细腻的控制力,并且也有了更高屋建瓴的拓扑、思维建模以及全局优化的可能。同时,我们的服务,都是由反应式流中流转的信号量进行驱动的,从而实现了动态的推拉结合——对,没错,控制论中的知识:)

我相信,通过面向流的编程,从数据first,切换服务编排,请求/响应拓扑first的思维,将会大大地提高我们对链路的理解能力。同时,有了这些工具,我们常见的反应式设计模式,都可以非常方便地应用,并完全可以结合FP以及DDD的一些思路,打造更加清晰、明了、性能优异的系统。而且,我们仅仅描述了服务的编排,从而产生拓扑,而剩下的事情,只需要动动手指,也许手指都不需要动:),这一切,都将会有下一代的架构来智能地保证。

小结

从上面,大家已经看到了,我们的编程模式,是如何一步一步地从传统的方式,变成我们的Reactive 化的方式。通过Reactive 的方式,我们可以方便的实现服务编排,有了服务编排之后,我们可以做更多的事情,服务将会是更加智能化的,而非一成不变,提升了客户体验、资源利用率,并降低了资源的浪费。


原文发布时间为:2018-02-07

本文作者:虎鸣

本文来自云栖社区合作伙伴“淘宝技术”,了解相关信息可以关注“淘宝技术”微信公众号



相关文章
|
6月前
v-model的实现原理
v-model的实现原理
47 0
|
6月前
|
JavaScript
v-model绑定vuex的state
v-model绑定vuex的state
|
4月前
|
JavaScript
vue 【详解】父子组件传值、父子组件数据双向绑定 —— : | update: |.sync | v-bind.sync | v-model(含model选项和自定义v-model)
vue 【详解】父子组件传值、父子组件数据双向绑定 —— : | update: |.sync | v-bind.sync | v-model(含model选项和自定义v-model)
43 1
|
4月前
vue3【实用教程】v-model(含给 v-model 添加参数,绑定多个 v-model ,v-model 的内置修饰符,自定义 v-model 的修饰符等)
vue3【实用教程】v-model(含给 v-model 添加参数,绑定多个 v-model ,v-model 的内置修饰符,自定义 v-model 的修饰符等)
179 0
|
4月前
|
JavaScript
vue中v-bind和v-model有什么区别?
在Vue.js中,v-bind和v-model都是指令,用于实现数据和DOM元素之间的双向绑定,但它们的使用场景和功能有所区别。
109 0
|
6月前
Vite中使用 Model绑定组件值
Vite中使用 Model绑定组件值
50 0
|
6月前
v-model绑定vuex的state怎么实现?
v-model绑定vuex的state怎么实现?
|
6月前
|
JavaScript 数据安全/隐私保护
v-model和.sync的区别
v-model和.sync的区别
147 0
|
JavaScript 前端开发
vue中v-model和v-bind区别
vue中v-model和v-bind区别