RabbitMQ
MQ作为一个消息中间件解耦程序的,对于那种对于实时性要求不太高的业务可以使用整个进行处理业务的,其实作为通信我们拥有很多的方式啊,比如共享内存,共享数据库,Http,webservice等等,很多都是一些业内的标准了,形成这样的标准主要是为了减轻开发人员的工作强度,而且能够非常方便大家对于中间件的理解。
参考资料
基本的概念
AMQP(高级消息队列协议)
AMQP(高级消息队列协议) 是一个异步消息传递所使用的应用层协议规范,作为线路层协议,而不是API(例如JMS),AMQP 客户端能够无视消息的来源任意发送和接受信息。AMQP的原始用途只是为金融界提供一个可以彼此协作的消息协议,而现在的目标则是为通用消息队列架构提供通用构建工具。因此,面向消息的中间件 (MOM)系统,例如发布/订阅队列,没有作为基本元素实现。反而通过发送简化的AMQ实体,用户被赋予了构建例如这些实体的能力。这些实体也是规范的一 部分,形成了在线路层协议顶端的一个层级:AMQP模型。这个模型统一了消息模式,诸如之前提到的发布/订阅,队列,事务以及流数据,并且添加了额外的特性,例如更易于扩展,基于内容的路由。
AMQP当中有四个概念非常重要
- virtual host,虚拟主机
- exchange,交换机
- queue,队列
- binding,绑定
何谓虚拟主机(virtual host),交换机(exchange),队列(queue)和绑定(binding)
virtual host,虚拟主机
一个虚拟主机持有一组交换机、队列和绑定。
为什么需要多个虚拟主机呢?因为RabbitMQ当中,用户只能在虚拟主机的粒度进行权限控制。因此,如果需要禁止A组访问B组的交换机/队列/绑定,必须为A和B分别创建一个虚拟主机。每一个RabbitMQ服务器都有一个默认的虚拟主机 / 。
queue,队列
队列(Queues)是你的消息(messages)的终点,可以理解成装消息的容器。消息就一直在里面,直到有客户端(也就是消费者,Consumer)连接到这个队列并且将其取走为止。
队列一般由消费者创建因为生产者仅仅是为了生产消息,而不是为了从队列中获取消息,获取消息是消费者的事情。
队列是由消费者(Consumer)通过程序建立的,不是通过配置文件或者命令行工具。这没什么问题,如果一个消费者试图创建一个已经存在的队列,RabbitMQ会直接忽略这个请求。因此我们可以将消息队列的配置写在应用程序的代码里面。
交换机(Exchange) 路由表
要把一个消息放进队列前,需要有一个交换机(Exchange),交换机(Exchange)可以理解成具有路由表的路由程序。
每个消息都有一个称为路由键(routing key)的属性,就是一个简单的字符串。【这里要理解,其实交换机发送消息的时候,需要一个routingkey属性的,并不需要进行队列绑定,那个是消费者的事情,当发送消息到当前交换机的rountingkey中,rabbitMq会去寻找绑定到整个路由的所有的队列的名称,然后将消息发送给整个队列哦。http://www.kancloud.cn/digest/rabbitmq-for-java/122038 这里讲解了分发模式和直连模式哦.
一般情况下,我们发送发就这样简单的搞定了
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");//一般是由消费者创建
channel.basicPublish(EXCHANGE_NAME,ROUTINGKEY, null, message.getBytes());
交换机当中有一系列的绑定(binding),即路由规则(routes)。
(例如,指明具有路由键 “X” 的消息要到名为timbuku的队列当中去。)
这个就是绑定一般发生在消费者,一个消费着申明一个交换机,一个队列,然后将整个绑定到一起就好了,在加上我们的路由REOUTINGKEY,一个队列可以绑定多个路由。
channel.queueBind(queueName_timbuku, EXCHANGE_NAME, “ROUTINGKEY_X”);·
下面就是一个简单的处理哦,在消费者里面。
connection = RabbitMQConnectionFactory.getConnection();
channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
消费者程序(Consumer)
消费者程序(Consumer)要负责创建你的交换机。交换机可以存在多个,每个交换机在自己独立的进程当中执行,因此增加多个交换机就是增加多个进程,可以充分利用服务器上的CPU核以便达到更高的效率。例如,在一个8核的服务器上,可以创建5个交换机来用5个核,另外3个核留下来做消息处理。类似的,在RabbitMQ的集群当中,你可以用类似的思路来扩展交换机一边获取更高的吞吐量。
绑定(binding)
交换机如何判断要把消息送到哪个队列?你需要路由规则,即绑定(binding)。一个绑定就是一个类似这样的规则:将交换机“desert(沙漠)”当中具有路由键“阿里巴巴”的消息送到队列“hideout(山洞)”里面去。换句话说,一个绑定就是一个基于路由键将交换机和队列连接起来的路由规则。例如,具有路由键“wangwang”的消息需要被送到两个队列,“jet”和“wang”。要做到这个,就需要创建两个绑定,每个都连接一个交换机和一个队列,两者都是由“audit”路由键触发。在这种情况下,交换机会复制一份消息并且把它们分别发送到两个队列当中。交换机不过就是一个由绑定构成的路由表。
特性
这些特性和我们的网络通信机制类似的,为了减少丢包重传机制等等。
持久化
你花了大量的时间来创建队列、交换机和绑定,然后,服务器程序挂了。你的队列、交换机和绑定怎么样了?还有,放在队列里面但是尚未处理的消息们呢?
如果你是用默认参数构造的这一切的话,那么,他们都灰飞烟灭了。RabbitMQ重启之后会干净的像个新生儿。你必须重做所有的一切,亡羊补牢,如何避免将来再度发生此类杯具?
队列和交换机有一个创建时候指定的标志durable。durable的唯一含义就是具有这个标志的队列和交换机会在重启之后重新建立,它不表示说在队列当中的消息会在重启后恢复。那么如何才能做到不只是队列和交换机,还有消息都是持久的呢?
但是首先需要考虑的问题是:是否真的需要消息的持久化?如果需要重启后消息可以回复,那么它需要被写入磁盘。但即使是最简单的磁盘操作也是要消耗时间的。所以需要衡量判断。
RabbitMQ 不允许你绑定一个非坚固(non-durable)的交换机和一个durable的队列。反之亦然。要想成功必须队列和交换机都是durable的。
一旦创建了队列和交换机,就不能修改其标志了。例如,如果创建了一个non-durable的队列,然后想把它改变成durable的,唯一的办法就是删除这个队列然后重现创建。因此,最好仔细检查创建的标志。
注意的是,如果已经声明了同名非持久化的Queue,则再次声明无效。
发送方和接收方都需要指定该参数。
boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);
负载分配
为了解决各个接收端工作量相差太大的问题(有的一直busy,有的空闲比较多),突破Round-robin。
int prefetchCount = 1;
channel.basicQos(prefetchCount);
意思为,最多为当前接收方发送一条消息。如果接收方还未处理完毕消息,还没有回发确认,就不要再给他分配消息了,应该把当前消息分配给其它空闲接收方。
实战
主要处理消费者
- 加载配置的routingkey,路由等等,灵活性,模块化,不要将所有的处理都放在一个文件中,要单一职责原则 RabbitMQConfigLoader.java 加载配置的属性
InputStream is = RabbitMQConfigLoader.class.getResourceAsStream("/config.properties");
Properties properties = new Properties();
properties.load(is);
is.close();
username = properties.getProperty("rabbitMQ_username");
password = properties.getProperty("rabbitMQ_password");
ip = properties.getProperty("rabbitMQ_host");
port = properties.getProperty("rabbitMQ_port", "5672");
- 创建处理连接的工厂
**
* Created by wangji on 2017/3/9.
* 创建RabbitMQ的工厂类
*/
public class RabbitMQConnectionFactory {
final static Logger log = LoggerFactory.getLogger(RabbitMQConfigLoader.class);
private volatile static ConnectionFactory connectionFactory = null;
/**
* 只创建一个ConnectionFactory
* 双重检查加锁
* @return
*/
private static ConnectionFactory getSingleInstance(){
if(connectionFactory == null){
synchronized (RabbitMQConnectionFactory.class){
if(connectionFactory == null){
connectionFactory = new ConnectionFactory();
connectionFactory.setUsername(RabbitMQConfigLoader.username);
connectionFactory.setPassword(RabbitMQConfigLoader.password);
connectionFactory.setHost(RabbitMQConfigLoader.ip);
connectionFactory.setPort(Integer.valueOf(RabbitMQConfigLoader.port));
}
}
}
return connectionFactory;
}
public static Connection getConnection(){
if(connectionFactory == null){
getSingleInstance();
}
Connection connection = null;
try {
connection =connectionFactory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
}
- RabbitMQDestory 处理消灭资源
interface RabbitMQDestory{
void destory();
}
- BaseConsumer 多个消费着在全部是Runbale的处理,在线程池中进行处理,基类是一个抽象类将RPC和一般的发送进行了处理哦,非常的方便处理的哈哈。这样不用在每个消费者都去重新写,这里还使用的模板方法模式。调用子类的work。
public abstract class BaseConsumer implements RabbitMQDestory, Runnable {
final static Logger log = LoggerFactory.getLogger(BaseConsumer.class);
protected String EXCHANGE_NAME = RabbitMQConfigLoader.cplcexchange;//交换机名称
protected String QUEUE_NAME;//对列名称
private String ROUTING_KEY;//路由
protected Connection connection;
protected boolean isRPC = false;
protected Channel channel;
protected QueueingConsumer consumer;
protected QueueingConsumer.Delivery delivery = null;
public BaseConsumer(String ROUTING_KEY, String QUEUE_NAME, boolean ISRPC) {
this.ROUTING_KEY = ROUTING_KEY;
this.QUEUE_NAME =QUEUE_NAME;
this.isRPC =ISRPC;
CreateConnection();
}
private void CreateConnection() {
try {
connection = RabbitMQConnectionFactory.getConnection();
channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
channel.basicQos(1);//负载均衡
consumer = new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME, false, consumer);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
String message = null;
boolean flag = true;
while (flag) {
try {
try {
delivery = consumer.nextDelivery(60000);
} catch (InterruptedException e) {
// e.printStackTrace();
} catch (ShutdownSignalException e) {
//e.printStackTrace();
} catch (ConsumerCancelledException e) {
//e.printStackTrace();
}
if (delivery == null) {
log.info("************ROUTING_KEY:"+ROUTING_KEY+",60s has no massage*************");
try {
Thread.sleep(1000);
if(consumer !=null){
continue;
}else{
if (channel != null) {
channel.close();
}
if (connection != null) {
connection.close();
}
CreateConnection();
}
} catch (Exception e) {
e.printStackTrace();
}
continue;
} else {
message = new String(delivery.getBody(), "UTF-8");
log.info("【收到消息】:"+message+",【routingkey】:"+ delivery.getEnvelope().getRoutingKey());
doWork(message);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
} catch (Exception e) {
log.error("--------Connect fail...Please check the network!!!--------", e);
try {
try {
Thread.sleep(60000 * 2);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
if (channel != null) {
channel.close();
}
if (connection != null) {
connection.close();
}
CreateConnection();
continue;
} catch (Exception e1) {
log.error("--------Thread error--------", e1);
try{
Thread.sleep(60000*2);
}catch(Exception e2){
}
continue;
}
}
}
}
/**
* 模板方法模式,参考GenericServlet.java
* @param message
*/
protected abstract void doWork(String message);
protected void publishMessage(String sendMessage, String rountingKey) {
if (connection != null && channel != null) {
try {
if (isRPC == false) {
Connection connectionsend = RabbitMQConnectionFactory.getConnection();
Channel sendMessageChannel = connectionsend.createChannel();
sendMessageChannel.exchangeDeclare(EXCHANGE_NAME, "direct");
sendMessageChannel.basicPublish(EXCHANGE_NAME, rountingKey, null, sendMessage.getBytes("UTF-8"));
log.info("【发送的消息】:" + sendMessage + ",【rountingKey】:" + rountingKey);
sendMessageChannel.close();
connectionsend.close();
} else {
AMQP.BasicProperties props = delivery.getProperties();
/**
* 如果一个客户端有很多的计算任务,按照上面的代码,
* 我们会为每个任务创建一个请求,然后等待返回的结果,
* 这种方法貌似很耗时,如果把所有的任务都放到同一个连接中,
* 那么我们又没法分辨出返回的结果是那个任务的?
* 为了解决这个问题,RabbitMQ提供了一个correlationid属性来解决这个问题。Ra
* bbitMQ为每个请求提供唯一的编号,然后在返回队列里如果看到了这个编号,就知道我们的任务处理完成了,
* 如果收到的编号不认识,就可以安全的忽略。
*/
AMQP.BasicProperties replyProps = new AMQP.BasicProperties.Builder().correlationId(props.getCorrelationId()).build();
Connection connectionsend = RabbitMQConnectionFactory.getConnection();
Channel sendMessageChannel = connectionsend.createChannel();
sendMessageChannel.exchangeDeclare(EXCHANGE_NAME, "direct");
log.info("【向发送RPC的消息】:" + sendMessage + ",【rountingKey】:" +props.getReplyTo());
sendMessageChannel.basicPublish("", props.getReplyTo(), replyProps, sendMessage.getBytes("UTF-8"));
sendMessageChannel.close();
connectionsend.close();
}
} catch (Exception e) {
e.printStackTrace();
log.info("*******向三方系统传递消息异常**************");
}
}
}
@Override
public void destory() {
log.info("********************关闭连接************************");
try {
if (this.channel != null) {
channel.close();
channel = null;
}
if (this.connection != null) {
if (connection.isOpen()) {
connection.close();
connection = null;
}
}
} catch (Exception e) {
e.printStackTrace();
log.info("********************关闭连接异常************************");
}
}
}
- 创建一个实现类
public class InfoConsumer extends BaseConsumer {
public InfoConsumer() {
super("ROUTINGKEY","queueName ,false");
}
@Override
protected void doWork(String message) {
//去处理业务
String resultMessage = InfoService.getResultForSaveRiskInfo(message);
log.info("reslutmessage:"+resultMessage);
//然后要向别人反馈消息就发送,到某个ROUTINGKEY中去就好了。
super.publishMessage(resultMessage, "ROUTINGKEY_MESSANGFE");
}
}
总结
希望自己一点点的进步吧,这些代码之前被我也是分模块处理的,但是消费者还是比较的复杂,idea总是提醒有很多的重复的逻辑,所以想着怎么去重构他啊,每天都是进步哈哈,不断的去学习。