scala_Akka并发编程框架

本文详细介绍了Scala中的Akka并发编程框架,包括Akka的介绍、特性、通信过程和Actor的创建。通过实例展示了如何创建和加载Actor,实现Actor间的通信,以及如何使用Akka进行定时任务和跨进程通信。此外,还提供了简易版Spark通信框架的案例分析,涵盖了工程搭建、Master和Worker的构建、注册及心跳机制的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Akka并发编程框架简介



Akka介绍

Akka是一个用于构建高并发、分布式和可扩展的基于事件驱动的应用的工具包。Akka是使用scala开发的库,同时可以使用scala和Java语言来开发基于Akka的应用程序。



Akka特性

  • 提供基于异步非阻塞、高性能的事件驱动编程模型
  • 内置容错机制,允许Actor在出错时进行恢复或者重置操作
  • 超级轻量级的事件处理(每GB堆内存几百万Actor)
  • 使用Akka可以在单机上构建高并发程序,也可以在网络中构建分布式程序。


Akka通信过程

以下图片说明了Akka Actor的并发编程模型的基本流程:

  1. 学生创建一个ActorSystem
  2. 通过ActorSystem来创建一个ActorRef(老师的引用),并将消息发送给ActorRef
  3. ActorRef将消息发送给Message Dispatcher(消息分发器)
  4. Message Dispatcher将消息按照顺序保存到目标Actor的MailBox中
  5. Message Dispatcher将MailBox放到一个线程中
  6. MailBox按照顺序取出消息,最终将它递给TeacherActor接受的方法中

在这里插入图片描述



创建Actor

Akka中,也是基于Actor来进行编程的。类似于之前学习过的Actor。但是Akka的Actor的编写、创建方法和之前有一些不一样。



API介绍

ActorSystem

在Akka中,ActorSystem是一个重量级的结构,它需要分配多个线程,所以在实际应用中,ActorSystem通常是一个单例对象,可以使用这个ActorSystem创建很多Actor。它负责创建和监督actor


Actor中获取ActorSystem

直接使用context.system就可以获取到管理该Actor的ActorSystem的引用


