消息路由

        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已经提供了多种类型的路由,例如广播、轮询、最快回复等,我们可以通过代码和配置两种创建方式,非常方便。

猜你喜欢

转载自blog.csdn.net/p_programmer/article/details/84204489