AKKA官方文档阅读笔记(3)JAVA版2.5.16

本文探讨了在Akka框架中,如何使用Actor模型处理复杂查询,特别是动态设备组的温度信息收集。通过创建专门的查询Actor,实现了对设备Actor状态的有效跟踪和管理,解决了设备动态增删带来的挑战。

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

以下内容来自官方文档:https://doc.akka.io/docs/akka/2.5/guide/tutorial_5.html

可能遇到的场景

到目前为止,例子中Actor之间的对话模式都是比较简单的,这个文档里会有个稍微复杂点儿的例子
接着上个文档的例子,现在有DeviceManager,DeviceGroup,Device三种Actor,我们的目标是查询一个group中所有device的温度信息。
第一个问题就是,device是动态的,随时可能新增或停止,我们当然可以在查询一开始的时候就约定,我要查这一瞬间存活的device的信息,但是这两会有两个问题:

  • 假如一个device在查询过程中停止了,就不会给你响应了
  • 加如一个device在查询过程中新增进来,结果中也不会包含它的信息,因为在查询开始的瞬间没有它

可以这样解决这两个问题:

  • 当group收到一个新查询请求时,建立一个当前存活的device的快照,只向这些actor请求数据
  • 之后新增的actor数据将被此次请求忽略
  • 如果快照中的actor在查询期间停止而没有应答,我们将向查询消息的发送者报告这个消息

除了device actor会动态地来来去去之外,一些actor虽然存活,但可能长时间都不应答。 例如,它们可能会陷入意外的死循环中,或者因为代码bug消息处理失败了,或者消息丢了。 我们不希望查询无限期地等待,因此我们将在以下任一情况下认为此次查群请求完成了:

  • 快照中的所有actor都已响应或已确认停止。
  • 达到了指定的超时时间。

鉴于这些设定,以及快照中的device可能刚刚启动,还没有接收到温度数据,自然就没有数据可以返回。我们可以针对温度查询为每个设备actor定义四种状态:

  • 它启动着并有温度数据: Temperature
  • 它启动着,但还没有温度:TemperatureNotAvailable
  • 它已停止:DeviceNotAvailable
  • 它没有在超时前回复:DeviceTimedOut

让我们把下面地代码添加到DeviceGroup类中:

// 查询温度请求
public static final class RequestAllTemperatures {
  final long requestId;

  public RequestAllTemperatures(long requestId) {
    this.requestId = requestId;
  }
}
// 查询温度请求响应
public static final class RespondAllTemperatures {
  final long requestId;
  final Map<String, TemperatureReading> temperatures;

  public RespondAllTemperatures(long requestId, Map<String, TemperatureReading> temperatures) {
    this.requestId = requestId;
    this.temperatures = temperatures;
  }
}

// device返回信息接口
public static interface TemperatureReading {
}
// 返回温度
public static final class Temperature implements TemperatureReading {
  public final double value;

  public Temperature(double value) {
    this.value = value;
  }
}
// 返回暂无数据
public static final class TemperatureNotAvailable implements TemperatureReading {
}
// 返回device已停止
public static final class DeviceNotAvailable implements TemperatureReading {
}
// 返回查询超时
public static final class DeviceTimedOut implements TemperatureReading {
}

执行查询

接下来该干啥呢?一种做法是把对查询请求地处理放在DeviceGroup类中,但是这样子会很麻烦而且容易出错,违背了单一职责原则。所以我们采取更简单更好的方案,我们将创建一个代表单个查询的actor,代替group actor执行完成查询所需的任务。 到目前为止,我们创建的actor都是典型的域对象,但现在,我们将创建一个代表进程或任务而不是实体的actor。 这样,保持我们的device group功能简单,并且能够更好地单独测试查询功能。

定义查询Actor
首先,我们需要设计查询actor的生命周期。 这包括确定其初始状态,初始化操作,结束清理工作(如有必要)。 查询actor将需要以下信息:

  • 要查询的device actor的快照和ID。
  • RequestId(以便我们可以将其包含在回复中)。
  • 查询发起者的actor的引用。 我们会直接将结果发送给这个actor。
  • 超时时间,指明查询应等待回复的时间。 这个参数将简化测试。

