akka设计模式系列-Backend模式
上一节我们介绍了Akka使用的基本模式,简单点来说就是,发消息给actor,处理结束后返回消息。但这种模式有个缺陷,就是一旦某个消息处理的比较慢,就会阻塞后面所有消息的处理。那么有没有方法规避这种阻塞呢,这就是本章要讲的Backend模式。
关于Backend模式,我们可以类比java中的线程池来理解,简单点来说就是把耗时或者阻塞的操作放到后台去执行。java中可能会将耗时的操作放到后台线程去执行,这样主线程不会阻塞。同样在Akka中我们也有类似的处理方式,只不过最简单的形式是用future来实现的。future对象用于表示异步方法获得的结果,这跟回调有点类似,但其出发点是不同的,future是可以获取结果的。
import scala.concurrent.ExecutionContext.Implicits.global
val futureResult = Future{
println(s"Future's Current timestamp ${System.currentTimeMillis()}")
Thread.sleep(1*1000)
println("Future say HelloWorld")
"Hello World"
}
println(s"Main thread's Current timestamp ${System.currentTimeMillis()}")
println("you can do other thing when future exec backend")
Thread.sleep(3*1000)
println("Main thread get future's result")
futureResult.foreach(println)
输出:
Main thread's Current timestamp 1531209777402,thread id 1
you can do other thing when future exec backend
Future's Current timestamp 1531209777403,thread id 12
Future say HelloWorld
Main thread get future's result
Hello World
从上面的例子可以看出,主线程和future几乎是同时执行的,且二者的线程ID不同。其实future还是用thread实现的,只不过又进行了封装。我们对future做了基本的介绍,下面就用future将耗时的工作放到后台执行,解决阻塞的问题。
class BackendPattern1Actor extends Actor {
implicit val executionContextExecutor: ExecutionContextExecutor = context.system.dispatcher
private def doWorkInLongTimeFor(sender:ActorRef):Unit = {
println(s"do work for $sender")
Thread.sleep(3*1000)
}
override def receive: Receive = {
case DoWorkNotInBackend(message,messageTime) =>
println(s"BackendPattern1Actor Receive DoWorkInBackend command at ${System.currentTimeMillis()}")
println(s"DoWorkInBackend message is $message,message time is $messageTime")
doWorkInLongTimeFor(sender())
println("DoWorkInBackend command done")
case DoWorkInBackend(message,messageTime) =>
println(s"BackendPattern1Actor Receive DoWorkInBackend command at ${System.currentTimeMillis()}")
println(s"DoWorkInBackend message is $message,message time is $messageTime")
val from = sender()
Future{
doWorkInLongTimeFor(from)
}
println("DoWorkInBackend command done")
}
}
object BackendPattern1{
def main(args: Array[String]): Unit = {
val system = ActorSystem("BackendPattern1",ConfigFactory.load())
val actor = system.actorOf(Props(new BackendPattern1Actor),"BackendPattern1Actor")
val cmd1 = DoWorkNotInBackend("command1",System.currentTimeMillis())
Thread.sleep(500)
val cmd2 = DoWorkNotInBackend("command2",System.currentTimeMillis())
println(s"cmd1 is [${cmd1.message},${cmd1.messageTime}]")
println(s"cmd2 is [${cmd2.message},${cmd2.messageTime}]")
actor ! cmd1
actor ! cmd2
Thread.sleep(3*1000)
val cmd3 = DoWorkInBackend("command3",System.currentTimeMillis())
Thread.sleep(500)
val cmd4 = DoWorkInBackend("command4",System.currentTimeMillis())
println(s"cmd3 is [${cmd3.message},${cmd3.messageTime}]")
println(s"cmd4 is [${cmd4.message},${cmd4.messageTime}]")
actor ! cmd3
actor ! cmd4
}
}
输出
cmd1 is [command1,1531210983581]
cmd2 is [command2,1531210984082]
BackendPattern1Actor Receive DoWorkNotInBackend command at 1531210984084
DoWorkNotInBackend message is command1,message time is 1531210983581
do work for Actor[akka://BackendPattern1/deadLetters]
DoWorkNotInBackend command done
BackendPattern1Actor Receive DoWorkNotInBackend command at 1531210987085
DoWorkNotInBackend message is command2,message time is 1531210984082
do work for Actor[akka://BackendPattern1/deadLetters]
cmd3 is [command3,1531210987084]
cmd4 is [command4,1531210987585]
DoWorkNotInBackend command done
BackendPattern1Actor Receive DoWorkInBackend command at 1531210990085
DoWorkInBackend message is command3,message time is 1531210987084
DoWorkInBackend command done
BackendPattern1Actor Receive DoWorkInBackend command at 1531210990090
DoWorkInBackend message is command4,message time is 1531210987585
DoWorkInBackend command done
do work for Actor[akka://BackendPattern1/deadLetters]
do work for Actor[akka://BackendPattern1/deadLetters]
从上面的输出,可以看出如果发送DoWorkNotInBackend消息,也就是业务逻辑直接在actor处理消息时阻塞运行时,前后两条消息间隔时间是3秒,刚好是业务逻辑的处理时间。如果发送DoWorkInBackend消息,也就是把业务逻辑放到Future中执行,前后两条消息开始处理的时间只间隔5毫秒。通过Future封装耗时、阻塞的业务逻辑是Backend模式的最基本形式。
当然机智的你可能会问,Future本质还是一个线程,那么如果业务逻辑阻塞的太多,消息又很多的时候,线程池会不会被耗尽,答案是肯定的。这样会导致没有线程处理actor的消息,后续的消息还是会阻塞。当然了解决方法还是有的,那就是给Future指定独立的executionContextExecutor,也就是指定独立的线程池。这里我就不再介绍了,留给大家去研究吧。
在Actor基本原则中有一条“actor可以创建有限数量的子actor”,我们知道actor一定会被分配一个线程去处理消息,那么能不能用actor来封装耗时、阻塞的业务逻辑呢?这就是Backend模式的另外一种形式,也是我比较喜欢的形式之一。
class BackendPattern2Actor extends Actor{
override def receive: Receive = {
case cmd @ DoWorkInBackend(message,messageTime) =>
println(s"BackendPattern2Actor receive command [$message,$messageTime] at ${System.currentTimeMillis()}")
val backend = context.actorOf(Props(new BackendActor(sender())),s"backend-$messageTime")
context.watch(backend)
backend ! cmd
}
}
class BackendActor(from:ActorRef) extends Actor{
private def doWorkInLongTimeFor(sender:ActorRef):Unit = {
println(s"do work for $sender")
Thread.sleep(3*1000)
println(s"work done ,you can send result to $sender")
}
override def receive: Receive = {
case DoWorkInBackend(message,messageTime) =>
println(s"BackendActor Receive DoWorkInBackend command at ${System.currentTimeMillis()}")
println(s"DoWorkInBackend message is $message,message time is $messageTime")
doWorkInLongTimeFor(from)
}
}
object BackendPattern2 {
def main(args: Array[String]): Unit = {
val system = ActorSystem("BackendPattern2",ConfigFactory.load())
val actor = system.actorOf(Props(new BackendPattern2Actor),"BackendPattern2Actor")
val cmd1 = DoWorkInBackend("command1",System.currentTimeMillis())
Thread.sleep(500)
val cmd2 = DoWorkInBackend("command2",System.currentTimeMillis())
println(s"cmd1 is [${cmd1.message},${cmd1.messageTime}]")
println(s"cmd2 is [${cmd2.message},${cmd2.messageTime}]")
actor ! cmd1
actor ! cmd2
}
}
输出:
cmd1 is [command1,1531211987054]
cmd2 is [command2,1531211987554]
BackendPattern2Actor receive command [command1,1531211987054] at 1531211987557
BackendPattern2Actor receive command [command2,1531211987554] at 1531211987558
BackendActor Receive DoWorkInBackend command at 1531211987559
DoWorkInBackend message is command1,message time is 1531211987054
BackendActor Receive DoWorkInBackend command at 1531211987559
DoWorkInBackend message is command2,message time is 1531211987554
do work for Actor[akka://BackendPattern2/deadLetters]
do work for Actor[akka://BackendPattern2/deadLetters]
work done ,you can send result to Actor[akka://BackendPattern2/deadLetters]
work done ,you can send result to Actor[akka://BackendPattern2/deadLetters]
在这种形式下,父actor创建了子actor,把发送者以构造函数的形式传递给子actor(前面的章节中我们讲过这个用法,当然也可用发消息的形式),然后把消息再发送给子actor,这样 子actor就会异步的处理对应的消息。看起来 这跟future的形式差不多,但其中的细节有非常大的区别。在子actor处理业务逻辑非常灵活,也可以非常方便的把结果返回调用方。用子actor的形式,在架构看来也比较统一(全都是面向actor编程),用future个人觉得会增加复杂度。试想,如果另一个actor用ask的方式请求BackendPattern2Actor会发生什么。本来应该是同步调用的,但用actor完全将所有的调用都异步执行了,这里涉及的优点这里就不再展开了。
当然如果创建了大量的阻塞子actor,同样会耗尽线程池。我们可以为actor指定独立的线程池,以减少BackendPattern2Actor的压力。这里也不作过多介绍,读者可以研究akka的官方文档,里面有详细的说明,如果实在找不到那就微信联系我再一块探讨喽。
其实Backend模式的第二种形式,我也喜欢定义为MasterWorker模式。
MasterWorker模式比较适用于workActor功能比较简单的场景,这种模式的好处就是把特定的耗时、阻塞的逻辑隔离、封装,可以对业务逻辑单独进行优化。其实MasterWorker模式还有一种变种形式,那就是只有一个workActor,也就是说worker跟随master创建且只有一个,master将对应的消息发送给改actor;当然还可以进一步进行扩展该形式,那就是为master收到的每种类型的消息创建对应的唯一一个workActor。
这种模型的一个好处就是可以对master的每种类型消息的处理做独立的性能、业务逻辑优化。例如一个workActor的功能就是把发过来的消息插入数据库,那么workActor就可以根据情况,选择以批量的形式将数据插入数据库,以提高吞吐量。如果简单的用future来实现。
今天我介绍了Backend模式,后面会介绍另外一种变异的Backend模式:Aggregate模式。这种模式比较简单,可以说仅仅是对Backend模式做了简单扩展,但这种用法我觉得比较重要,就单拎出来抽象成一个设计模式了,供大家参考。
akka设计模式系列-Backend模式的更多相关文章
- akka设计模式系列-While模式
While模式严格来说是while循环在Akka中的合理实现.while是开发过程中经常用到的语句之一,也是绝大部分编程语言都支持的语法.但while语句是一个循环,如果循环条件没有达到会一直执行wh ...
- akka设计模式系列-Chain模式
链式调用在很多框架和系统中经常存在,算不得上是我自己总结的设计模式,此处只是简单介绍在Akka中的两种实现方式.我在这边博客中简化了链式调用的场景,简化后也更符合Akka的设计哲学. trait Ch ...
- akka设计模式系列-Aggregate模式
所谓的Aggregate模式,其实就是聚合模式,跟masterWorker模式有点类似,但其出发点不同.masterWorker模式是指master向worker发送命令,worker完成某种业务逻辑 ...
- akka设计模式系列-基础模式
本文介绍akka的基本使用方法,由于属于基础功能,想不出一个很高大上的名称,此处就以基础模式命名.下文会介绍actor的使用方法,及其优劣点. class SimpleActor(name:Strin ...
- akka设计模式系列-慎用ask
慎用ask应该是Akka设计的一个准则,很多时候我们应该禁用ask.之所以单独把ask拎出来作为一篇博文,主要是akka的初学者往往对ask的使用比较疑惑. "Using ask will ...
- akka设计模式系列-消息模型(续)
在之前的akka设计模式系列-消息模型中,我们介绍了akka的消息设计方案,但随着实践的深入,发现了一些问题,这里重新梳理一下设计方法,避免之前的错误.不当的观点给大家带来误解. 命令和事件 我们仍然 ...
- akka设计模式系列
由于本人爱好Scala,顺便也就爱好Akka,但目前网上对Akka的介绍大多都是概念上或技术方向上的介绍,基本没有Akka设计模式或者Actor模型设计模式的资料.这对于Akka的普及非常不利,因为即 ...
- PHP设计模式系列 - 外观模式
外观模式 通过在必需的逻辑和方法的集合前创建简单的外观接口,外观设计模式隐藏了调用对象的复杂性. 外观设计模式和建造者模式非常相似,建造者模式一般是简化对象的调用的复杂性,外观模式一般是简化含有很多逻 ...
- akka设计模式系列-actor锚定
actor锚定模式是指使用actorSelection对acor进行锚定的设计模式,也可以说是一个对actor的引用技巧.在某些情况下,我们可能需要能够根据Actor的path锚定对应的实例.简单来说 ...
随机推荐
- Python学习笔记(3)动态类型
is运算符 ==是值相等而is必须是相同的引用才可以 l=[1,2,3] m=[1,2,3] print(l==m) # True print(l is m) # False sys模块 getref ...
- Maya Calendar POJ - 1008 (模拟)
简述 注意260天的情况,这个地方还是0年 代码 #include <iostream> #include <map> #include <sstream> usi ...
- [luogu3067 USACO12OPEN] 平衡的奶牛群
传送门 Solution 折半搜索模板题 考虑枚举每个点在左集合和右集合或者不在集合中,然后排序合并即可 Code //By Menteur_Hxy #include <cmath> #i ...
- Java 中 break和 continue 的使用方法及区别
break break可用于循环和switch...case...语句中. 用于switch...case中: 执行完满足case条件的内容内后结束switch,不执行下面的语句. eg: publi ...
- C#创建excel并释放资源
using System; using Microsoft.Office.Interop.Excel; using Excel = Microsoft.Office.Interop.Excel; us ...
- STM32F103移值FreeRtos笔记
RTOS版本:FreeRTOS_V8.2.2 一.下载FreeRTOS源文件 这个可以在百度上下载,或者在官网上面下载http://www.freertos.org/a00104.html ...
- @RequestParam 注解的使用----https://blog.csdn.net/lovincc/article/details/72800117
- SGU - 321 - The Spy Network
先上题目: 321. The Spy Network Time limit per test: 0.5 second(s)Memory limit: 65536 kilobytes input: st ...
- 10、Java并发性和多线程-线程安全与不可变性
以下内容转自http://ifeve.com/thread-safety-and-immutability/: 当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生 ...
- jq仿苹果的时间/日期选择效果
1.html文件,index.html <!DOCTYPE html> <html lang="en"> <head> <meta cha ...