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

 

目录
相关文章
|
2月前
|
设计模式 前端开发 搜索推荐
前端必须掌握的设计模式——模板模式
模板模式(Template Pattern)是一种行为型设计模式,父类定义固定流程和步骤顺序,子类通过继承并重写特定方法实现具体步骤。适用于具有固定结构或流程的场景,如组装汽车、包装礼物等。举例来说,公司年会节目征集时,蜘蛛侠定义了歌曲的四个步骤:前奏、主歌、副歌、结尾。金刚狼和绿巨人根据此模板设计各自的表演内容。通过抽象类定义通用逻辑,子类实现个性化行为,从而减少重复代码。模板模式还支持钩子方法,允许跳过某些步骤,增加灵活性。
155 11
|
3月前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
1月前
|
设计模式
「全网最细 + 实战源码案例」设计模式——模式扩展(配置工厂)
该设计通过配置文件和反射机制动态选择具体工厂,减少硬编码依赖,提升系统灵活性和扩展性。配置文件解耦、反射创建对象,新增产品族无需修改客户端代码。示例中,`CoffeeFactory`类加载配置文件并使用反射生成咖啡对象,客户端调用时只需指定名称即可获取对应产品实例。
89 40
|
1月前
|
设计模式 关系型数据库
「全网最细 + 实战源码案例」设计模式——简单工厂模式
简单工厂模式是一种创建型设计模式,通过工厂类根据传入参数创建不同类型的对象,也称“静态工厂方法”模式。其结构包括工厂类、产品接口和具体产品类。优点是封装性强、代码复用性好;缺点是扩展性差,增加新产品时需修改工厂类代码,违反开闭原则。适用于对象种类较少且调用者无需关心创建细节的场景。
57 19
|
1月前
|
设计模式 Java
「全网最细 + 实战源码案例」设计模式——生成器模式
生成器模式(Builder Pattern)是一种创建型设计模式,用于分步骤构建复杂对象。它允许用户通过控制对象构造的过程,定制对象的组成部分,而无需直接实例化细节。该模式特别适合构建具有多种配置的复杂对象。其结构包括抽象建造者、具体建造者、指挥者和产品角色。适用于需要创建复杂对象且对象由多个部分组成、构造过程需对外隐藏或分离表示与构造的场景。优点在于更好的控制、代码复用和解耦性;缺点是增加复杂性和不适合简单对象。实现时需定义建造者接口、具体建造者类、指挥者类及产品类。链式调用是常见应用方式之一。
56 12
|
1月前
|
设计模式 关系型数据库
「全网最细 + 实战源码案例」设计模式——工厂方法模式
简单工厂模式是一种创建型设计模式,通过一个工厂类根据传入参数创建不同类型的产品对象,也称“静态工厂方法”模式。其结构包括工厂类、产品接口和具体产品类。适用于创建对象种类较少且调用者无需关心创建细节的场景。优点是封装性强、代码复用性好;缺点是扩展性差,增加新产品时需修改工厂类代码,违反开闭原则。
51 15
|
3月前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###
|
3月前
|
设计模式 安全 Java
Kotlin - 改良设计模式 - 构建者模式
Kotlin - 改良设计模式 - 构建者模式
|
3月前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
66 1
|
4月前
|
设计模式 Java Kotlin
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
本教程详细讲解Kotlin语法,适合希望深入了解Kotlin的开发者。对于快速学习Kotlin语法,推荐查看“简洁”系列教程。本文重点介绍了构建者模式在Kotlin中的应用与改良,包括如何使用具名可选参数简化复杂对象的创建过程,以及如何在初始化代码块中对参数进行约束和校验。
47 3