查询超时处理
隆重推出调度器:scheduler

  • 我们从ActorSystem获得调度器,它也可以从actor的上下文访问:getContext().getSystem().scheduler()。 参数中有一个ExecutionContext,它是执行计时器任务的线程池。 在我们的例子中,我们通过传入getContext().dispatcher()来使用与actor相同的调度程序。
  • scheduler.scheduleOnce(time,actorRef,message,executor,sender)方法将在指定时间内将指定消息发送给指定ActorRef。

我们需要一个表示查询超时的消息。 为此,我们创建了一个简单的消息CollectionTimeout,没有任何参数。 scheduleOnce的返回值是一个可取消的对象(Cancellable),如果查询在指定时间内完成,可用于取消计时器。 在查询开始时,我们需要向device依次询问当前温度。 假如device在返回ReadTemperature前就停止了,我们需要收到通知,就要观察(watch)每个actor。 这样,actor停止时我们会收到Terminated消息,而不需要等到超时才能将这些actor标记为不可用。
把这些内容整合起来,我们的DeviceGroupQueryactor的大致框架如下所示:

public class DeviceGroupQuery extends AbstractActor {
  public static final class CollectionTimeout {
  }

  private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);

  // Actor快照 
  final Map<ActorRef, String> actorToDeviceId;
  final long requestId;
  // 请求消息的发送者
  final ActorRef requester;
  // 超时定时器的关闭句柄
  Cancellable queryTimeoutTimer;

  public DeviceGroupQuery(Map<ActorRef, String> actorToDeviceId, long requestId, ActorRef requester, FiniteDuration timeout) {
    this.actorToDeviceId = actorToDeviceId;
    this.requestId = requestId;
    this.requester = requester;

    queryTimeoutTimer = getContext().getSystem().scheduler().scheduleOnce(
            timeout, getSelf(), new CollectionTimeout(), getContext().dispatcher(), getSelf()
    );
  }

  public static Props props(Map<ActorRef, String> actorToDeviceId, long requestId, ActorRef requester, FiniteDuration timeout) {
    return Props.create(DeviceGroupQuery.class, actorToDeviceId, requestId, requester, timeout);
  }

  // 初始化工作:向每一个actor发出温度读取请求消息,并watch
  @Override
  public void preStart() {
    for (ActorRef deviceActor : actorToDeviceId.keySet()) {
      getContext().watch(deviceActor);
      deviceActor.tell(new Device.ReadTemperature(0L), getSelf());
    }
  }

  // 清理工作:关闭定时器
  @Override
  public void postStop() {
    queryTimeoutTimer.cancel();
  }

}

跟踪Actor状态
除了挂起的计时器之外,查询actor还有一个有状态切面,就是需要跟踪已经回复,已经停止或没有回复的actor集合。 跟踪此状态的一种方法是在actor中创建可变字段。我们使用另一种方法,利用actor可以修改消息响应方法的能力。 Receive只是一个可以从另一个函数返回的函数(或者一个对象,例如python中把函数也当作对象)。 默认情况下,receive代码块定义actor处理消息的行为,但可以在actor的生命周期内多次更改它。 我们调用context.become(newBehavior),其中newBehavior的类型为Receive。 我们将利用此功能来跟踪actor的状态。
对于我们的用例:

  • 我们不是直接定义receive,而是委托waitingForReplies函数来创建Receive。
  • waitingForReplies函数将跟踪两个不断变化的值:
    • 已经收到的回复
    • 仍在等待的一组actor

我们有三个事件要处理:

  • 正常返回温度数据的RespondTemperature消息。
  • actor的终止的Terminated消息。
  • 达到超时时间,定时器发送的CollectionTimeout

在前两种情况下,我们需要跟踪回复(维护前面提到的两个集合),我们把这个功能委托给一个方法receivedResponse,细节在后面讨论。 在超时的情况下,只需要将DeviceTimedOut作为所有仍未恢复的Actor的响应。 然后我们把已经收到响应的结果集发送给查询的发起者并停止查询actor。
将以下内容添加到DeviceGroupQuery源文件中:

@Override
public Receive createReceive() {
  return waitingForReplies(new HashMap<>(), actorToDeviceId.keySet());
}

