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模式做了简单扩展,但这种用法我觉得比较重要,就单拎出来抽象成一个设计模式了,供大家参考。

 

posted @ 2018-07-10 17:15  gabry.wu  阅读(581)  评论(0编辑  收藏  举报