在Akka中,Actor也经常会处在一个无法处理某些消息的状态。例如数据库客户端离线了,那么在重新上线之前,它都无法处理任何消息。我们可以选择不断重新建立客户端的连接,直到连接成功为止。在这种情况下,在成功连接之前,客户端会丢弃收到的所有消息。另一种做法是把客户端无法处理的消息先放在一旁,等到客户端恢复连接状态之后再做处理。
Akka提供了一种叫做stash的机制,用于对于Akka中消息状态的来回切换:
- stash:将消息暂存到一个独立的队列中,该队列存储目前无法处理的消息:
- unstash:把消息从暂存队列中取出,放回到邮箱队列中,actor就能继续处理这些消息
要注意的是,虽然 stash()和 unstash()在希望快速改变状态的时候使用起来非常方便,但是 stash 消息的状态一定要和某个时间限制进行绑定,否则就有可能填满邮箱(MailBox),可以在actor的构造函数或preStart方法中调度执行这个消息避免邮箱填满问题:
System.scheduler().scheduleOnce(
Duration.create(1000,TimeUnit.MILLISECONDS),self(),CheckConnected,system.dispacher(),null);
条件语句
最直观的方法就是将状态存储在 Actor 中,然后使用一个条件语句来决定 Actor 应该执行的操作。
public PartialFunction receive() {
return RecieveBuilder
.match(GetRequest.class, x -> {
if (online) {
processMessage(x);
} else {
stash();
}
})
.match(Tcp.Connected.class, x -> {
online = true;
unstash();
)
.match(Disconnected.class, x -> online = false).build();
}
}
使用了 stash/unstash,所以一旦 Actor 在线,就会处理 stash 的所有消息。
很多时候, Actor 会存储状态, 然后根据这个状态值的不同会有不同的行为。 使用条件语句是一种非常过程化的用于处理行为和状态的方法。
热交换(Hotswap):Become/Unbecome
- become(PartialFunction behavior):这个方法将 receive 块中定义的行为修改为一个新的 PartialFunction。
- unbecome():这个方法将 Actor 的行为修改回默认行为。
private PartialFunction<Object, BoxedUnit> disconnected;
private PartialFunction<Object, BoxedUnit> online;
public HotswapClientActor(String dbPath) {
remoteDb = context().actorSelection(dbPath);
/*
* 在接收到 Connected 消息之前,这些消息都会被暂存起来放在一边。
*/
disconnected = ReceiveBuilder.
match(Request.class, x -> { //can't handle until we know remote system is responding
remoteDb.tell(new Connected(), self()); //see if the remote actor is up
stash(); // 缓存消息
}).
match(Connected.class, x -> { // Okay to start processing messages.
context().become(online);
unstash(); //获取消息
}).build();
/*
* 一旦接收到Connected 消息,Actor就调用become,将状态修改为在线(定义在 online 方法中) 。
* 此时, Actor还会调用unstash将所有暂存的消息取回到工作队列中。 这样就可以使用online方法中定义的行为来处理所有的消息了。
*/
online = ReceiveBuilder.
match(Request.class, x -> {
remoteDb.forward(x, context()); //forward instead of tell to preserve sender
}).
build();
/**
* 可以定义任意数量的 receive 块,并相互切换
*/
receive(disconnected); //初始化状态
}
相较于条件语句,每个状态的行为都定义在自己独立的PartialFunction 中。
Actor 一开始处于离线状态, 这时候它无法对 GetRequest 做出响应, 所以把消息 stash 起来。在接收到 Connected 消息之前,这些消息都会被暂存起来放在一边。一旦接收到Connected 消息,Actor就调用 become,将状态修改为在线。此时, Actor还会调用unstash 将所有暂存的消息取回到工作队列中。 这样就可以使用online状态方法中定义的行为来处理所有的消息了。 如果Actor接收到了一个Disconnected消息, 就调用unbecome,把Actor行为恢复为默认设置。
有限自动机(Finite State Machine, FSM)
更多关于FSM的请参考这两篇文章: www.jianshu.com/p/41905206b… www.jianshu.com/p/c5b0559f4…
FSM 中也有状态以及基于状态的行为变化。跟热交换比起来,FSM 是一个更重量级的抽象概念,需要更多的代码和类型才能够实现并运行。所以通常来说,热交换是一个更简单、可读性更高的选择。
FSM 中的类型有两个参数:状态和容器。
定义状态
- Disconnected:离线,队列中没有任何消息;
- Disconnected and Pending:离线,队列中包含消息;
- Connected:在线,队列中没有消息;
- Connected and Pending:在线,队列包含消息
enum State{
DISCONNECTED,
CONNECTED,
CONNECTED_AND_PENDING,
}
状态容器
状态容器就是存储消息的地方。FSM 允许我们定义状态容器,并且在切换状态时修改状态容器。
public class EventQueue extends LinkedList<Request> {}
-
继承akka.actor.AbstractFSM<S, D>
public class BunchingAkkademyClient extends AbstractFSM<State, RequestQueue>{ {//init block } }
-
初始化块中定义行为
{ startWith(DISCONNECTED, null); //init block }
-
定义不同状态对不同消息的响应方法以及如何根据接收到的消息切换状态。
/* * 定义不同状态对不同消息的响应方法以及如何根据接收到的消息切换状态。 * when(S state, PartialFunction pf) * * matchEvent创建一个[[akka.japi.pf.FSMStateFunctionBuilder]]和第一个case语句集。匹配事件、数据类型和谓词的case语句。 * */ when(DISCONNECTED, // Disconnected 状态会保存消息或是转移到 Connected 状态。它会忽略除了 Connected 和 GetRequest 以外的所有消息。 matchEvent(FlushMsg.class, (msg, container) -> stay()) .event(Request.class, (msg, container) -> { remoteDb.tell(new Connected(), self()); container.add(msg); return stay(); }) .event(Connected.class, (msg, container) -> { if (container.size() == 0) { return goTo(CONNECTED); } else { return goTo(CONNECTED_AND_PENDING); } })); when(CONNECTED, //Connected 状态只关心那些会将状态转移到 ConnectedAnd Pending 状态的消息。 matchEvent(FlushMsg.class, (msg, container) -> stay()) .event(Request.class, (msg, container) -> { container.add(msg); return goTo(CONNECTED_AND_PENDING); })); when(CONNECTED_AND_PENDING, // ConnectedAndPending 状态可以把容器中的所有请求都发送出去, 也可以向容器中再添加一个请求。 matchEvent(FlushMsg.class, (msg, container) -> { remoteDb.tell(container, self()); container = new EventQueue(); return goTo(CONNECTED); }) .event(Request.class, (msg, container) -> { container.add(msg); return goTo(CONNECTED_AND_PENDING); }));
在某些状态中忽略了一些消息,而在另一些状态中则会处理这些消息。Actor 必须要返回对状态的描述,要么是停留在 FSM 中的某个状态,要么是转移到了另一个状态。
-
initialize()
在这个代码块中要做的最后一件事就是调用 initialize();
参考文献
- 《Akka入门与实践》
关注公众号 数据工匠记
,专注于大数据领域离线、实时技术干货定期分享!回复Akka
领取《Akka入门与实践》书籍!个人网站 www.lllpan.top