// 构建Receive需要两个参数,也就是需要维护的两个集合,只要每次改变集合后重新通过此方法获得Receive
// 并通过context.become改变自身的消息处理行为,就实现了状态的跟踪
public Receive waitingForReplies(
        Map<String, DeviceGroup.TemperatureReading> repliesSoFar,
        Set<ActorRef> stillWaiting) {
  return receiveBuilder()
          .match(Device.RespondTemperature.class, r -> {
            ActorRef deviceActor = getSender();
            DeviceGroup.TemperatureReading reading = r.value
                    .map(v -> (DeviceGroup.TemperatureReading) new DeviceGroup.Temperature(v))
                    .orElse(new DeviceGroup.TemperatureNotAvailable());
            receivedResponse(deviceActor, reading, stillWaiting, repliesSoFar);
          })
          .match(Terminated.class, t -> {
            receivedResponse(t.getActor(), new DeviceGroup.DeviceNotAvailable(), stillWaiting, repliesSoFar);
          })
          .match(CollectionTimeout.class, t -> {
            Map<String, DeviceGroup.TemperatureReading> replies = new HashMap<>(repliesSoFar);
            for (ActorRef deviceActor : stillWaiting) {
              String deviceId = actorToDeviceId.get(deviceActor);
              replies.put(deviceId, new DeviceGroup.DeviceTimedOut());
            }
            // 将最终结果返回给消息请求者
            requester.tell(new DeviceGroup.RespondAllTemperatures(requestId, replies), getSelf());
            // 记得要关闭自身
            getContext().stop(getSelf());
          })
          .build();
}

如果看到这里,你还是不太清楚到底是怎么维护repliesSoFar结果集和stillWaiting结果集的话,你只需要知道:

  • repliesSoFar表示已经得到明确答复的响应集合
  • stillWaiting表示仍没有回复消息的device集合
  • waitingForReplies方法本身不处理消息,它返回消息的处理器,或者说,它返回用于处理消息的function
  • 处理消息的function依赖于repliesSoFar和stillWaiting,我们要做的就是在必要的时候,修改这两个集合,传入waitingForReplies以获得消息处理程序(Receive),并使用context.become(Receive)来告诉当前actor,用使用最新集合的消息处理程序来处理消息

现在,来弄清楚receivedResponse要做什么。 首先,我们需要在repliesSoFar中记录得到的响应,并从stillWaiting中删除actor。 下一步是检查stillWaiting是不是空了。 如果空了,表示所有的device都返回消息了,我们将查询结果发送给原始请求者并停止查询actor。 否则,我们需要使用更新后的repliesSoFar和stillWaiting获得新Receive并等待更多消息。 在之前的代码中,我们将Terminated作为隐式响应DeviceNotAvailable,因此receivedResponse不需要做任何特殊的事情。 但是,仍有一个小细节我们要考虑到,我们可能会从设备actor收到期望的响应,但随后他有可能就停止了。 我们不希望第二个事件覆盖已经收到的回复。 换句话说,我们不希望在记录响应后收到Terminated。 通过调用context.unwatch(ref)可以很容易地实现。 多次调用也是安全的,只有第一次调用才会有效,其余的都会被忽略。 有了这些知识,我们可以创建receivedResponse方法:

public void receivedResponse(ActorRef deviceActor,
                             DeviceGroup.TemperatureReading reading,
                             Set<ActorRef> stillWaiting,
                             Map<String, DeviceGroup.TemperatureReading> repliesSoFar) {
  getContext().unwatch(deviceActor);
  String deviceId = actorToDeviceId.get(deviceActor);

  Set<ActorRef> newStillWaiting = new HashSet<>(stillWaiting);
  newStillWaiting.remove(deviceActor);

  Map<String, DeviceGroup.TemperatureReading> newRepliesSoFar = new HashMap<>(repliesSoFar);
  newRepliesSoFar.put(deviceId, reading);
  if (newStillWaiting.isEmpty()) {
    requester.tell(new DeviceGroup.RespondAllTemperatures(requestId, newRepliesSoFar), getSelf());
    getContext().stop(getSelf());
  } else {
    getContext().become(waitingForReplies(newRepliesSoFar, newStillWaiting));
  }
}

