使用Rabbitmq/spring进行RPC

2 篇文章 0 订阅
1 篇文章 0 订阅

使用RabbitMQ进行RPC的必要

常见的RPC方法/协议包括CORBA,Java RMI,Web Service,Hessian,Thrift及Rest API,相对于前面提到的RPC方式,使用RabbbitMQ(JMS也是一样)方式需要在client-service provider中间增加MQ组件,这样做增加了部署的复杂性,但同时带来额外的好处。

  1. 可以对service provider进行保护,MQ对请求进行缓冲,处理不了的请求可以被MQ抛弃而不会压垮service provider。
  2. 可以隔离低安全区对高安全区的访问,此优点是其他rpc没有的。
    一个典型的互联网访问方式如下(FW表示防火墙)
    client—-FW1–>frontend server(DMZ)—–FW2—->service provider(高安全区)
    通过MQ进行RPC则变成
    client—-FW1–>frontend server(DMZ)—>MQ<–FW2—-service provider(高安全区)
    这里一个重要的区别是service provider(高安全区)是主动访问位于DMZ区域的MQ,由此可以设定FW2只允许高安全区访问DMZ,而禁止DMZ访问高安全区。此方式对于安全性要求高的如银行,金融,政府系统尤为重要。

实现spring remoting实现使用MQ的RPC


原理

一个RPC交互大致分为以下几个步骤:
1. 服务端监听MQ队列
2. 客户端将调用请求(调用请求包括结果返回队列)发送到队列中。
3. 客户端在结果返回队列监听。
4. 服务端处理业务,将结果发送到结果返回队列。

公共内容

定义RabbitMQ的连接,定义service接口,进行通讯的queue。

public interface Service {
    String sayHello(String name);
}

服务端实现

配置文件内容

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:lang="http://www.springframework.org/schema/lang" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-3.0.xsd
        http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit-1.3.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">
    <!--MQ连接-->
    <rabbit:connection-factory id="rabbitConnectionFactory" />

    <!--创建必要的exchange及queue-->
    <rabbit:admin connection-factory="rabbitConnectionFactory" />
    <rabbit:direct-exchange name="exchange">
        <rabbit:bindings>
            <rabbit:binding queue="queue.appgw" key="queue.appgw">
            </rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>
    <rabbit:queue name="queue.appgw">
    </rabbit:queue>

    <!--返回结果的template-->
    <rabbit:template id="amqpTemplateInternetProxy"
        connection-factory="rabbitConnectionFactory">
    </rabbit:template>
    <!--服务监听-->
    <rabbit:listener-container acknowledge="none"
        max-concurrency="128" prefetch="10">
        <rabbit:listener ref="service" queue-names="queue.appgw" />
    </rabbit:listener-container>
    <!--服务实现-->
    <bean id="serviceImpl" class="net.nxmax.atp.exporter.ServiceImpl"></bean>
    <!--服务发布-->
    <bean id="service"
        class="org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter">
        <property name="serviceInterface" value="net.nxmax.atp.exporter.Service" />
        <property name="service" ref="serviceImpl" />
        <property name="amqpTemplate" ref="amqpTemplateInternetProxy" />
    </bean>
</beans>

发布服务代码:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
while (!r.readLine().equalsIgnoreCase("exit")) {
}
ctx.destroy();

客户端实现

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:lang="http://www.springframework.org/schema/lang" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
    xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-3.0.xsd
        http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit-1.3.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    <!--MQ连接-->
    <rabbit:connection-factory id="rabbitConnectionFactory"/>
    <!--发送请求的template-->
    <rabbit:template id="amqpTemplate" connection-factory="rabbitConnectionFactory"
        queue="queue.appgw" exchange="exchange" reply-timeout="60000" />
    <bean class="org.springframework.amqp.remoting.client.AmqpProxyFactoryBean"
        p:serviceInterface="net.nxmax.atp.exporter.Service" p:routingKey="queue.appgw">
        <property name="amqpTemplate" ref="amqpTemplate" />
    </bean>
</beans>

客户端调用代码:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("ctx-client.xml");
Service service = ctx.getBean(Service.class);
service.sayHello("name");
ctx.destroy();

优化

问题

