akka设计模式系列-Backend模式

简介:   上一节我们介绍了Akka使用的基本模式,简单点来说就是,发消息给actor,处理结束后返回消息。但这种模式有个缺陷,就是一旦某个消息处理的比较慢,就会阻塞后面所有消息的处理。那么有没有方法规避这种阻塞呢,这就是本章要讲的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模式做了简单扩展,但这种用法我觉得比较重要,就单拎出来抽象成一个设计模式了,供大家参考。

 

目录
相关文章
|
23天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
2月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。
|
25天前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###
|
18天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
37 1
|
1月前
|
设计模式 Java Kotlin
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
本教程详细讲解Kotlin语法,适合希望深入了解Kotlin的开发者。对于快速学习Kotlin语法,推荐查看“简洁”系列教程。本文重点介绍了构建者模式在Kotlin中的应用与改良,包括如何使用具名可选参数简化复杂对象的创建过程,以及如何在初始化代码块中对参数进行约束和校验。
24 3
|
2月前
|
设计模式 算法 安全
设计模式——模板模式
模板方法模式、钩子方法、Spring源码AbstractApplicationContext类用到的模板方法
设计模式——模板模式
|
2月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:如何提高代码的可维护性与扩展性在软件开发领域,PHP 是一种广泛使用的服务器端脚本语言。随着项目规模的扩大和复杂性的增加,保持代码的可维护性和可扩展性变得越来越重要。本文将探讨 PHP 中的设计模式,并通过实例展示如何应用这些模式来提高代码质量。
设计模式是经过验证的解决软件设计问题的方法。它们不是具体的代码,而是一种编码和设计经验的总结。在PHP开发中,合理地使用设计模式可以显著提高代码的可维护性、复用性和扩展性。本文将介绍几种常见的设计模式,包括单例模式、工厂模式和观察者模式,并通过具体的例子展示如何在PHP项目中应用这些模式。
|
2月前
|
设计模式 Java Spring
spring源码设计模式分析-代理设计模式(二)
spring源码设计模式分析-代理设计模式(二)
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
39 0
|
2月前
|
设计模式 Java
Java设计模式-工厂方法模式(4)
Java设计模式-工厂方法模式(4)