准备工作:
- 下载官方HelloWorld Demo:
https://developer.lightbend.com/start/?group=akka&project=akka-quickstart-java - 点击页面上的 CREATE A PROJECT FOR ME!
- 到本地通过Maven导入,可以成功执行com.lightbend.akka.sample.AkkaQuickstart.main
以下内容来自文档:https://doc.akka.io/docs/akka/2.5/guide/tutorial_1.html
Actor层级结构
其实在你用代码创建Actor之前,Akka自己就已经创建三个actor了,它们都是负责监管自己下面的actor的:
/
这个就是传说中的跟监管者,是所有actor的祖先,当系统终止时,它一定是最后一个被停止的/user
这个是所有咱们用代码创建的Actor的祖先/system
不晓得这是做啥的,非用户创建的actor的祖先吧
想搞清楚actor的层级结构,最好就是自己创建一些actor,并打印出它们的引用,你可以在包com.lightbend.akka.sample
中增加下面这个类:
package com.lightbend.akka.sample;
import akka.actor.AbstractActor;
import akka.actor.AbstractActor.Receive;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.Props;
class PrintMyActorRefActor extends AbstractActor {
@Override
public Receive createReceive() {
return receiveBuilder()
.matchEquals("printit", p -> {
ActorRef secondRef = getContext().actorOf(Props.empty(), "second-actor");
System.out.println("Second: " + secondRef);
})
.build();
}
}
public class ActorHierarchyExperiments {
public static void main(String[] args) throws java.io.IOException {
ActorSystem system = ActorSystem.create("testSystem");
ActorRef firstRef = system.actorOf(Props.create(PrintMyActorRefActor.class), "first-actor");
System.out.println("First: " + firstRef);
firstRef.tell("printit", ActorRef.noSender());
System.out.println(">>> Press ENTER to exit <<<");
try {
System.in.read();
} finally {
system.terminate();
}
}
}
执行main方法可以看到输出的内容是:
First: Actor[akka://testSystem/user/first-actor#1053618476]
Second: Actor[akka://testSystem/user/first-actor/second-actor#-1544706041]
其中testSystem是ActorSystem
的名称,其后的user表示这是用户(就是咱们用代码)创建的Actor,后面就是类似linux文件夹一样的表示方式了,很容易理解。#后面的一串儿数值是Actor的唯一标识,大部分时候不用管
Actor的生命周期
actor的生命是从我们创建它开始,当一个Actor停止时,会先递归地停止它的孩子Actor,这种做法简化了资源释放并避免资源泄露。
推荐在actor内部通过getContext().stop(getSelf())
写法来中止它自己,一般是在actor完成它地任务之后收到一个约定好的停止消息,作为对这个消息地响应,停止它自己。也可以通过getContext().stop(actorRef)
停止其他地actor,但是这种做法不推荐使用,最好改为给相应的actor发送停止消息
Akka的actor API有很多生命周期相关的钩子你可以去重写,最常用的就是preStart()
和preStop()
了,看名字也很容易理解,preStart()方法一定会在开始处理消息之前被执行。
Actor错误处理
在父Actor和子Actor的整个生命周期中,它们都是紧密相关的,任何时候一个Actor出错了(就是receive方法中抛出了错误),它就会被临时挂起,然后这个错误会传到父级那里(具体怎么传这里没说,其实也是Actor类里的一个方法),父级Actor会决定怎么处理子Actor抛出的这个错误,可以看出,每个Actor负责监管它的子级,默认的监管策略
是停止并重启子Actor
以下内容来自文档:https://doc.akka.io/docs/akka/2.5/guide/tutorial_3.html
给远程Actor发送消息
发消息给远程actor和本地的actor是类似的,但是我们要记住:
- 两者的延迟会有较为明显的差别,因为你要考虑带宽和消息的大小
- 发送远程消息包含更多的步骤也更容易出错,要考虑可靠性
- 发送本地消息不会对消息做限制,反之,远程消息会限制message size
对于消息发送,Akka有以下两个特性:
- 消息交付:最多发送一次(消息不会重复发送,但不保证交付)
- 消息顺序:对于一对发送者和接收者,消息是有序的
消息交付
消息传递的分类往往是以下三类:
最多一次 At-most-once delivery:
每条消息发送接收方会收到0次或者1次。就是有可能收不到但绝不会重复。至少一次 At-least-once delivery:
每条消息会被潜在地多次发送,至少有一条发送成功。有可能收到重复消息但绝不会丢消息。准确一次 Exactly-once delivery:
每条消息不多不少发送一次
第一种就是Akka采用地,它是性能最高的,开销最小,因为消息即发即弃( fire-and-forget),发送端接收端都不用维护消息的状态。相应的第二种,发送端要维护消息的状态,接收端要有确认机制。第三种是最昂贵的,发送端和接收端都要维护消息状态。
在一个actor系统中, 我们要确定交付的含义 — 在哪一个时间点,系统认为消息发送完成了:
-
- 当消息发送到网络中?
-
- 当消息被目标Actor所在的主机接收时?
-
- 当消息投放到目标Actor所在的邮箱?
-
- 当目标Actor开始处理这条消息?
-
- 当目标Actor处理消息完成?
大部分框架和协议宣称在类似4或5的时候,听起来挺合理的,但是这些框架和协议提供的保证真的能达到4或5的级别吗?举个简单又实际的例子: 用户提交一个订单,我们希望返回此订单被处理并成功写入数据库磁盘中了。
如果我们依赖于成功处理消息,则只要订单已提交给相应API,处理它并将其放入数据库,actor就会报告成功。 不幸的是,在调用API之后,可能会发生以下任何一种情况:
- 主机崩了
- 反序列化失败了
- 数据校验失败了
- 数据库崩了
- 代码出BUG了
这表明交付保证不会转化为域级保证。 我们只想在订单实际完全处理和保留后报告成功。 可以报告成功的唯一实体是应用程序本身(也就是消费者实际处理消息的代码),因为只有它知道怎样才算处理成功。 没有通用框架可以确定特定域的细节以及在该域中被认为是成功的。
在这个特定的例子中,我们只希望在成功的数据库写入后发出成功信号,数据库确认订单现在已安全存储。 由于这些原因,Akka将保证的责任提升到应用程序本身,即您必须自己实现它们。 这使您可以完全控制要提供的保证。 现在,让我们考虑Akka提供的消息排序,以便于推理应用程序逻辑。
消息排序
在Akka中,对于给定的一对Actor(发送者和接收者),直接从第一个发送到第二个的消息不会乱序接收。 强调:这种保证仅适用于消息直接发送,而不是使用中间者(中介)
如果:
- Actor A1 给A2发送了消息: M1, M2, M3
- Actor A3 也给A2发送了消息: M4, M5, M6
这意味着:
M1一定比M2, M3先到
M2一定比M3先到
M4一定比M5, M6先到
M5一定比M6先到
A2从A1和A3处收到的消息可能会是夹杂着间隔着的
因为是不可靠交付,任何消息都有可能丢失, 也就是到达不了A2