Akka具有分布式、集群、微服务等特点,可以快速构建高可用、高性能的分布式应用。多个Actor之间消息传递,可以使用tell、ask、forward等简单方式,但是当一组Actor需要进行有规律的消息传递时,就显得稍微复杂。Akka路由组件,就是为了解决我们复杂的消息传递,例如广播、轮询、随机等,有两种实现方式:配置和代码创建。
什么是路由
路由就是一组消息按照指定规则被分配到各个地方。Akka中使用路由,需要依赖两个对象Router和Routee。Router表示路由器,消息会进入该路由器然后转发出去,相当于消息中转站。Routee表示路由目标,最终消息会被分配到这里。上面所说的指定规则就是路由策略,表明我们应该怎样分配这些消息。
路由使用
使用路由组件,我们需要先创建路由目标Actor,然后将它们包装成Routee对象(表明这些Actor是路由目标),之后创建Router对象,按照路由策略将消息转发给路由目标Actor。如下:
创建路由目标Actor:
/**
* @author php
* @date 2018/11/18
* 路由目标(用于接受路由器转发的消息)
*/
public class FirstRoutee extends AbstractActor {
@Override
public Receive createReceive() {
return receiveBuilder().matchAny(o -> {
//打印当前actor信息,便于我们分析路由规则
System.out.println(getSelf() + "-->" + o);
}).build();
}
}
创建路由器Actor:
public class FirstRouter extends AbstractActor {
private Router router;
public static void main(String[] args) {
ActorSystem system = ActorSystem.create("system");
ActorRef router = system.actorOf(Props.create(FirstRouter.class), "router");
router.tell("MessageOne",ActorRef.noSender());
router.tell("MessageTwo",ActorRef.noSender());
router.tell("MessageThree",ActorRef.noSender());
}
/**
* 生命周期方法
* 当该Actor启动时,先调用该方法,
* 我们在这里初始化Routee列表
*
* @throws Exception exception
*/
@Override
public void preStart() throws Exception {
List<Routee> routeeList = new ArrayList<>();
for (int i = 0; i < 2; i++) {
ActorRef ref = getContext().actorOf(Props.create(FirstRoutee.class), "routee" + i);
//监控子actor
getContext().watch(ref);
//创建路由目标,并添加到集合中去
routeeList.add(new ActorRefRoutee(ref));
}
//创建轮询路由器RoundRobinRoutingLogic
router = new Router(new RoundRobinRoutingLogic(), routeeList);
}
@Override
public Receive createReceive() {
return receiveBuilder().match(Terminated.class, t -> {
//当子Actor停止,从路由列表中移除
router.removeRoutee(t.actor());
}).matchAny(o -> {
//转发消息给路由目标
router.route(o, getSender());
}).build();
}
}
上述我们创建了路由目标类FirstRoutee和路由器FirstRouter,其中创建Router过程中,使用new RoundRobinRoutingLogic()轮询策略,表明消息按照轮询的方式给Routee发送消息。另外,我们在Router中监控了每个子Actor,当子Actor停止时,就会从列表中移除。
执行main方法,结果如下:
Actor[akka://system/user/router/routee0#-311531524]-->MessageOne
Actor[akka://system/user/router/routee1#1417357478]-->MessageTwo
Actor[akka://system/user/router/routee0#-311531524]-->MessageThree
从结果中可以看出,消息进过Router会轮询发送给Routee。大家执行一下,你们可能看到不一样的结果,或者如下:
Actor[akka://system/user/router/routee0#-206475727]-->MessageOne
Actor[akka://system/user/router/routee0#-206475727]-->MessageThree
Actor[akka://system/user/router/routee1#1122932044]-->MessageTwo
这里大家注意,多个Actor接受同一个发送者的消息是并行的而不是串行的(前面我们已经提到过),但是一个Actor接受同一个Actor的消息一定是串行的。
大家是不是特别想知道RoundRobinRoutingLogic是怎样实现轮询的呢?当大家默认了(嘻嘻),贴一段源码:
int size = routees.size();
int index = (int)(this.next().getAndIncrement() % (long)size);
var10000 = (Routee)routees.apply(index < 0?size + index:index);
RoundRobinRoutingLogic继承RoutingLogic,并重写select方法。在select方法中使用上述代码,大家对上述代码段是不是有点熟悉呢,这就是采用取余轮询方式,这里定义了一个AtomicLong next = new AtomicLong()对象,通过原子类递增,然后使用routees的长度取余数,得到路由目标的下标,从而做到轮询。这里贴出源码,是想告诉大家,框架内含原理是我们应该特别注重的,这就是一个典型的轮询实现方式,大家或许在其它地方使用过,或者(数据库分库),废话不多说,继续。
路由策略
Akka已经为我们提供了许多内置的路由策略,大家可以放心的使用,总结表格如下:
路由策略 | 含义 |
akka.routing.RoundRobinRoutingLogic | 轮询策略,轮询的给每个Routee发送消息。 |
akka.routing.RandomRoutingLogic | 随机策略,随机的给某个Routee发送消息。 |
akka.routing.SmallestMailboxRoutingLogic | 优先消息较少策略,给消息较少的非挂起的Routee发送消息。 |
akka.routing.BroadcastRoutingLogic | 广播策略,以广播的形式给所有Routee发送消息。 |
akka.routing.ScatterGatherFirstCompletedRoutingLogic | 最快回复策略,发送消息给所有Routee,期待最快回复(其它消息丢弃)。 |
akka.routing.TailChoppingRoutingLogic | 随机间隔策略,随机发送消息给一个Routee,然后间隔时间后发送消息给第二个随机Routee,也是期待最快回复(其它消息丢弃)。 |
akka.routing.ConsistentHashingRoutingLogic | 一致性hash策略,使用一致性hash算法来选择Routee。 |
路由Actor
路由器可以是一个自包含的Actor,它通常管理着自己的所有Routee,一般来讲,我们会把路由配置到文件中,最终通过编码的方式加载并创建路由器。
创建路由Actor有两种模式:pool和group。
pool:该方式路由器Actor会创建子Actor作为Routee并对其监督和监控,当子Actor停止时,从Router列表中移除。
示例:
public class PoolRouter extends AbstractActor {
private ActorRef router;
@Override
public void preStart() throws Exception {
//创建pool类型路由器并制定路由目标数量,以及子类类型
router = getContext().actorOf(new RoundRobinPool(3).props(Props.create(PoolRoutee.class)), "poolRoutee");
}
@Override
public Receive createReceive() {
return receiveBuilder().matchAny(o -> router.tell(o, getSender())).build();
}
public static void main(String[] args) {
ActorSystem system = ActorSystem.create("system");
ActorRef ref = system.actorOf(Props.create(PoolRouter.class),"poolRouter");
ref.tell("MessageA",ActorRef.noSender());
ref.tell("MessageB",ActorRef.noSender());
ref.tell("MessageC",ActorRef.noSender());
}
}
class PoolRoutee extends AbstractActor {
@Override
public Receive createReceive() {
return receiveBuilder().matchAny(o -> {
//输出父类信息,便于观察
System.out.println(getSelf() + "-->" + o + "-->" + getContext().parent());
}).build();
}
}
使用pool方式,会自动创建PoolRoutee类型的子类Actor,大家可以自行执行main方法,验证该结果,这里不占太多篇幅。上面我们是使用代码的方式创建pool路由,其实还可以使用配置方式,如下:
akka.actor.deployment{
/poolRouter/poolRoutee{
router=round-robin-pool
#路由目标数量
nr-of-instances=3
}
}
使用该方式,需要修改router的创建方式,如: router=getContext().actorOf(FromConfig.getInstance().props(Props.create(PoolRoutee.class)),"poolRoutee");
group:该方式可以将Routee的创建方式放在外部,路由器通过路径对这些路由目标发送消息。
为了篇幅问题,这里给出核心代码,其它代码和pool示例类似,示例:
preStart():
getContext().actorOf(Props.create(TaskRoutee.class),"p1");
getContext().actorOf(Props.create(TaskRoutee.class),"p2");
getContext().actorOf(Props.create(TaskRoutee.class),"p3");
router=getContext().actorOf(FromConfig.getInstance().props(),"groupRouter");
配置如下:
akka.actor.deployment{
/group/groupRouter{
router=round-robin-group
routees.paths=["/user/group/p1","/user/group/p2","/user/group/p3"]
}
}
在这里,/group/groupRouter表示router的路径,routees.paths表示routee的路径,其中Routee的路径不一定在同一个层级下,本地和远程Actor都支持,只需在paths中配置路径则可。
广播-Broadcast
使用广播路由策略,广播路由器会给其中所有的Routee发送消息。示例:
public class BroadcastRouter extends AbstractActor {
private ActorRef router;
@Override
public void preStart() throws Exception {
getContext().actorOf(Props.create(BroadcastRoutee.class), "broadcastRouteeA");
getContext().actorOf(Props.create(BroadcastRoutee.class), "broadcastRouteeB");
router = getContext().actorOf(FromConfig.getInstance().props(), "broadcastRouter");
}
@Override
public Receive createReceive() {
return receiveBuilder().matchAny(o -> router.tell(o, getSender())).build();
}
public static void main(String[] args) {
//创建system并加载system.conf配置,获取group配置
ActorSystem system = ActorSystem.create("system", ConfigFactory.load("system"));
ActorRef ref = system.actorOf(Props.create(BroadcastRouter.class),"broadcast");
ref.tell("MessageA",ActorRef.noSender());
ref.tell("MessageB",ActorRef.noSender());
ref.tell("MessageC",ActorRef.noSender());
}
}
class BroadcastRoutee extends AbstractActor {
@Override
public Receive createReceive() {
return receiveBuilder().matchAny(o -> System.out.println(getSelf() + "-->" + o)).build();
}
}
上述我们采用group的方式进行编写,那么必不可少我们的router配置,如下:
akka.actor.deployment{
/broadcast/broadcastRouter{
router=broadcast-group
routees.paths= ["/user/broadcast/broadcastRouteeA","/user/broadcast/broadcastRouteeB"]
}
}
执行main方法,我们来看看routee是不是获取同样的消息:
Actor[akka://system/user/broadcast/broadcastRouteeA#1160871370]-->MessageA
Actor[akka://system/user/broadcast/broadcastRouteeA#1160871370]-->MessageB
Actor[akka://system/user/broadcast/broadcastRouteeA#1160871370]-->MessageC
Actor[akka://system/user/broadcast/broadcastRouteeB#978193175]-->MessageA
Actor[akka://system/user/broadcast/broadcastRouteeB#978193175]-->MessageB
Actor[akka://system/user/broadcast/broadcastRouteeB#978193175]-->MessageC
最快回复-ScatterGatherFirstCompleted
使用ScatterGatherFirstCompleted路由策略,会发送所有消息给所有Routee,它会等待一个最快回复,一收到回复,其它回复就会被丢弃。ScatterGatherFirstCompleted提供了within参数,用于设置最长等待时间,如果超过时间还没有得到回复,就会收到time out异常消息。使用ScatterGatherFirstCompleted,这里我们贴出核心配置代码,创建Routee和上述代码类似,参考配置:
akka.actor.deployment{
/scatter/scatterRouter{
router=scatter-gather-group
routees.paths=["/user/scatter/responA","/user/scatter/responB"]
#超时时限2秒
within= 2 seconds
}
}
为了获取Routee回复的消息,我们可以采用ask方式发送消息,之后可以通过Future异步回调方式获取结果(ask请求方式可以参考Actor简介(一))。
特殊消息处理
在正常情况下,发送给路由的消息会按照指定的路由策略发送给路由目标,但是对于某些特殊的消息,它的处理方式可能跟路由策略无关。
Broadcast消息
要想实现广播消息发送,我们可以使用Broadcast路由,但是在这里,我们可以使用Broadcast包装的消息来实现该功能,如下:
router.tell(new Broadcast("广播消息"), ActorRef.noSender());
这样,所有的Routee都可以接受到“广播消息”,与原本的路由策略无关。
PoisonPill消息
给路由器发送PoisonPill消息,该消息不会被转发给Routee,而是被路由器内部消化。对于具有层级关系的路由策略,类似于pool类型的路由,当父级收到PoisonPill消息时,会先终止子级再终止自己,子级在处理完当前消息后终止,不包括邮箱队列中的消息。这不是一个好的方式,我们希望子级处理完自己的消息包括邮箱队列中的消息再停止自己,那么Broadcast消息就派上用场了,给Router发送Broadcast消息,那么每个Routee会受到Broadcast消息,该消息会进入Routee的邮箱队列,这样可以确保Routee会再处理完现有的消息下,再进行停止。如下:
router.tell(new Broadcast(PoisonPill.getInstance()), ActorRef.noSender());
管理消息
消息类型 | 描述 |
akka.routing.GetRoutees | 查询指令,返回一个Routees对象,其中包含Routee列表 |
akka.routing.AddRoutee | 新增指令,向路由发起新增Routee对象 |
akka.routing.RemoveRoutee | 删除指令,删除已经存在的Routee对象 |
akka.routing.AdjustPoolSize | 改变pool池大小的请求,需要一个int参数,正数表示新增,负数表示删除,数量为参数值 |
注意:新增和删除操作可能不会立马生效,我们可以发送GetRoutees消息来确定是否已经完成。
总结
针对复杂的消息传递,我们可以使用Akka内置的路由策略。Akka中包含两种路由:pool和group,pool具有父子层级关系的路由器,group可以根据任意path添加Routee的路由器。Akka已经提供了多种类型的路由,例如广播、轮询、最快回复等,我们可以通过代码和配置两种创建方式,非常方便。