到了这一刻,你可能很自然地想问,我们使用context.become()而不是使repliesSoFar和stillWaiting成为actor的可变字段?这样做获得了什么, 是的,在这个简单的例子中,好处并不明显。 但是当你突然有更多种类的状态时,这种状态保持的价值变得更加明显。 由于每个状态可能具有相关的临时数据,因此将这些数据作为字段会污染全局状态,即不清楚在什么状态下使用哪些字段。 使用参数化的“工厂”方法,我们可以保持仅与状态相关的数据私有。建议你熟悉我们在此处使用的解决方案,因为它有助于以更清晰,更易于维护的方式构建更复杂的actor代码。
完整的查询Actor代码如下:

public class DeviceGroupQuery extends AbstractActor {
  public static final class CollectionTimeout {
  }

  private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);

  final Map<ActorRef, String> actorToDeviceId;
  final long requestId;
  final ActorRef requester;

  Cancellable queryTimeoutTimer;

  public DeviceGroupQuery(Map<ActorRef, String> actorToDeviceId, long requestId, ActorRef requester, FiniteDuration timeout) {
    this.actorToDeviceId = actorToDeviceId;
    this.requestId = requestId;
    this.requester = requester;

    queryTimeoutTimer = getContext().getSystem().scheduler().scheduleOnce(
            timeout, getSelf(), new CollectionTimeout(), getContext().dispatcher(), getSelf()
    );
  }

  public static Props props(Map<ActorRef, String> actorToDeviceId, long requestId, ActorRef requester, FiniteDuration timeout) {
    return Props.create(DeviceGroupQuery.class, actorToDeviceId, requestId, requester, timeout);
  }

  @Override
  public void preStart() {
    for (ActorRef deviceActor : actorToDeviceId.keySet()) {
      getContext().watch(deviceActor);
      deviceActor.tell(new Device.ReadTemperature(0L), getSelf());
    }
  }

  @Override
  public void postStop() {
    queryTimeoutTimer.cancel();
  }

  @Override
  public Receive createReceive() {
    return waitingForReplies(new HashMap<>(), actorToDeviceId.keySet());
  }

  public Receive waitingForReplies(
          Map<String, DeviceGroup.TemperatureReading> repliesSoFar,
          Set<ActorRef> stillWaiting) {
    return receiveBuilder()
            .match(Device.RespondTemperature.class, r -> {
              ActorRef deviceActor = getSender();
              DeviceGroup.TemperatureReading reading = r.value
                      .map(v -> (DeviceGroup.TemperatureReading) new DeviceGroup.Temperature(v))
                      .orElse(new DeviceGroup.TemperatureNotAvailable());
              receivedResponse(deviceActor, reading, stillWaiting, repliesSoFar);
            })
            .match(Terminated.class, t -> {
              receivedResponse(t.getActor(), new DeviceGroup.DeviceNotAvailable(), stillWaiting, repliesSoFar);
            })
            .match(CollectionTimeout.class, t -> {
              Map<String, DeviceGroup.TemperatureReading> replies = new HashMap<>(repliesSoFar);
              for (ActorRef deviceActor : stillWaiting) {
                String deviceId = actorToDeviceId.get(deviceActor);
                replies.put(deviceId, new DeviceGroup.DeviceTimedOut());
              }
              requester.tell(new DeviceGroup.RespondAllTemperatures(requestId, replies), getSelf());
              getContext().stop(getSelf());
            })
            .build();
  }

  public void receivedResponse(ActorRef deviceActor,
                               DeviceGroup.TemperatureReading reading,
                               Set<ActorRef> stillWaiting,
                               Map<String, DeviceGroup.TemperatureReading> repliesSoFar) {
    getContext().unwatch(deviceActor);
    String deviceId = actorToDeviceId.get(deviceActor);

    Set<ActorRef> newStillWaiting = new HashSet<>(stillWaiting);
    newStillWaiting.remove(deviceActor);

    Map<String, DeviceGroup.TemperatureReading> newRepliesSoFar = new HashMap<>(repliesSoFar);
    newRepliesSoFar.put(deviceId, reading);
    if (newStillWaiting.isEmpty()) {
      requester.tell(new DeviceGroup.RespondAllTemperatures(requestId, newRepliesSoFar), getSelf());
      getContext().stop(getSelf());
    } else {
      getContext().become(waitingForReplies(newRepliesSoFar, newStillWaiting));
    }
  }
}

接下来是很长一段的测试代码,感兴趣的看下原文。

内容简介 本书将尝试帮助入门级、中级以及高级读者理解基本的分布式计算概念,并且展示 如何使用 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、付费专栏及课程。

余额充值