使用Rabbitmq/spring进行RPC

来源:互联网 发布:台湾奶粉代购 知乎 编辑:程序博客网 时间:2024/06/06 05:09

使用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);}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

服务端实现

配置文件内容

<?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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

发布服务代码:

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

客户端实现

<?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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

客户端调用代码:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("ctx-client.xml");Service service = ctx.getBean(Service.class);service.sayHello("name");ctx.destroy();
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

优化

问题

使用默认的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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

使用自定义的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);        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

重写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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

测试结果

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

0 0