实现Actor类

  • 继承Actor(注意:要导入akka.actor包下的Actor
  • 实现receive方法,receive方法中直接处理消息即可,不需要添加loop和react方法调用。Akka会自动调用receive来接收消息
  • 【可选】还可以实现preStart()方法,该方法在Actor对象构建后执行,在Actor声明周期中仅执行一次

加载Akka Actor

  1. 要创建Akka的Actor,必须要先获取创建一个ActorSystem。需要给ActorSystem指定一个名称,并可以去加载一些配置项(后面会使用到)
  2. 调用ActorSystem.actorOf(Props(Actor对象), “Actor名字”)来加载Actor

Actor Path

每一个Actor都有一个Path,就像使用Spring MVC编写一个Controller/Handler一样,这个路径可以被外部引用。路径的格式如下:

Actor类型路径示例
本地Actorakka://actorSystem名称/user/Actor名称akka://SimpleAkkaDemo/user/senderActor
远程Actorakka.tcp://my-sys@ip地址:port/user/Actor名称akka.tcp://192.168.10.17:5678/user/service-b

入门案例

案例说明

基于Akka创建两个Actor,Actor之间可以互相发送消息。

在这里插入图片描述



实现步骤

  1. 创建Maven模块
  2. 创建并加载Actor
  3. 发送/接收消息
 <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <encoding>UTF-8</encoding>
        <scala.version>2.11.12</scala.version>
        <scala.compat.version>2.11</scala.compat.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>

        <dependency>
            <groupId>com.typesafe.akka</groupId>
            <artifactId>akka-actor_2.11</artifactId>
            <version>2.3.14</version>
        </dependency>

        <dependency>
            <groupId>com.typesafe.akka</groupId>
            <artifactId>akka-remote_2.11</artifactId>
            <version>2.3.14</version>
        </dependency>

    </dependencies>

    <build>
        <sourceDirectory>src/main/scala</sourceDirectory>
        <testSourceDirectory>src/test/scala</testSourceDirectory>
        <plugins>
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                        <configuration>
                            <args>
                                <arg>-dependencyfile</arg>
                                <arg>${project.build.directory}/.scala_dependencies</arg>
                            </args>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.4.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>reference.conf</resource>
                                </transformer>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass></mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

1. 创建Maven模块

使用Akka需要导入Akka库,我们这里使用Maven来管理项目

  1. 创建Maven模块
  2. 打开pom.xml文件,导入akka Maven依赖和插件

2. 创建并加载Actor

创建两个Actor

  • SenderActor:用来发送消息
  • ReceiveActor:用来接收,回复消息

创建Actor

  1. 创建ActorSystem
  2. 创建自定义Actor
  3. ActorSystem加载Actor

3. 发送/接收消息

  • 使用样例类封装消息
  • SubmitTaskMessage——提交任务消息
  • SuccessSubmitTaskMessage——任务提交成功消息
  • 使用类似于之前学习的Actor方式,使用!发送异步消息


参考代码

case class SubmitTaskMessage(msg:String)
case class SuccessSubmitTaskMessage(msg:String)

// 注意:要导入的是Akka下的Actor
object SenderActor extends Actor {

  override def preStart(): Unit = println("执行SenderActor的preStart()方法")

  override def receive: Receive = {
    case "start" =>
      val receiveActor = this.context.actorSelection("/user/receiverActor")
      receiveActor ! SubmitTaskMessage("请完成#001任务!")
    case SuccessSubmitTaskMessage(msg) =>
      println(s"接收到来自${sender.path}的消息: $msg")
  }
}

object ReceiverActor extends Actor {

  override def preStart(): Unit = println("执行ReceiverActor()方法")

  override def receive: Receive = {
    case SubmitTaskMessage(msg) =>
      println(s"接收到来自${sender.path}的消息: $msg")
      sender ! SuccessSubmitTaskMessage("完成提交")
    case _ => println("未匹配的消息类型")
  }
}

object SimpleAkkaDemo {
  def main(args: Array[String]): Unit = {
    val actorSystem = ActorSystem("SimpleAkkaDemo", ConfigFactory.load())

    val senderActor: ActorRef = actorSystem.actorOf(Props(SenderActor), "senderActor")
    val receiverActor: ActorRef = actorSystem.actorOf(Props(ReceiverActor), "receiverActor")

    senderActor ! "start"
      
  }
}

程序输出:

接收到来自akka://SimpleAkkaDemo/user/senderActor的消息: 请完成#001任务!
接收到来自akka://SimpleAkkaDemo/user/receiverActor的消息: 完成提交

Akka定时任务

如果我们想要使用Akka框架定时的执行一些任务,该如何处理呢?



使用方式

Akka中,提供一个scheduler对象来实现定时调度功能。使用ActorSystem.scheduler.schedule方法,可以启动一个定时任务。


schedule方法针对scala提供两种使用形式:

第一种:发送消息

def schedule(
    initialDelay: FiniteDuration,		// 延迟多久后启动定时任务
    interval: FiniteDuration,			// 每隔多久执行一次
    receiver: ActorRef,					// 给哪个Actor发送消息
    message: Any)						// 要发送的消息
(implicit executor: ExecutionContext)	// 隐式参数:需要手动导入

第二种:自定义实现

def schedule(
    initialDelay: FiniteDuration,			// 延迟多久后启动定时任务
    interval: FiniteDuration				// 每隔多久执行一次
)(f: ⇒ Unit)								// 定期要执行的函数,可以将逻辑写在这里
(implicit executor: ExecutionContext)		// 隐式参数:需要手动导入


示例一

示例说明

  • 定义一个Actor,每1秒发送一个消息给Actor,Actor收到后打印消息
  • 使用发送消息方式实现

参考代码

 // 1. 创建一个Actor,用来接收消息,打印消息
  object ReceiveActor extends Actor {
    override def receive: Receive = {
      case x => println(x)
    }
  }

  // 2. 构建ActorSystem,加载Actor
  def main(args: Array[String]): Unit = {
    val actorSystem = ActorSystem("actorSystem", ConfigFactory.load())
    val receiveActor = actorSystem.actorOf(Props(ReceiveActor))

    // 3. 启动scheduler,定期发送消息给Actor
    // 导入一个隐式转换
    import scala.concurrent.duration._
    // 导入隐式参数
    import actorSystem.dispatcher

    actorSystem.scheduler.schedule(0 seconds,
      1 seconds,
      receiveActor, "hello")
  }


示例二

示例说明

  • 定义一个Actor,每1秒发送一个消息给Actor,Actor收到后打印消息
  • 使用自定义方式实现

参考代码

object SechdulerActor extends Actor {
  override def receive: Receive = {
    case "timer" => println("收到消息...")
  }
}

object AkkaSchedulerDemo {
  def main(args: Array[String]): Unit = {
    val actorSystem = ActorSystem("SimpleAkkaDemo", ConfigFactory.load())

    val senderActor: ActorRef = actorSystem.actorOf(Props(SechdulerActor), "sechdulerActor")

    import actorSystem.dispatcher
    import scala.concurrent.duration._

    actorSystem.scheduler.schedule(0 seconds, 1 seconds) {
      senderActor ! "timer"
    }
  }
}


[!NOTE]

  1. 需要导入隐式转换import scala.concurrent.duration._才能调用0 seconds方法
  2. 需要导入隐式参数import actorSystem.dispatcher才能启动定时任务

实现两个进程之间的通信

案例介绍

基于Akka实现在两个进程间发送、接收消息。Worker启动后去连接Master,并发送消息,Master接收到消息后,再回复Worker消息。

在这里插入图片描述



1. Worker实现

步骤

  1. 创建一个Maven模块,导入依赖和配置文件
  2. 创建启动WorkerActor
  3. 发送"setup"消息给WorkerActor,WorkerActor接收打印消息
  4. 启动测试
akka.actor.provider = "akka.remote.RemoteActorRefProvider"
akka.remote.netty.tcp.hostname = "127.0.0.1"
akka.remote.netty.tcp.port = "9999"

参考代码

Worker.scala

val workerActorSystem = ActorSystem("actorSystem", ConfigFactory.load())
val workerActor: ActorRef = workerActorSystem.actorOf(Props(WorkerActor), "WorkerActor")

// 发送消息给WorkerActor
workerActor ! "setup"

WorkerActor.scala

object WorkerActor extends Actor{
  override def receive: Receive = {
    case "setup" =>
      println("WorkerActor:启动Worker")
  }
}

2. Master实现

步骤

  1. 创建Maven模块,导入依赖和配置文件
  2. 创建启动MasterActor
  3. WorkerActor发送"connect"消息给MasterActor
  4. MasterActor回复"success"消息给WorkerActor
  5. WorkerActor接收并打印接收到的消息
  6. 启动Master、Worker测试
akka.actor.provider = "akka.remote.RemoteActorRefProvider"
akka.remote.netty.tcp.hostname = "127.0.0.1"
akka.remote.netty.tcp.port = "8888"

参考代码

Master.scala

val masterActorSystem = ActorSystem("MasterActorSystem", ConfigFactory.load())
val masterActor: ActorRef = masterActorSystem.actorOf(Props(MasterActor), "MasterActor")

MasterActor.scala

object MasterActor extends Actor{
  override def receive: Receive = {
    case "connect" =>
      println("2. Worker连接到Master")
      sender ! "success"
  }
}

WorkerActor.scala

object WorkerActor extends Actor{
  override def receive: Receive = {
    case "setup" =>
      println("1. 启动Worker...")
      val masterActor = context.actorSelection("akka.tcp://MasterActorSystem@127.0.0.1:9999/user/MasterActor")

      // 发送connect
      masterActor ! "connect"
    case "success" =>
      println("3. 连接Master成功...")
  }
}

简易版spark通信框架案例

案例介绍

模拟Spark的Master与Worker通信

  • 一个Master
    • 管理Worker
  • 若干个Worker(Worker可以按需添加)
    • 注册
    • 发送心跳

在这里插入图片描述

实现思路

  1. 构建Master、Worker阶段
    • 构建Master ActorSystem、Actor
    • 构建Worker ActorSystem、Actor
  2. Worker注册阶段
    • Worker进程向Master注册(将自己的ID、CPU核数、内存大小(M)发送给Master)
  3. Worker定时发送心跳阶段
    • Worker定期向Master发送心跳消息
  4. Master定时心跳检测阶段
    • Master定期检查Worker心跳,将一些超时的Worker移除,并对Worker按照内存进行倒序排序
  5. 多个Worker测试阶段
    • 启动多个Worker,查看是否能够注册成功,并停止某个Worker查看是否能够正确移除

1. 工程搭建

项目使用Maven搭建工程


步骤

  1. 分别搭建几下几个项目
工程名说明
spark-demo-common存放公共的消息、实体类
spark-demo-masterAkka Master节点
spark-demo-workerAkka Worker节点
  1. 导入依赖(资料包中的pom.xml)
    • master/worker添加common依赖
  2. 导入配置文件(资料包中的application.conf)
    • 修改Master的端口为7000
    • 修改Worker的端口为7100

2. 构建Master和Worker

分别构建Master和Worker,并启动测试


步骤

  1. 创建并加载Master Actor
  2. 创建并加载Worker Actor
  3. 测试是否能够启动成功

参考代码

Master.scala

val sparkMasterActorSystem = ActorSystem("sparkMaster", ConfigFactory.load())
val masterActor = sparkMasterActorSystem.actorOf(Props(MasterActor), "masterActor")

MasterActor.scala

object MasterActor extends Actor{
  override def receive: Receive = {
    case x => println(x)
  }
}

Worker.scala

val sparkWorkerActorSystem = ActorSystem("sparkWorker", ConfigFactory.load())
sparkWorkerActorSystem.actorOf(Props(WorkerActor), "workerActor")

WorkerActor.scala

object WorkerActor extends Actor{
  override def receive: Receive = {
    case x => println(x)
  }
}

3. Worker注册阶段实现

在Worker启动时,发送注册消息给Master


步骤

  1. Worker向Master发送注册消息(workerid、cpu核数、内存大小)
    • 随机生成CPU核(1、2、3、4、6、8)
    • 随机生成内存大小(512、1024、2048、4096)(单位M)
  2. Master保存Worker信息,并给Worker回复注册成功消息
  3. 启动测试

参考代码

MasterActor.scala

object MasterActor extends Actor{

  private val regWorkerMap = collection.mutable.Map[String, WorkerInfo]()

  override def receive: Receive = {
    case WorkerRegisterMessage(workerId, cpu, mem) => {
      println(s"1. 注册新的Worker - ${workerId}/${cpu}核/${mem/1024.0}G")
      regWorkerMap += workerId -> WorkerInfo(workerId, cpu, mem, new Date().getTime)
      sender ! RegisterSuccessMessage
    }
  }
}

WorkerInfo.scala

/**
  * 工作节点信息
  * @param workerId workerid
  * @param cpu CPU核数
  * @param mem 内存多少
  * @param lastHeartBeatTime 最后心跳更新时间
  */
case class WorkerInfo(workerId:String, cpu:Int, mem:Int, lastHeartBeatTime:Long)

MessagePackage.scala

/**
  * 注册消息
  * @param workerId
  * @param cpu CPU核数
  * @param mem 内存大小
  */
case class WorkerRegisterMessage(workerId:String, cpu:Int, mem:Int)

/**
  * 注册成功消息
  */
case object RegisterSuccessMessage

WorkerActor.scala

object WorkerActor extends Actor{

  private var masterActor:ActorSelection = _
  private val CPU_LIST = List(1, 2, 4, 6, 8)
  private val MEM_LIST = List(512, 1024, 2048, 4096)

  override def preStart(): Unit = {
    masterActor = context.system.actorSelection("akka.tcp://sparkMaster@127.0.0.1:7000/user/masterActor")

    val random = new Random()
    val workerId = UUID.randomUUID().toString.hashCode.toString
    val cpu = CPU_LIST(random.nextInt(CPU_LIST.length))
    val mem = MEM_LIST(random.nextInt(MEM_LIST.length))

    masterActor ! WorkerRegisterMessage(workerId, cpu, mem)
  }

  ...
}

4. Worker定时发送心跳阶段

Worker接收到Master返回注册成功后,发送心跳消息。而Master收到Worker发送的心跳消息后,需要更新对应Worker的最后心跳时间。


步骤

  1. 编写工具类读取心跳发送时间间隔
  2. 创建心跳消息
  3. Worker接收到注册成功后,定时发送心跳消息
  4. Master收到心跳消息,更新Worker最后心跳时间
  5. 启动测试

参考代码

ConfigUtil.scala

object ConfigUtil {
  private val config: Config = ConfigFactory.load()

  val `worker.heartbeat.interval` = config.getInt("worker.heartbeat.interval")
}

MessagePackage.scala

package com.itheima.spark.common

...

/**
  * Worker心跳消息
  * @param workerId
  * @param cpu CPU核数
  * @param mem 内存大小
  */
case class WorkerHeartBeatMessage(workerId:String, cpu:Int, mem:Int)

WorkerActor.scala

object WorkerActor extends Actor{
  ...

  override def receive: Receive = {
    case RegisterSuccessMessage => {
      println("2. 成功注册到Master")

      import scala.concurrent.duration._
      import context.dispatcher

      context.system.scheduler.schedule(0 seconds,
        ConfigUtil.`worker.heartbeat.interval` seconds){
        // 发送心跳消息
        masterActor ! WorkerHeartBeatMessage(workerId, cpu, mem)
      }
    }
  }
}

MasterActor.scala

object MasterActor extends Actor{
  ...

  override def receive: Receive = {
	...
    case WorkerHeartBeatMessage(workerId, cpu, mem) => {
      println("3. 接收到心跳消息, 更新最后心跳时间")
      regWorkerMap += workerId -> WorkerInfo(workerId, cpu, mem, new Date().getTime)
    }
  }
}

5. Master定时心跳检测阶段

如果某个worker超过一段时间没有发送心跳,Master需要将该worker从当前的Worker集合中移除。可以通过Akka的定时任务,来实现心跳超时检查。


步骤

  1. 编写工具类,读取检查心跳间隔时间间隔、超时时间
  2. 定时检查心跳,过滤出来大于超时时间的Worker
  3. 移除超时的Worker
  4. 对现有Worker按照内存进行降序排序,打印可用Worker

参考代码

ConfigUtil.scala

object ConfigUtil {
  private val config: Config = ConfigFactory.load()

  // 心跳检查时间间隔
  val `master.heartbeat.check.interval` = config.getInt("master.heartbeat.check.interval")
  // 心跳超时时间
  val `master.heartbeat.check.timeout` = config.getInt("master.heartbeat.check.timeout")
}

MasterActor.scala

  override def preStart(): Unit = {
    import scala.concurrent.duration._
    import context.dispatcher

    context.system.scheduler.schedule(0 seconds,
      ConfigUtil.`master.heartbeat.check.interval` seconds) {
      // 过滤出来超时的worker
      val timeoutWorkerList = regWorkerMap.filter {
        kv =>
          if (new Date().getTime - kv._2.lastHeartBeatTime > ConfigUtil.`master.heartbeat.check.timeout` * 1000) {
            true
          }
          else {
            false
          }
      }

      if (!timeoutWorkerList.isEmpty) {
        regWorkerMap --= timeoutWorkerList.map(_._1)
        println("移除超时的worker:")
        timeoutWorkerList.map(_._2).foreach {
          println(_)
        }
      }

      if (!regWorkerMap.isEmpty) {
        val sortedWorkerList = regWorkerMap.map(_._2).toList.sortBy(_.mem).reverse
        println("可用的Worker列表:")
        sortedWorkerList.foreach {
          var rank = 1
          workerInfo =>
            println(s"<${rank}> ${workerInfo.workerId}/${workerInfo.mem}/${workerInfo.cpu}")
            rank = rank + 1
        }
      }
    }
  }
  ...
}

