以下内容来自文档:https://doc.akka.io/docs/akka/2.5/guide/tutorial_4.html
Actor的最佳划分粒度是使用Akka的猿们需要重点设计的,不能太粗(Actor太少)也不能太细(Actor太多),通常会有多种可行方案,但是我们要找到最好的。
下面的几条准则可以帮我们设计出最好的Actor层级结构:
- 总的来说,粗粒度更好一些,引入不必要的更多更小的Actor引起的问题会比它们解决的问题要多
- 当系统存在以下需求的时候使用细粒度Actor:
- 高并发
- 具有许多状态的Actor之间存在复杂的交互。 我们将在下一章中看到一个很好的例子。
- Actor的状态很多,细分成更多的Actor才有意义
- 当一个Actor承担多个不相关的职责时。 将Actor细分可以使得它在失败并恢复的时候不影响其他Actor
官网给出了一个例子,假设我们要接收一些设备发送的温度数据,每个设备有自己的唯一ID(device id),同时分了若干个组,每个设备属于一个组,所以系统收到的温度数据中也会有组ID(group id),actor层级结构设计如下:
我们可以列出以下主要功能点:
- DevideManager会收到所有带有group id和device id的消息,它要对每条消息:
- 判断此group id是否有对应的Actor,有的话把消息发给它
- 没有的话创建新Actor
- DeviceGroup与DeviceManager做的事情类型,只是它使用device id判断它的下一级Actor
- Device收到消息
下面这两个类分别是注册请求和注册成功的确认消息:
public static final class RequestTrackDevice {
public final String groupId;
public final String deviceId;
public RequestTrackDevice(String groupId, String deviceId) {
this.groupId = groupId;
this.deviceId = deviceId;
}
}
public static final class DeviceRegistered {
}
下面贴出代码,遵循从底层往高层的顺序,先Device,然后是DeviceGroup,然后是DeviceManager
Device
接收三类消息:
- 注册。返回注册成功
- 记录温度。将温度数据写入本身状态
lastTemperatureReading
- 查询温度。返回
lastTemperatureReading
import akka.actor.AbstractActor;
import akka.actor.Props;
import akka.event.Logging;
import akka.event.LoggingAdapter;
import jdocs.tutorial_4.DeviceManager.DeviceRegistered;
import jdocs.tutorial_4.DeviceManager.RequestTrackDevice;
import java.util.Optional;
public class Device extends AbstractActor {
private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
final String groupId;
final String deviceId;
public Device(String groupId, String deviceId) {
this.groupId = groupId;
this.deviceId = deviceId;
}
public static Props props(String groupId, String deviceId) {
return Props.create(Device.class, groupId, deviceId);
}
public static final class RecordTemperature {
final long requestId;
final double value;
public RecordTemperature(long requestId, double value) {
this.requestId = requestId;
this.value = value;
}
}
public static final class TemperatureRecorded {
final long requestId;
public TemperatureRecorded(long requestId) {
this.requestId = requestId;
}
}
public static final class ReadTemperature {
final long requestId;
public ReadTemperature(long requestId) {
this.requestId = requestId;
}
}
public static final class RespondTemperature {
final long requestId;
final Optional<Double> value;
public RespondTemperature(long requestId, Optional<Double> value) {
this.requestId = requestId;
this.value = value;
}
}
Optional<Double> lastTemperatureReading = Optional.empty();
@Override
public void preStart() {
log.info("Device actor {}-{} started", groupId, deviceId);
}
@Override
public void postStop() {
log.info("Device actor {}-{} stopped", groupId, deviceId);
}
@Override
public Receive createReceive() {
return receiveBuilder()
.match(RequestTrackDevice.class, r -> {
if (this.groupId.equals(r.groupId) && this.deviceId.equals(r.deviceId)) {
getSender().tell(new DeviceRegistered(), getSelf());
} else {
log.warning(
"Ignoring TrackDevice request for {}-{}.This actor is responsible for {}-{}.",
r.groupId, r.deviceId, this.groupId, this.deviceId
);
}
})
.match(RecordTemperature.class, r -> {
log.info("Recorded temperature reading {} with {}", r.value, r.requestId);
lastTemperatureReading = Optional.of(r.value);
getSender().tell(new TemperatureRecorded(r.requestId), getSelf());
})
.match(ReadTemperature.class, r -> {
getSender().tell(new RespondTemperature(r.requestId, lastTemperatureReading), getSelf());
})
.build();
}
}
Device测试类
@Test
public void testReplyToRegistrationRequests() {
TestKit probe = new TestKit(system);
ActorRef deviceActor = system.actorOf(Device.props("group", "device"));
deviceActor.tell(new DeviceManager.RequestTrackDevice("group", "device"), probe.getRef());
probe.expectMsgClass(DeviceManager.DeviceRegistered.class);
assertEquals(deviceActor, probe.getLastSender());
}
@Test
public void testIgnoreWrongRegistrationRequests() {
TestKit probe = new TestKit(system);
ActorRef deviceActor = system.actorOf(Device.props("group", "device"));
deviceActor.tell(new DeviceManager.RequestTrackDevice("wrongGroup", "device"), probe.getRef());
probe.expectNoMessage();
deviceActor.tell(new DeviceManager.RequestTrackDevice("group", "wrongDevice"), probe.getRef());
probe.expectNoMessage();
}
DeviceGroup
- 处理注册请求,要么发送给已有的Actor,要么创建一个新的Actor
- 跟踪已有的Actor,把停止了的移除
DeviceGroup内部维护了一个Map<String, ActorRef>,key是device id,用于根据设备ID判断相应的DeviceActor是否存在了并获取其引用
注意这个类里给DeviceActor发消息时没有使用tell
,而是使用的forward
,它们的区别是啥呢?tell有两个参数,第二个参数可以指定发送者,而forward只有一个参数,就是消息本身,不管经历几次中专,它会保持最初的发送者
public class DeviceGroup extends AbstractActor {
private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
final String groupId;
public DeviceGroup(String groupId) {
this.groupId = groupId;
}
public static Props props(String groupId) {
return Props.create(DeviceGroup.class, groupId);
}
final Map<String, ActorRef> deviceIdToActor = new HashMap<>();
@Override
public void preStart() {
log.info("DeviceGroup {} started", groupId);
}
@Override
public void postStop() {
log.info("DeviceGroup {} stopped", groupId);
}
private void onTrackDevice(DeviceManager.RequestTrackDevice trackMsg) {
if (this.groupId.equals(trackMsg.groupId)) {
ActorRef deviceActor = deviceIdToActor.get(trackMsg.deviceId);
if (deviceActor != null) {
deviceActor.forward(trackMsg, getContext());
} else {
log.info("Creating device actor for {}", trackMsg.deviceId);
deviceActor = getContext().actorOf(Device.props(groupId, trackMsg.deviceId), "device-" + trackMsg.deviceId);
deviceIdToActor.put(trackMsg.deviceId, deviceActor);
deviceActor.forward(trackMsg, getContext());
}
} else {
log.warning(
"Ignoring TrackDevice request for {}. This actor is responsible for {}.",
groupId, this.groupId
);
}
}
@Override
public Receive createReceive() {
return receiveBuilder()
.match(DeviceManager.RequestTrackDevice.class, this::onTrackDevice)
.build();
}
}
到目前为止,我们已经实现了在group中注册device的功能。还记得在group中维护的Map<String,ActorRef>吗,我们总不能只增不减吧。假设现在只要终端设备停止了,它对应的Actor就会停止,那问题来了,group怎么知道它下面的某个子Actor停止了呢?
Akka提供了一个叫Death Watch
(死亡凝视哈哈哈哈哈)的功能,允许Actor监视watch
(请把这个词和监管supervision
区分开)其他Actor,在被监视的Actor停止时,监视者会收到通知Terminated(actorRef)
,里面包含着被监视者的引用。和监管不同,监视不仅限于在父子Actor之间使用。所以ActorGroup要增加如下功能:
- watch每一个创建的DeviceActor
- 当收到子Actor停止的消息时,把它从Map中移除
现在还有个问题是,group收到的通知里面只有子Actor的引用,而我们删除子Actor是需要它的设备ID,因为Map的key是设备ID,所以没办法了,再加一个反过来的Map吧:Map<ActorRef, String>
下面是改过之后的类,重点关注对Terminated.class
类消息的处理:
public class DeviceGroup extends AbstractActor {
private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
final String groupId;
public DeviceGroup(String groupId) {
this.groupId = groupId;
}
public static Props props(String groupId) {
return Props.create(DeviceGroup.class, groupId);
}
final Map<String, ActorRef> deviceIdToActor = new HashMap<>();
final Map<ActorRef, String> actorToDeviceId = new HashMap<>();
@Override
public void preStart() {
log.info("DeviceGroup {} started", groupId);
}
@Override
public void postStop() {
log.info("DeviceGroup {} stopped", groupId);
}
private void onTrackDevice(DeviceManager.RequestTrackDevice trackMsg) {
if (this.groupId.equals(trackMsg.groupId)) {
ActorRef deviceActor = deviceIdToActor.get(trackMsg.deviceId);
if (deviceActor != null) {
deviceActor.forward(trackMsg, getContext());
} else {
log.info("Creating device actor for {}", trackMsg.deviceId);
deviceActor = getContext().actorOf(Device.props(groupId, trackMsg.deviceId), "device-" + trackMsg.deviceId);
getContext().watch(deviceActor);
actorToDeviceId.put(deviceActor, trackMsg.deviceId);
deviceIdToActor.put(trackMsg.deviceId, deviceActor);
deviceActor.forward(trackMsg, getContext());
}
} else {
log.warning(
"Ignoring TrackDevice request for {}. This actor is responsible for {}.",
groupId, this.groupId
);
}
}
private void onTerminated(Terminated t) {
ActorRef deviceActor = t.getActor();
String deviceId = actorToDeviceId.get(deviceActor);
log.info("Device actor for {} has been terminated", deviceId);
actorToDeviceId.remove(deviceActor);
deviceIdToActor.remove(deviceId);
}
@Override
public Receive createReceive() {
return receiveBuilder()
.match(DeviceManager.RequestTrackDevice.class, this::onTrackDevice)
.match(Terminated.class, this::onTerminated)
.build();
}
}
DeviceManager
跟DeviceGroup很类似的
public class DeviceManager extends AbstractActor {
private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
public static Props props() {
return Props.create(DeviceManager.class);
}
public static final class RequestTrackDevice {
public final String groupId;
public final String deviceId;
public RequestTrackDevice(String groupId, String deviceId) {
this.groupId = groupId;
this.deviceId = deviceId;
}
}
public static final class DeviceRegistered {
}
final Map<String, ActorRef> groupIdToActor = new HashMap<>();
final Map<ActorRef, String> actorToGroupId = new HashMap<>();
@Override
public void preStart() {
log.info("DeviceManager started");
}
@Override
public void postStop() {
log.info("DeviceManager stopped");
}
private void onTrackDevice(RequestTrackDevice trackMsg) {
String groupId = trackMsg.groupId;
ActorRef ref = groupIdToActor.get(groupId);
if (ref != null) {
ref.forward(trackMsg, getContext());
} else {
log.info("Creating device group actor for {}", groupId);
ActorRef groupActor = getContext().actorOf(DeviceGroup.props(groupId), "group-" + groupId);
getContext().watch(groupActor);
groupActor.forward(trackMsg, getContext());
groupIdToActor.put(groupId, groupActor);
actorToGroupId.put(groupActor, groupId);
}
}
private void onTerminated(Terminated t) {
ActorRef groupActor = t.getActor();
String groupId = actorToGroupId.get(groupActor);
log.info("Device group actor for {} has been terminated", groupId);
actorToGroupId.remove(groupActor);
groupIdToActor.remove(groupId);
}
public Receive createReceive() {
return receiveBuilder()
.match(RequestTrackDevice.class, this::onTrackDevice)
.match(Terminated.class, this::onTerminated)
.build();
}
}
在下一章中,我们将介绍组查询功能,它将建立一个新的Actor交互模式:分散 - 聚集。 强调:它有个强大的功能,允许用户查询属于组的所有设备的状态!