目录:
一、 并发编程模型:
Akka 的介绍:
1.1、 Actor 模型用于解决什么问题
1.2、 Akka 中 Actor 模型详解
对上图的详解如下:
1.3、Actor 模型工作机制说明
Actor模型工作机制说明(对照工作机制示意图理解):
Actor 间传递消息机制(对照工作机制示意图理解):
Actor 模型应用实例
Actor 自我通讯
构建maven项目pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.akka</groupId>
<artifactId>SayHelloActor</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 定义一下常量 -->
<properties>
<encoding>UTF-8</encoding>
<scala.version>2.11.8</scala.version>
<scala.compat.version>2.11</scala.compat.version>
<akka.version>2.4.17</akka.version>
</properties>
<dependencies>
<!-- 添加scala的依赖 -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
</dependency>
<!-- 添加akka的actor依赖 -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_${scala.compat.version}</artifactId>
<version>${akka.version}</version>
</dependency>
<!-- 多进程之间的Actor通信 -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-remote_${scala.compat.version}</artifactId>
<version>${akka.version}</version>
</dependency>
</dependencies>
<!-- 指定插件-->
<build>
<!-- 指定源码包和测试包的位置 -->
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/test/scala</testSourceDirectory>
<plugins>
<!-- 指定编译scala的插件 -->
<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>
<!-- maven打包的插件 -->
<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>
<!-- 指定main方法 -->
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>xxx</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
代码实现:
package com.test.akka.actor
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
// 1. 当我们继承 Actor 后,就是一个 Actor,需要重写该 Actor 的核心方法 receive
class SayHelloActor extends Actor {
// 循环的接收消息
// 1. receive方法,会被该 Actor 的 MailBox(实现了 Runnable 接口)调用
// 2. 当该 Actor 的 MailBox 接收到消息,就会调用 receive 方法
// 3. Receive 的底层:type Receive = PartialFunction[Any, Unit]
override def receive: Receive = {
// 接受消息并处理,如果接收到 exit,就退出
case "hello" => println("发送:hello\t\t回应:hello too:)")
case "ok" => println("发送:ok\t\t\t回应:ok too:)")
case "exit" => {
println("接收到exit~指令,退出系统...")
context.stop(self) // 停止自己的 ActorRef
context.system.terminate() // 关闭 ActorSystem
}
}
}
object SayHelloActor {
// 1. 先创建一个 ActorSystem,专门用于创建 Actor
private val actoryFactory = ActorSystem("actoryFactory")
// 2. 创建一个 Actor 的同时,返回 Actor 的 ActorRef
private val sayHelloActorRef: ActorRef = actoryFactory.actorOf(Props[SayHelloActor], "sayHelloActor")
// (1) Props[SayHelloActor] 创建了一个 SayHelloActor 实例,这里使用到了反射
// (2) "sayHelloActor" 是 Actor 的名字
// (3) sayHelloActorRef: ActorRef =>是 Props[SayHelloActor] 的引用
// (4) 创建的 SayHelloActor 实例被 ActorSystme 接管
def main(args: Array[String]): Unit = {
// 给 SayHelloActor 发消息(邮箱)
sayHelloActorRef ! "hello"
sayHelloActorRef ! "ok"
sayHelloActorRef ! "ok~"
// 研究异步如何退出 ActorSystem
sayHelloActorRef ! "exit"
}
}
输出结果如下:
小结:
当程序执行 private val sayHelloActorRef: ActorRef = actoryFactory.actorOf(Props[SayHelloActor], “sayHelloActor”) 会完成如下任务:
1、actorFactory 是 ActorSystem(“actorFactory”) 创建的。
2、这里的 Props[SayHelloActor] 会使用反射机制,创建一个 SayHelloActor 对象,如果是 actorFactory.actorOf(Props(new SayHelloActor(其他代理对象的引用)), “sayHelloActor”) 形式,就是使用 new 的方式创建一个 SayHelloActor 对象。注意:Props() 是小括号。
3、会创建一个 SayHelloActor 对象的代理对象 sayHelloActorRef,使用 sayHelloActorRef 才能发送消息。
4、会在底层创建 Dispather Message,是一个线程池,用于分发消息,消息是发送到对应的 Actor 的 MailBox。
5、会在底层创建 SayHelloActor 的 MailBox 对象,该对象是一个队列,可接收 Dispatcher Message 发送的消息。
6、MailBox 实现了 Runnable 接口,是一个线程,一直运行并调用 Actor 的 receive 方法,因此当Dispather 发送消息到 MailBox 时,Actor 在r eceive 方法就可以得到信息。
7、SayHelloActorRef ! “hello” ,表示把 hello 消息发送到 SayHello Actor 的 Mailbox (通过Dispatcher Message 转发)。
Actor 之间通讯:
两个 Actor 的通讯机制原理图
代码实现:
AActor.scala
package com.test.akka.actors
import akka.actor.{Actor, ActorRef}
class AActor(bActorRef: ActorRef) extends Actor {
var count = 0
override def receive: Receive = {
case "start" => {
println("AActor 出招了,start ok")
bActorRef ! "我打"
}
case "我打" => {
count += 1
// 给 BActor 发出消息
// 这里需要持有 BActor 的引用(BActorRef)才可以
println(s"AActor(黄飞鸿) 厉害!看我佛山无影脚 第${count}脚")
Thread.sleep(1000)
bActorRef ! "我打" // 给 BActor 发出消息
}
}
}
BActor.scala
package com.test.akka.actors
import akka.actor.Actor
class BActor extends Actor {
var count = 0
override def receive: Receive = {
case "我打" => {
count += 1
println(s"BActor(乔峰) 挺猛 看我降龙十八掌 第${count}掌")
Thread.sleep(1000)
// 通过 sender() 方法,可以获取到发送消息的 Actor 的 ActorRef
sender() ! "我打"
}
}
}
ActorApp.scala
package com.test.akka.actors
import akka.actor.{ActorRef, ActorSystem, Props}
// 100招后,就退出
object ActorApp extends App {
// 创建 ActorSystem
val actorfactory = ActorSystem("actorfactory")
// 先创建 BActor 的引用/代理
val bActorRef: ActorRef = actorfactory.actorOf(Props[BActor], "bActor")
// 创建 AActor 的引用时需要持有 BActor 的引用
val aActorRef: ActorRef = actorfactory.actorOf(Props(new AActor(bActorRef)), "aActor")
// aActor 先出招
aActorRef ! "start"
}
代码的小结:
1、两个 Actor 通讯机制和 Actor 自身发消息机制基本一样,只是要注意如下:
2、如果 A Actor 在需要给 B Actor 发消息,则需要持有 B Actor 的 ActorRef,可以通过创建 A Actor 时,传入 B Actor 的代理对象(ActorRef)。
3、当 B Actor 在 receive 方法中接收到消息,需要回复时,可以通过 sender() 获取到发送 Actor 的代理对象。
如何理解 Actor 的 receive 方法被调用?
1、每个 Actor 对应 MailBox。
2、MailBox 实现了 Runnable 接口,处于运行的状态。
3、当有消息到达 MailBox,就会去调用 Actor 的 receive 方法,即将消息推送给 receive 方法。
二、Akka 网络编程:
Akka 网络编程基本介绍:
2.1、Akka 网络编程-小黄鸡客服案例
需求分析
1、服务端进行监听(9999)
2、客户端可以通过键盘输入,发送咨询问题给小黄鸡客服(服务端)
3、小黄鸡(服务端)回答客户的问题
示例代码如下:
YellowChickenServer.scala
package com.test.akka.yellowchicken.server
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import com.atguigu.akka.yellowchicken.common.{ClientMessage, ServerMessage}
import com.typesafe.config.ConfigFactory
class YellowChickenServer extends Actor {
override def receive: Receive = {
case "start" => println("start 小黄鸡客服开始工作了...")
// 如果接收到了服务端的发来的消息,即 ClientMessage
case ClientMessage(mes) => {
println("客户咨询的问题是:" + mes)
mes match {
// 使用 match case 匹配(模糊匹配)
case "价格" => sender() ! ServerMessage("20000 RMB")
case "地址" => sender() ! ServerMessage("北京市")
case "技术" => sender() ! ServerMessage("大数据")
case _ => sender() ! ServerMessage("你说的啥子:)")
}
}
}
}
// 主程序入口
object YellowChickenServerApp extends App {
val host = "127.0.0.1" // 服务端ip地址
val port = 9999 // 端口
// 创建 config 对象,指定协议类型、监听的ip和端口
val config = ConfigFactory.parseString(
s"""
|akka.actor.provider="akka.remote.RemoteActorRefProvider"
|akka.remote.netty.tcp.hostname=$host
|akka.remote.netty.tcp.port=$port
""".stripMargin)
// 创建 ActorSystem
val serverActorSystem = ActorSystem("Server", config)
// 创建 YellowChickenServer 的 Actor 和 ActorRef
val yellowChickenServerActorRef: ActorRef = serverActorSystem.actorOf(Props[YellowChickenServer], "YellowChickenServer-01")
// 启动服务端
yellowChickenServerActorRef ! "start"
}
CustomerActor.scala
package com.test.akka.yellowchicken.client
import akka.actor.{Actor, ActorRef, ActorSelection, ActorSystem, Props}
import com.test.akka.yellowchicken.common.{ClientMessage, ServerMessage}
import com.typesafe.config.ConfigFactory
import scala.io.StdIn
class CustomerActor(serverHost: String, serverPort: Int) extends Actor {
// 定义一个 YellowChickenServerRef
var serverActorRef: ActorSelection = _
// 在 Actor 中有一个方法 preStart 方法,它会在 Actor 运行前执行
// 在 Akka 开发中,通常将初始化的工作,放在 preStart 方法中
override def preStart(): Unit = {
this.serverActorRef = context.actorSelection(s"akka.tcp://Server@${serverHost}:${serverPort}/user/YellowChickenServer-01")
println("this.serverActorRefer=" + this.serverActorRef)
}
override def receive: Receive = {
case "start" => println("start 客户端运行,可以咨询问题")
case mes: String => {
// 发给服务端
// serverActorRef ! mes // 不应该发送字符串,应该包装一把,应该发送一个(样例)对象(即协议)
serverActorRef ! ClientMessage(mes) // 此时发送的是一个对象,该样例类默认实现了序列化 和 apply 方法
}
// 如果接受到了服务器端的消息
case ServerMessage(mes) => {
println(s"收到小黄鸡客服(Server)消息:$mes")
}
}
}
// 主程序入口
object CustomerActorApp extends App {
val (host, port, serverHost, serverPort) = ("127.0.0.1", 9990, "127.0.0.1", 9999)
val config = ConfigFactory.parseString(
s"""
|akka.actor.provider="akka.remote.RemoteActorRefProvider"
|akka.remote.netty.tcp.hostname=$host
|akka.remote.netty.tcp.port=$port
""".stripMargin)
// 创建 ActorSystem
val clientActorSystem = ActorSystem("Client", config)
// 创建 CustomerActor 的 Actor 和 ActorRef
val clientActorRef: ActorRef = clientActorSystem.actorOf(Props(new CustomerActor(serverHost, serverPort)), "CustomerActor-01")
// 启动客户端
clientActorRef ! "start"
// 客户端发送消息
while (true) {
val mes = StdIn.readLine()
clientActorRef ! mes
}
}
MessageProtocol.scala
package com.test.akka.yellowchicken.common
// 使用样例类来构建协议
// 1、客户端发送服务端的协议(序列化对象)
case class ClientMessage(mes: String) // 回顾:样例类的构造器中的每一个参数都默认为 val ,即只可读。
// 2、服务器端发送给客户端的协议
case class ServerMessage(mes: String)
2.2、Akka 网络编程-Spark Master Worker 进程通讯项目
项目意义:
1、深入理解 Spark 的 Master 和 Worker 的通讯机制。
2、为了方便同学们看 Spark 的底层源码,命名的方式和源码保持一致(如:通讯消息类命名就是一样的)。
3、加深对主从服务心跳检测机制(HeartBeat)的理解,方便以后 spark 源码二次开发。
项目需求分析:
2.2.1、实现功能 1-Worker 完成注册
功能要求: Worker 注册到 Master,Master 完成注册,并回复 Worker 注册成功。
示例代码如下:
MasterActor.scala
package com.test.akka.sparkmasterworker.master
import akka.actor.{Actor, ActorSystem, Props}
import com.test.akka.sparkmasterworker.common.{RegisterWorkerInfo, RegisteredWorkerInfo, WorkerInfo}
import com.typesafe.config.ConfigFactory
import scala.collection.mutable
class MasterActor extends Actor {
// 定义一个 mutable.HashMap 属性,用于管理 Worker
val workers = mutable.HashMap[String, WorkerInfo]()
override def receive: Receive = {
case "start" => println("Master服务器启动了...")
// 接收到 Worker 客户端注册的信息,保存进 HashMap
case RegisterWorkerInfo(id, cpu, ram) => {
if (!workers.contains(id)) {
// 创建 WorkerInfo
val workerInfo = new WorkerInfo(id, cpu, ram)
// 加入到 HashMap
workers += (id -> workerInfo)
println("服务器的Workers= " + workers)
// 回复客户端注册成功
sender() ! RegisteredWorkerInfo
}
}
}
}
object MasterActorApp {
def main(args: Array[String]): Unit = {
val host = "127.0.0.1" // 服务端ip地址
val port = 10005 // 端口
// 创建 config 对象,指定协议类型、监听的ip和端口
val config = ConfigFactory.parseString(
s"""
|akka.actor.provider="akka.remote.RemoteActorRefProvider"
|akka.remote.netty.tcp.hostname=$host
|akka.remote.netty.tcp.port=$port
""".stripMargin)
// 先创建 ActorSystem
val masterActorSystem = ActorSystem("Master", config)
// 再创建 Master 的 Actor 和 ActorRef
val masterActorRef = masterActorSystem.actorOf(Props[MasterActor], "MasterActor-01")
// 启动 Master
masterActorRef ! "start"
}
}
WorkerActor.scala
package com.test.akka.sparkmasterworker.worker
import akka.actor.{Actor, ActorRef, ActorSelection, ActorSystem, Props}
import com.atguigu.akka.sparkmasterworker.common.{RegisterWorkerInfo, RegisteredWorkerInfo}
import com.typesafe.config.ConfigFactory
class WorkerActor(serverHost: String, serverPort: Int) extends Actor {
// 定义一个 MasterActorRef
var masterActorProxy: ActorSelection = _
// 定义 Worker 的编号
var id = java.util.UUID.randomUUID().toString
// 在 Actor 中有一个方法 preStart 方法,它会在 Actor 运行前执行
// 在 Akka 开发中,通常将初始化的工作,放在 preStart 方法中
override def preStart(): Unit = {
this.masterActorProxy = context.actorSelection(s"akka.tcp://Master@${serverHost}:${serverPort}/user/MasterActor-01")
println("this.masterActorProxy=" + this.masterActorProxy)
}
override def receive = {
case "start" => {
println("Worker客户端启动运行")
// 给服务器发送一个注册信息
masterActorProxy ! RegisterWorkerInfo(id, 16, 16 * 1024)
}
case RegisteredWorkerInfo => {
println("WorkedId= " + id + " 注册成功!")
}
}
}
object WorkerActorApp {
def main(args: Array[String]): Unit = {
val (host, port, serverHost, serverPort) = ("127.0.0.1", 10001, "127.0.0.1", 10005)
val config = ConfigFactory.parseString(
s"""
|akka.actor.provider="akka.remote.RemoteActorRefProvider"
|akka.remote.netty.tcp.hostname=$host
|akka.remote.netty.tcp.port=$port
""".stripMargin)
// 创建 ActorSystem
val workerActorSystem = ActorSystem("Worker", config)
// 创建 WorkerActor 的 Actor 和 ActorRef
val workerActorRef: ActorRef = workerActorSystem.actorOf(Props(new WorkerActor(serverHost, serverPort)), "WorkerActor-01")
// 启动客户端
workerActorRef ! "start"
}
}
MessageProtocol.scala
package com.test.akka.sparkmasterworker.common
// 使用样例类来构建协议
// Worker 注册信息
case class RegisterWorkerInfo(id: String, cpu: Int, ram: Int)
// 这个是 WorkerInfo,是保存在 Master 的 HashMap 中的,该 HashMap 用于管理 Worker
// 将来这个 WorkerInfo 会扩展,比如 增加 Worker 上一次的心跳时间
class WorkerInfo(val id: String, val cpu: Int, val ram: Int)
// 当 Worker 注册成功,服务器返回一个 RegisteredWorkerInfo 对象
case object RegisteredWorkerInfo
2.2.2、实现功能 2-Worker 定时发送心跳
功能要求:Worker 定时发送心跳给 Master,Master 能够接收到,并更新 Worker 上一次心跳时间。
示例代码如下:
MessageProtocol.scala 中增加代码
package com.test.akka.sparkmasterworker.common
// 使用样例类来构建协议
// Worker 注册信息
case class RegisterWorkerInfo(id: String, cpu: Int, ram: Int)
// 这个是 WorkerInfo,是保存在 Master 的 HashMap 中的,该 HashMap 用于管理 Worker
// 将来这个 WorkerInfo 会扩展,比如 增加 Worker 上一次的心跳时间
class WorkerInfo(val id: String, val cpu: Int, val ram: Int) {
// 新增属性:心跳时间
var lastHeartBeatTime: Long = _
}
// 当 Worker 注册成功,服务器返回一个 RegisteredWorkerInfo 对象
case object RegisteredWorkerInfo
// 每隔一定时间定时器发送给 Master 一个心跳
case class HeartBeat(id: String)
// Worker 每隔一定时间定时器发送给 自己 一个消息
case object SendHeartBeat
MasterActor.scala 中增加代码
case HeartBeat(id) => {
// 更新对应的 Worker 的心跳时间
// 1、先从 Worker 中取出 WorkerInfo
val workerInfo = workers(id)
workerInfo.lastHeartBeatTime = System.currentTimeMillis()
println("Master更新了 " + id + " 的心跳时间 ")
}
WorkerActor.scala 中增加代码
// 当客户端注册成功后,就定义一个定时器,每隔一定时间,发送 SendHeartBeat 给自己
import context.dispatcher
context.system.scheduler.schedule(0 millis, 3000 millis, self, SendHeartBeat)
case SendHeartBeat => {
println("WorkedId= " + id + " 给Master发送心跳")
masterActorProxy ! HeartBeat(id)
}
2.2.3、实现功能 3-Master 启动定时任务,定时检测注册的 Worker
功能要求:Master 启动定时任务,定时检测注册的 Worker 有哪些没有更新心跳,已经超时的 Worker,将其从 HashMap 中删除掉。
示例代码如下:
MessageProtocol.scala 中增加代码
// Master 给自己发送一个触发检查超时 Worker 的信息
case object StartTimeOutWorker
// Master 给自己发消息,检测 Worker,对于心跳超时的
case object RemoveTimeOutWorker
MasterActor.scala 中增加代码
case "start" => {
println("Master服务器启动了...")
// Master 启动定时任务,定时检测注册的 Worker 有哪些没有更新心跳,已经超时的 Worker,将其从 HashMap 中删除掉。
self ! StartTimeOutWorker
}
// 开启定时器,每隔一定时间检测是否有 Worker 的心跳超时
case StartTimeOutWorker => {
println("开启了定时检测Worker心跳的任务")
import context.dispatcher // 使用调度器时候必须导入dispatcher
context.system.scheduler.schedule(0 millis, 9000 millis, self, RemoveTimeOutWorker)
}
// 判断哪些 Worker 心跳超时(nowTime - lastHeartBeatTime),对已经超时的 Worker,将其从 HashMap 中删除掉。
case RemoveTimeOutWorker => {
// 首先获取所有 Workers 的所有 WorkerInfo
val workerInfos = workers.values
val nowTime = System.currentTimeMillis()
// 过滤出所有超时的 workerInfo 并删除即可
workerInfos.filter(workerInfo => (nowTime - workerInfo.lastHeartBeatTime) > 6000)
.foreach(workerInfo => workers.remove(workerInfo.id))
println("当前有 " + workers.size + " 个Worker存活")
}
2.2.4、实现功能 4-Master,Worker 的启动参数运行时指定
功能要求:Master,Worker 的启动参数运行时指定,而不是固定写在程序中的。
MasterActor.scala 中修改代码
if (args.length != 3) {
println("请输入参数 host port MasterActor的名字")
sys.exit()
}
val host = args(0) // 服务端ip地址
val port = args(1) // 端口
val masterName = args(2) // MasterActor的名字
......
// 再创建 Master 的 Actor 和 ActorRef
val masterActorRef = masterActorSystem.actorOf(Props[MasterActor], s"${masterName}")
WorkerActor.scala 中增修改代码
if (args != 6) {
println("请输入参数 host port WorkerActor的名字 serverHost serverPort MasterActor的名字")
}
val host = args(0)
val port = args(1)
val workerName = args(2)
val serverHost = args(3)
val serverPort = args(4)
val masterName = args(5)
......
// 创建 WorkerActor 的 Actor 和 ActorRef
val workerActorRef: ActorRef = workerActorSystem.actorOf(Props(new WorkerActor(serverHost, serverPort.toInt, masterName)), s"${workerName}")