6. 多个Worker测试阶段

修改配置文件,启动多个worker进行测试。


步骤

  1. 测试启动新的Worker是否能够注册成功
  2. 停止Worker,测试是否能够从现有列表删除
内容简介 本书将尝试帮助入门级、中级以及高级读者理解基本的分布式计算概念,并且展示 如何使用 Akka 来构建具备高容错性、可以横向扩展的分布式网络应用程序。Akka 是一 个强大的工具集,提供了很多选项,可以对在本地机器上处理或网络远程机器上处理的 某项工作进行抽象封装,使之对开发者不可见。本书将介绍各种概念,帮助读者理解 网络上各系统进行交互的困难之处,并介绍如何使用 Akka 提供的解决方案来解决这些 问题。 作者简介 Jason Goodwin 是一个基本上通过自学成才的开发者。他颇具企业家精神,在学校 学习商学。不过他从 15 岁起就开始学习编程,并且一直对技术保持着浓厚的兴趣。这对 他的职业生涯产生了重要的影响,从商学转向了软件开发。现在他主要从事大规模分布 式系统的开发。在业余时间,他喜欢自己原创电子音乐。 他在 mDialog 公司第一次接触到 Akka 项目。mDialog 是一家使用 Scala/Akka 的公司, 为主流出版商提供视频广告插入软件。这家公司最终被 Google 收购。他同时还是一名很 有影响力的“技术控”,将 Akka 引入加拿大一家主要的电信公司,帮助该公司为客户提 供容错性更高、响应更及时的软件。除此之 外,他还为该公司中的一些团队教授 Akka、 函数式以及并发编程等知识。 目录 第 1 章 初识 Actor:Akka 工具集以及 Actor 模型的介绍。 第 2 章 Actor 与并发:响应式编程。Actor 与 Future 的使用。 第 3 章 传递消息:消息传递模式。 第 4 章 Actor 的生命周期—处理状态与错误:Actor 生命周期、监督机制、Stash/ Unstash、Become/Unbecome 以及有限自动机。 第 5 章 纵向扩展:并发编程、Router Group/Pool、Dispatcher、阻塞 I/O 的处理以 及 API。 第 6 章 横向扩展—集群化:集群、CAP 理论以及 Akka Cluster。 第 7 章 处理邮箱问题:加大邮箱负载、不同邮箱的选择、熔断机制。 第 8 章 测试与设计:行为说明、领域驱动设计以及 Akka Testkit。 第 9 章 尾声:其他 Akka 特性。下一步需要学习的知识。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值