6.RPC
RPC工作原理:
1.客户端发起RPC请求时,request请求中会发送两个参数replyTo和correlationId
replyTo:同步互斥队列,也就是该请求对应的队列
correlationId:唯一标识
2.请求存入rpc队列,采用的是有界数组阻塞队列(ArrayBlockingQueue)
3.消息接受端(也就是服务器端)接受到请求之后,利用replyTo中的携带的数据,处理任务并返回结果,返回结果中携带correlationId和具体结果
咱们来复习一下BlockingQueue(ArrayBlockingQueue回头再单独学习):
阻塞队列:在队列元素为空的情况下,阻塞获取数据,直到队列不空;在队列元素已满的情况下,阻塞插入数据,直到队列不满
boolean add(E e); // 将数据插入队列
boolean offer(E e); // 将数据插入队列,比add好,因为add队列满时抛出异常,offer是返回false; // 根本是add = {!offer(e) throw new IllegalStateException("Deque full");}
void put(E e) throws InterruptedException; // 等到队列不满时插入元素
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; // 如果队列满时,等待一段时间,待超时后返回false;如果超时时间内不满,就插入返回true
E take() throws InterruptedException; // 从队列头部取出数据(即获取到数据并将其从队列中删除)
E poll(long timeout, TimeUnit unit) throws InterruptedException; // 在超时时间内取出队头数据
int remainingCapacity(); // 队列剩余容量
boolean remove(Object o); // 将元素从队列中删除
public boolean contains(Object o); // 判断元素是否存在队列中
int drainTo(Collection<? super E> c); // 将队列数据迁移到另一个集合中
int drainTo(Collection<? super E> c, int maxElements); // 将队列数据迁移到另一个集合中,限定大小
此处做一点小优化,既然服务每次都需要去做连接,不如把Connection提取出来:
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* @author Becolette
* @description AMQP连接公共服务
* @datetime 2020/9/24 15:02
*/
public class MqConnection {
/**
* 创建RabbitMq连接
*
* @param hostName
* @param port
* @param userName
* @param password
* @param vHost
* @return
*/
public static Connection createConnection(String hostName, int port, String userName,
String password, String vHost) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(hostName);
factory.setPort(port);
factory.setUsername(userName);
factory.setPassword(password);
factory.setVirtualHost(vHost);
Connection connection = null;
try {
connection = factory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
}
客户端
import com.becolette.amqp.rabbit.simple.MqConnection;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* @author Becolette
* @description 客户端
* @datetime 2020/9/28 16:08
*/
public class RpcClient{
/**
* 1.客户端发起RPC请求时,request请求中会发送两个参数replyTo和correlationId
* replyTo:同步互斥队列,也就是该请求对应的队列
* correlationId:唯一标识
* 2.请求存入rpc队列,采用的是有界数组阻塞队列(ArrayBlockingQueue)
* 3.消息接受端(也就是服务器端)接受到请求之后,利用replyTo中的携带的数据,处理任务并返回结果,返回结果中携带correlationId和具体结果
*/
private static String requestQueueName = "rpc_queue";
public static void main(String[] argv) throws Exception {
// 第一步:创建连接和通道
Connection connection = null;
Channel channel = null;
try {
connection = MqConnection.createConnection("localhost", 5672, "guest", "guest", "/");
channel = connection.createChannel();
// 第二步:发起请求
for (int i = 0; i < 32; i++) {
String i_str = Integer.toString(i);
System.out.println(" [x] Requesting fib(" + i_str + ")");
String response = call(i_str, channel);
System.out.println(" [.] Got '" + response + "'");
}
}catch (Exception e){
e.printStackTrace();
} finally {
channel.close();
connection.close();
}
}
public static String call(String message, Channel channel) throws IOException, InterruptedException {
// 使用uuid作为唯一键
final String corrId = UUID.randomUUID().toString();
String replyQueueName = channel.queueDeclare().getQueue();
// 将correlationId和replyTo作为属性参数发送给服务器
AMQP.BasicProperties props = new AMQP.BasicProperties
.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build();
/** 采用的是工作模式 **/
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
/** 有界阻塞队列大小为1 */
final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
String cTag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
// 入队
response.offer(new String(delivery.getBody(), "UTF-8"));
}
}, consumerTag -> {
});
String result = response.take();
channel.basicCancel(cTag);
return result;
}
}
服务端
import com.becolette.amqp.rabbit.simple.MqConnection;
import com.rabbitmq.client.*;
/**
* @author Becolette
* @description 服务端
* @datetime 2020/9/28 16:10
*/
public class RpcServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
/**
* 斐波那契数列
*
* @param n
* @return
*/
private static int fib(int n) {
if (n == 0 || n == 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] argv) throws Exception {
Connection connection = MqConnection.createConnection("localhost", 5672, "guest", "guest", "/");
try (Channel channel = connection.createChannel()) {
// 声明队列,队列名,不进行持久化,不互斥,不自动删除,无其他参数
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
// 清除给定队列的内容
channel.queuePurge(RPC_QUEUE_NAME);
// 公平分配
channel.basicQos(1);
System.out.println(" [x] Awaiting RPC requests");
Object monitor = new Object();
// 返回给客户端参数
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
AMQP.BasicProperties replyProps = new AMQP.BasicProperties
.Builder()
.correlationId(delivery.getProperties().getCorrelationId())
.build();
String response = "";
try {
String message = new String(delivery.getBody(), "UTF-8");
int n = Integer.parseInt(message);
System.out.println(" [.] fib(" + message + ")");
response += fib(n);
} catch (RuntimeException e) {
System.out.println(" [.] " + e.toString());
} finally {
channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes("UTF-8"));
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
// RabbitMq consumer worker thread notifies the RPC server owner thread
synchronized (monitor) {
monitor.notify();
}
}
};
channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> {}));
// 等到所有的请求处理完成后,再把数据返回给客户端
while (true) {
synchronized (monitor) {
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
RPC工作优缺点
优点:
1.客户端请求可以发送给多个消费者,取最快返回结果
2.客户端发送一条请求,结果无需同步返回
3.无需关心有没有服务器处理请求,响应超时、出错或故障该如何处理;无需关心无效输入信息
缺点:
1.RPC的响应可能会很慢
2.出现错误时,排查原因比较麻烦