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

 

目录
相关文章
|
4月前
|
设计模式 Java 数据库连接
【设计模式】【创建型模式】工厂方法模式(Factory Methods)
一、入门 什么是工厂方法模式? 工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它定义了一个用于创建对象的接口,但由子类决定实例化哪个类。工厂方法模式使类的实例化延迟
112 16
|
4月前
|
设计模式 负载均衡 监控
并发设计模式实战系列(2):领导者/追随者模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第二章领导者/追随者(Leader/Followers)模式,废话不多说直接开始~
116 0
|
4月前
|
设计模式 监控 Java
并发设计模式实战系列(1):半同步/半异步模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第一章半同步/半异步(Half-Sync/Half-Async)模式,废话不多说直接开始~
92 0
|
4月前
|
设计模式 安全 Java
并发设计模式实战系列(12):不变模式(Immutable Object)
🌟 大家好,我是摘星!🌟今天为大家带来的是并发设计模式实战系列,第十二章,废话不多说直接开始~
83 0
|
4月前
|
设计模式 算法 Java
设计模式觉醒系列(04)策略模式|简单工厂模式的升级版
本文介绍了简单工厂模式与策略模式的概念及其融合实践。简单工厂模式用于对象创建,通过隐藏实现细节简化代码;策略模式关注行为封装与切换,支持动态替换算法,增强灵活性。两者结合形成“策略工厂”,既简化对象创建又保持低耦合。文章通过支付案例演示了模式的应用,并强调实际开发中应根据需求选择合适的设计模式,避免生搬硬套。最后推荐了JVM调优、并发编程等技术专题,助力开发者提升技能。
|
4月前
|
设计模式 Prometheus 监控
并发设计模式实战系列(20):扇出/扇入模式(Fan-Out/Fan-In)(完结篇)
🌟 大家好,我是摘星!🌟今天为大家带来的是并发设计模式实战系列,第二十章,废话不多说直接开始~
125 0
|
6月前
|
设计模式 Java 关系型数据库
设计模式:工厂方法模式(Factory Method)
工厂方法模式是一种创建型设计模式,通过将对象的创建延迟到子类实现解耦。其核心是抽象工厂声明工厂方法返回抽象产品,具体工厂重写该方法返回具体产品实例。适用于动态扩展产品类型、复杂创建逻辑和框架设计等场景,如日志记录器、数据库连接池等。优点包括符合开闭原则、解耦客户端与具体产品;缺点是可能增加类数量和复杂度。典型应用如Java集合框架、Spring BeanFactory等。
|
8月前
|
设计模式
「全网最细 + 实战源码案例」设计模式——模式扩展(配置工厂)
该设计通过配置文件和反射机制动态选择具体工厂,减少硬编码依赖,提升系统灵活性和扩展性。配置文件解耦、反射创建对象,新增产品族无需修改客户端代码。示例中,`CoffeeFactory`类加载配置文件并使用反射生成咖啡对象,客户端调用时只需指定名称即可获取对应产品实例。
137 40
|
8月前
|
设计模式 关系型数据库
「全网最细 + 实战源码案例」设计模式——工厂方法模式
简单工厂模式是一种创建型设计模式,通过一个工厂类根据传入参数创建不同类型的产品对象,也称“静态工厂方法”模式。其结构包括工厂类、产品接口和具体产品类。适用于创建对象种类较少且调用者无需关心创建细节的场景。优点是封装性强、代码复用性好;缺点是扩展性差,增加新产品时需修改工厂类代码,违反开闭原则。
107 15
|
8月前
|
设计模式 Java
「全网最细 + 实战源码案例」设计模式——生成器模式
生成器模式(Builder Pattern)是一种创建型设计模式,用于分步骤构建复杂对象。它允许用户通过控制对象构造的过程,定制对象的组成部分,而无需直接实例化细节。该模式特别适合构建具有多种配置的复杂对象。其结构包括抽象建造者、具体建造者、指挥者和产品角色。适用于需要创建复杂对象且对象由多个部分组成、构造过程需对外隐藏或分离表示与构造的场景。优点在于更好的控制、代码复用和解耦性;缺点是增加复杂性和不适合简单对象。实现时需定义建造者接口、具体建造者类、指挥者类及产品类。链式调用是常见应用方式之一。
129 12