使用默认的spring实现存在以下的限制:
- 消息使用持久化,对于RPC来说消息是不需要持久化的。
- 消息没有过期时间,意味着后端可能需要处理很久以前的请求,对于RPC来说很久以前的请求应该要抛弃。
- 默认使用临时队列作为结果返回队列,意味着每次调用都需要创建队列,性能极差。
- 可以配置使用固定队列返回结果,但是如果多个节点使用一个配置,同时监听固定队列,可能造成节点收不到结果。
- 使用默认的java作为序列化实现,性能不如Kryo。

方案

针对默认实现存在的问题,可以使用以下优化方案:
- 每一个客户端使用独立规定的结果返回队列,避免创建临时队列。
- 设置消息为非持久化。
- 设置消息的超时时间,使用Kryo作为序列化库。

实现

需要做的关键修改有以下3个:
创建自定义的MessageConverter,将消息设置为非持久化,设定过期时间,以及使用kryo序列化。

public class MessageConverterWithExpire extends SimpleMessageConverter {

    /** Logger */
    protected static final Logger log = LoggerFactory
            .getLogger(MessageConverterWithExpire.class);

    // Setup ThreadLocal of Kryo instances
    private ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {
        protected Kryo initialValue() {
            Kryo kryo = new Kryo();
            // configure kryo instance, customize settings
            kryo.register(RemoteInvocation.class);
            Registration reg = kryo.register(RemoteInvocationResult.class);
            reg.setInstantiator(new ObjectInstantiator() {
                @Override
                public Object newInstance() {
                    return new RemoteInvocationResult(null);
                }

            });
            return kryo;
        };
    };

    @Override
    protected Message createMessage(Object object,
            MessageProperties messageProperties)
            throws MessageConversionException {
        // expire in ms.
        messageProperties.setExpiration("45000");
        messageProperties.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT);

        Kryo k = kryos.get();
        Output output = new Output(4096, 4194304);
        k.writeClassAndObject(output, object);
        return new Message(output.toBytes(), messageProperties);
        // return super.createMessage(object, messageProperties);
    }

    @Override
    public Object fromMessage(Message message)
            throws MessageConversionException {
        Kryo k = kryos.get();
        return k.readClassAndObject(new Input(message.getBody()));
        // return super.fromMessage(message);
    }
}

使用自定义的listenerContainer,在监听前创建规定的结果返回队列。

public class DynamicReplyMessageListenerContainer extends
        SimpleMessageListenerContainer {
    @Override
    protected void doInitialize() throws Exception {
        super.doInitialize();
        Object listener = getMessageListener();
        if (listener instanceof RabbitTemplate) {
            RabbitTemplate template = (RabbitTemplate) listener;
            Queue queue = getRabbitAdmin().declareQueue();
            template.setReplyQueue(queue);
            setQueues(queue);
        }
    }
}

重写AmqpInvokerServiceExporter ,提供通过固定队列返回结果的支持。

public class AmqpInvokerServiceExporterCorrelate extends
        AmqpInvokerServiceExporter {
    @Override
    public void onMessage(Message message) {
        Address replyToAddress = message.getMessageProperties()
                .getReplyToAddress();
        if (replyToAddress == null) {
            throw new AmqpRejectAndDontRequeueException(
                    "No replyToAddress in inbound AMQP Message");
        }

        Object invocationRaw = getMessageConverter().fromMessage(message);

        RemoteInvocationResult remoteInvocationResult;
        if (invocationRaw == null
                || !(invocationRaw instanceof RemoteInvocation)) {
            remoteInvocationResult = new RemoteInvocationResult(
                    new IllegalArgumentException(
                            "The message does not contain a RemoteInvocation payload"));
        } else {
            RemoteInvocation invocation = (RemoteInvocation) invocationRaw;
            remoteInvocationResult = invokeAndCreateResult(invocation,
                    getService());
        }
        send(remoteInvocationResult, replyToAddress, message);
    }

    private void send(Object object, Address replyToAddress,
            Message sourceMessage) {
        MessageProperties mp = new MessageProperties();
        mp.setCorrelationId(sourceMessage.getMessageProperties()
                .getCorrelationId());
        Message message = getMessageConverter().toMessage(object, mp);

        getAmqpTemplate().send(replyToAddress.getExchangeName(),
                replyToAddress.getRoutingKey(), message);
    }

}

测试结果

默认实现的tps在640左右。经过优化的TPS在4000左右。更多内容请参见测试代码。
测试代码下载

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值