一、消息队列
MQ的选择
rabbitmq和kafka都支持高可用,持久化,但是kafka吞吐量性能更高一些,一般用于日志采集和流式计算,支持10万级别的吞吐量,rocketmq和rabbitmq支持万级。
MQ的使用目的
异步、消峰、解耦
RabbitMQ
AMQP messaging 中的基本概念
- Broker: 接收和分发消息的应用,RabbitMQ Server就是Message Broker。
- Virtual host: 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。
- Connection: publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。
- Channel: 如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。
- Exchange: message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。
- Queue: 消息最终被送到这里等待consumer取走。一个message可以被同时拷贝到多个queue中。
- Binding: exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。
交换机类型
- fanout类型
Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。 - direct类型
Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。(支持多重绑定,两个队列使用一个路由键,绑定一个交换机) - topic类型
Routing Key必须与Binding Key相匹配的时候才将消息传送给Queue
Rabbitmq可靠性
生产者可靠性保证
a. rabbitmq发送消息,如果不进行特殊设置(设置为事务模式或者确认模型),则生产者可靠性无法保证,默认rabbitmq不会返回任何信息给生产者。
b.事务模式的缺点,事务模式本身为同步机制,性能上比确认模式要差,确认模式为异步模式(与mqserver连接的信道也为双向信道)。
mandatory参数与备份交换机
mandatory和备份交换器一起使用,mandatory参数无效, 备份交换机的使用是为了减少生产者代码复杂度。用了mandatory参数就要增加addReturnListener监听器。
mandatory参数设置为true
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ProducerV5 {
static String EXCHANGE_NAME = "exchange_name";
static String QUEUE_NAME = "queue_name";
static String ROUTING_KEY = "root_key";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("****");
factory.setPassword("****");
factory.setVirtualHost("/");
factory.setHost("**.**.**.**");
factory.setPort(5672);
Connection conn = null;
final Channel channel;
try {
conn = factory.newConnection();
//注意conn不关闭时,程序不停止。
channel = conn.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, null);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
//当rabbimq无法将消息正确路由到队列时,触发。
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("replyCode:"+replyCode);
System.out.println("replyText:"+replyText);
System.out.println("exchange:"+exchange);
System.out.println("routingKey:"+routingKey);
String string = new String(body);
System.out.println("basic return"+string);
}
});
int i=1;
while (i<20){
String string = "hello world"+i;
//rabbitmq无法找到EXCHANGE_NAME时,会触发异常。无法找到queue时,mq会发送Basic.Return命令给生产者,addReturnListener进行监听。
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY+"uuuu", true, MessageProperties.PERSISTENT_TEXT_PLAIN,
string.getBytes());
i++;
System.out.println(i+"ddd");
}
System.out.println("dddfdafdf");
//channel.addReturnListener();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
conn.close();
}
System.out.println("end");
}
}
备份交换机
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import org.omg.CosNaming.NamingContextExtPackage.StringNameHelper;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
public class ProducerV5 {
static String EXCHANGE_NAME = "exchange_name_v2";
static String BEI_FEN_EXCHANGE_NAME = "bei_fen_exchange_name";
static String UN_ROUTED_QUEUE_NAME = "un_routed_exchange_name";
static String QUEUE_NAME = "queue_name";
static String ROUTING_KEY = "root_key";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("***");
factory.setPassword("***");
factory.setVirtualHost("/");
factory.setHost("**.**.**.**");
factory.setPort(5672);
Connection conn = null;
final Channel channel;
try {
conn = factory.newConnection();
channel = conn.createChannel();
Map<String,Object> argsProperties = new HashMap<String, Object>();
argsProperties.put("alternate-exchange", BEI_FEN_EXCHANGE_NAME);
channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, argsProperties);
channel.exchangeDeclare(BEI_FEN_EXCHANGE_NAME,"fanout", true, false, null);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueDeclare(UN_ROUTED_QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
channel.queueBind(UN_ROUTED_QUEUE_NAME, BEI_FEN_EXCHANGE_NAME, "");
int i=1;
while (i<20){
String string = "hello world"+i;
//此时路由不到队列时,消息发送到备份交换机。
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY+"uuuufff", true, MessageProperties.PERSISTENT_TEXT_PLAIN,
string.getBytes());
i++;
System.out.println(i+"ddd");
}
//channel.addReturnListener();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
conn.close();
}
System.out.println("end");
}
}
注意:备份交换机与普通交换机没有区别,发送到备份交换机的路由key与正常交换机一样。所以备份交换机建议设置为fanout。
confirm异步模式
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.TreeMap;
import java.util.concurrent.TimeoutException;
public class ProducerV3 {
static String EXCHANGE_NAME = "exchange_name";
static String QUEUE_NAME = "queue_name";
static String ROUTING_KEY = "root_key";
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("***");
factory.setPassword("****");
factory.setVirtualHost("/");
factory.setHost("10.12.24.104");
factory.setPort(5672);
Connection conn = null;
final Channel channel;
final TreeMap<Long, String> map = new TreeMap<Long, String>();
try {
conn = factory.newConnection();
channel = conn.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, null);
channel.exchangeDeclare(EXCHANGE_NAME+"dddd","direct", true, false, null);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
//设置mq为认证模式。
channel.confirmSelect();
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("replyCode:"+replyCode);
System.out.println("replyText:"+replyText);
System.out.println("exchange:"+exchange);
System.out.println("routingKey:"+routingKey);
String string = new String(body);
System.out.println("basic return"+string);
}
});
//rabbitmq发送Basic.Ack.(这里测试与书上说的不一样,并不是正确的投递到队列,才会发送Basic.Ack,自己测试得到的结果消息到达了交换机就会发送Ack,所以发送认证机制最好跟备份交换机或者madatory参数共同保证可靠性。Nack理解可能是rabbitmq因为自身错误,无法接收消息时,才发送Nack。)
channel.addConfirmListener(new ConfirmListener() {
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("handleAck"+deliveryTag);
map.remove(deliveryTag);
}
//mutiple为之前的消息是否正确投递
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("handleNack"+deliveryTag);
// 重新发送一条相同的消息。注意信道认证模式时,没发送到信道的消息都有唯一的deliveryTag.
String string = map.get(deliveryTag);
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, true, MessageProperties.PERSISTENT_TEXT_PLAIN,
string.getBytes());
map.clear(deliveryTag);
}
});
int i=1;
while (i<20){
String string = "hello world"+i;
long nextNo = channel.getNextPublishSeqNo();
//这里无法路由到正确的队里,仍然会触发ack.
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY+"gggggg", true, MessageProperties.PERSISTENT_TEXT_PLAIN,
string.getBytes());
i++;
map.put(nextNo, string);
}
System.out.println("dddfdafdf");
//channel.addReturnListener();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
confirm批量认证模式
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeoutException;
public class ProducerV4 {
static String EXCHANGE_NAME = "exchange_name";
static String QUEUE_NAME = "queue_name";
static String ROUTING_KEY = "root_key";
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("kreditplus_dev");
factory.setPassword("kreditplus_dev");
factory.setVirtualHost("/");
factory.setHost("10.12.24.104");
factory.setPort(5672);
Connection conn = null;
Channel channel = null;
try {
conn = factory.newConnection();
channel = conn.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, null);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
channel.confirmSelect();
ArrayList<String> arraylist = new ArrayList<String>();
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("replyCode:" + replyCode);
System.out.println("replyText:" + replyText);
System.out.println("exchange:" + exchange);
System.out.println("routingKey:" + routingKey);
String string = new String(body);
System.out.println("basic return" + string);
}
});
int i=1;
int mod = 0;
while (i<40) {
String string = "hello world" + i;
long nextNo = channel.getNextPublishSeqNo();
System.out.println("nextNo"+ nextNo);
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, true, MessageProperties.PERSISTENT_TEXT_PLAIN,
string.getBytes());
i++;
arraylist.add(string);
if (++mod >= 20) {
mod = 0;
try {
//批量认证模式
boolean value = channel.waitForConfirms();
if (!value) {
//重复发送
for (String string_v2 : arraylist) {
channel.basicPublish(EXCHANGE_NAME, "dadfa", true, MessageProperties.PERSISTENT_TEXT_PLAIN,
string_v2.getBytes());
}
System.out.println();
} else {
arraylist.clear();
System.out.println("success" + string);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
rabbitmq持久化可靠性保证
交换机,队列,消息持久化。
交换机持久化:保证重启时,交换机元数据不丢失。不持久化,则mq重启后,交换机无法继续使用。 不影响消息是否丢失。
队列持久化:队列不持久化时,重启后消息丢失。
消息持久化:发送消息时,deliveryMode设置为2,将消息持久化,如果只有队列持久化,没有消息持久化,重启后,消息会丢失。
镜像队列
消息持久化到磁盘时,可能需要在操作系统缓存保存一段时间,才能够存到物理磁盘中,这段时间如果rabbitmq挂掉,可能会丢失数据,可通过镜像队里进行保证。
消息的保存时间
消息保存时间有两个限制:
1.消息本身的的保存时间TTL和队列的保存时间TTL,消息的过期时间,取两者中小的那个,不设置TTL,则消息会等到消费后,才会从rabbitmq删除。
消费者可靠性保证
- 消费者可靠性保证主要利用手动确认机制
- 如果多个消费者订阅同一队列,rabbitmq采用轮训方式推送消息到消费者。
- channel.basicQos(2),控制了单个消费者,在信道上未确认的个数,channel.basixQos(10, true), global为true整体信道上的消费者都需要遵从这个限定值,(加和限定和单个限定有待实验)。
- 手动确认,basicNack 方法的第三个参数代表是否重回队列,如果你填 false 那么消息就直接丢弃了,相当于没有保障消息可靠。如果你填 true ,当发生消费报错之后,这个消息会被重回消息队列顶端,继续推送到消费端,继续消费这条消息。通常需要手动处理这条消息。
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerTestV2 {
static String QUEUE_NAME = "queue_name";
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("10.12.24.104");
factory.setPort(5672);
factory.setUsername("kreditplus_dev");
factory.setPassword("kreditplus_dev");
//factory.setVirtualHost("/");
Connection connection = null;
try {
connection = factory.newConnection();
final Channel channel = connection.createChannel();
//设置客户端最多接收未被ack的消息的个数, 起到了一个滑动窗口的作用,channel.basicQos还有针对一个消费者和多个消费者的区别。
channel.basicQos(2);
System.out.println("aaaaaa");
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, "myConsumerTagV2",
new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException
{
String routingKey = envelope.getRoutingKey();
String contentType = properties.getContentType();
long deliveryTag = envelope.getDeliveryTag();
// (process the message components here ...)
System.out.print("aaaaaavdadfadsa"+new String(body)+deliveryTag);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 手动确认
channel.basicAck(deliveryTag, false);
}
});
// channel.close();
// connection.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
}
}
}
4.死信队列
消息在一个队列中变成死信,能够过重新被发送到一个交换机DLX,
绑定DLX的队列就称之为死信队列。
消息变成死信的情况:
- 消息被拒绝
- 消息过期
- 队列达到最大长度。
生产者
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import com.rabbitmq.client.AMQP.BasicProperties;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
public class Producer {
static String EXCHANGE_NAME = "exchange_name";
static String QUEUE_NAME = "queue_name_v2";
static String ROUTING_KEY = "root_key";
static String DEAD_DLX_EXCHANGE = "dlx_exchange";
static String DEAD_QUEUE = "dlx_queue";
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("kreditplus_dev");
factory.setPassword("kreditplus_dev");
factory.setVirtualHost("/");
factory.setHost("10.12.24.104");
factory.setPort(5672);
Connection conn = null;
Channel channel = null;
try {
conn = factory.newConnection();
channel = conn.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, null);
//dead exchange
channel.exchangeDeclare(DEAD_DLX_EXCHANGE,"direct", true, false, null);
Map<String,Object> ars= new HashMap<String,Object>();
ars.put("x-dead-letter-exchange", DEAD_DLX_EXCHANGE);
//也可以为这个DLX指定路由键,如果没有特殊指定,则使用原队列的路由键
ars.put("x-dead-letter-routing-key","dlx-routing-key");
//给正常队列绑定死信交换机
channel.queueDeclare(QUEUE_NAME, true, false, false, ars);
channel.queueDeclare(DEAD_QUEUE, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
//死信交换机、死信队列、路由key绑定
channel.queueBind(DEAD_QUEUE, DEAD_DLX_EXCHANGE, "dlx-routing-key");
int i=10;
while (i<20){
String string = "hello world"+i;
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, new BasicProperties.Builder().deliveryMode(2).build(), //设置为持久化
string.getBytes());
i++;
}
channel.close();
conn.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
System.out.println("aaaaaa");
}
}
消费者
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerTest {
static String QUEUE_NAME = "queue_name";
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("10.12.24.104");
factory.setPort(5672);
factory.setUsername("kreditplus_dev");
factory.setPassword("kreditplus_dev");
//factory.setVirtualHost("/");
Connection connection = null;
try {
connection = factory.newConnection();
final Channel channel = connection.createChannel();
//设置客户端最多接收未被ack的消息的个数
channel.basicQos(3);
System.out.println("aaaaaa");
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, "myConsumerTag",
new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException
{
String routingKey = envelope.getRoutingKey();
String contentType = properties.getContentType();
long deliveryTag = envelope.getDeliveryTag();
// (process the message components here ...)
System.out.println("aaaaaavdadfadsa"+new String(body));
System.out.println(deliveryTag);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//重点是这里最后一个参数决定了是否让消息重新回到队里,还是转发到死信队列中。
channel.basicNack(deliveryTag, false, false);
//channel.basicAck(deliveryTag, false);
}
});
// channel.close();
// connection.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
}
}
}
延迟队列
延迟队列是在死信队列的基础上实现的,可通过消息的TTL来控制,生产者消息生产时,设置死信队列和消息TTL时间,消息过期进入到死信队列。 消费者消费死信队列保证了延迟功能。
幂等性
生产者给每条消息设置一个唯一uuid。
生产者
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import com.rabbitmq.redis.RedisPool;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.io.IOException;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeoutException;
public class ProducerV6 {
static String EXCHANGE_NAME = "exchange_name";
static String QUEUE_NAME = "queue_name";
static String ROUTING_KEY = "root_key";
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("/");
factory.setHost("192.168.0.17");
factory.setPort(5672);
Connection conn = null;
final Channel channel;
final TreeMap<Long, String> map = new TreeMap<Long, String>();
try {
conn = factory.newConnection();
channel = conn.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, null);
channel.exchangeDeclare(EXCHANGE_NAME+"dddd","direct", true, false, null);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
channel.confirmSelect();
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("replyCode:"+replyCode);
System.out.println("replyText:"+replyText);
System.out.println("exchange:"+exchange);
System.out.println("routingKey:"+routingKey);
String string = new String(body);
System.out.println("basic return"+string);
}
});
channel.addConfirmListener(new ConfirmListener() {
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("handleAck"+deliveryTag);
map.remove(deliveryTag);
}
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("handleNAck"+deliveryTag);
map.remove(deliveryTag);
// string need has primary key,
// repeat send
String string = map.get(deliveryTag);
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, true, MessageProperties.PERSISTENT_TEXT_PLAIN,
string.getBytes());
}
});
//这里为了做实验保证幂等性,实际corrId每条消息唯一。
String corrId = "10389234451254177779900";
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.deliveryMode(2)
.correlationId(corrId)
.build();
int i=1;
while (i<20){
String string = "hello world"+i;
long nextNo = channel.getNextPublishSeqNo();
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, false, props,
string.getBytes());
i++;
map.put(nextNo, string);
}
System.out.println("dddfdafdf");
//channel.addReturnListener();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
消费者避免重复消费。
消息重复发送的原因有,重试,生产者重复发送等,需要自己在消费者端保证消息的唯一性。
方法有
- 利用redis的setnx命令判断是否已经消费过,缓存时间的设置,要看内存的大小,及消息重复发送的时间间隔进行评估。
- 利用mysql的唯一健,但是高并发的情况下,对是数据库压力较大。
消费失败的处理 - 手动确认,basicNack 方法的第三个参数代表是否重回队列,如果你填 false 那么消息就直接丢弃了,相当于没有保障消息可靠。如果你填 true ,当发生消费报错之后,这个消息会被重回消息队列顶端,继续推送到消费端,继续消费这条消息。通常代码的报错并不会因为重试就能解决,所以这个消息将会出现这种情况:继续被消费,继续报错,重回队列,继续被消费…死循环。
所以真实的场景一般是:
当消费失败后将此消息存到 Redis,记录消费次数,如果消费了三次还是失败,就丢弃掉消息,记录日志落库保存
直接填 false ,不重回队列,记录日志、发送邮件等待开发手动处理。
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import com.rabbitmq.redis.RedisPool;
import redis.clients.jedis.Jedis;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerTestV3 {
static String QUEUE_NAME = "queue_name";
static Jedis jedis = RedisPool.getJedis();
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.0.17");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
//factory.setVirtualHost("/");
Connection connection = null;
try {
connection = factory.newConnection();
final Channel channel = connection.createChannel();
//设置客户端最多接收未被ack的消息的个数
channel.basicQos(3);
System.out.println("aaaaaa");
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, "myConsumerTag",
new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException
{
String routingKey = envelope.getRoutingKey();
String contentType = properties.getContentType();
long deliveryTag = envelope.getDeliveryTag();
String uuid = properties.getCorrelationId();
System.out.println(uuid);
//缓存超时时间需要根据业务指定
if(jedis.set(uuid,"success","NX","PX",30000)!=null){
// (process the message components here ...)
System.out.println(new String(body));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
channel.basicNack(deliveryTag, false, false);
//channel.basicAck(deliveryTag, false);
}
});
// channel.close();
// connection.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
}
}
}
Rabbitmq的顺序性
rabbitmq的顺序性实现的话核心原理
- 按顺序的进入队列
- 按顺序的处理消息
- 按顺序进入队列需要保证,生产者唯一,发送消息时保证顺序,比如数据1,数据2,数据3,按顺序发送到同一队列。
- 消费者处理消息时,处理数据1,数据2,数据3时,核心是保证一个消费者线程处理数据1,数据2,数据3.,如果多个消费者,或者有重试情况。个人理解需要结合共享内存和消息特点(序号顺序,时间戳等进行处理)。
Rabbitmq消费消息的两种模式
推模式
- 推模式接收消息是最有效的一种消息处理方式。channel.basicConsume(queneName,consumer)方法将信道(channel)设置成投递模式,直到取消队列的订阅为止;在投递模式期间,当消息到达RabbitMQ时,RabbitMQ会自动地、不断地投递消息给匹配的消费者,而不需要消费端手动来拉取,当然投递消息的个数还是会受到channel.basicQos的限制。
- 推模式将消息提前推送给消费者,消费者必须设置一个缓冲区缓存这些消息。优点是消费者总是有一堆在内存中待处理的消息,所以当真正去消费消息时效率很高。缺点就是缓冲区可能会溢出。
- 由于推模式是信息到达RabbitMQ后,就会立即被投递给匹配的消费者,所以实时性非常好,消费者能及时得到最新的消息。
拉模式
- 如果只想从队列中获取单条消息而不是持续订阅,则可以使用channel.basicGet方法来进行消费消息。
- 拉模式在消费者需要时才去消息中间件拉取消息,这段网络开销会明显增加消息延迟,降低系统吞吐量。
- 由于拉模式需要消费者手动去RabbitMQ中拉取消息,所以实时性较差;消费者难以获取实时消息,具体什么时候能拿到新消息完全取决于消费者什么时候去拉取消息。
Rabbitmq的消息堆积
- 监控报警
- 新增消费者
- 调查消费瓶颈
高可用性
rabbitmq支持三种模式搭建:
一是单机模式,生产环境不常用。
二是普通集群模式,这种情况创建队列只会在一个节点上,其他的rabbitmq实例会同步这个queue的元数据(队里所在的位置),如果消费者连接队列所在的mq实例宕机了,则服务不可用,其他消费者也消费不到数据。连接其他实例获取数据,也需要从队列所在的mq实例拉取数据,会导致内部产生大量数据传输,可用性无保障。
三是镜像集群模式,指的是将队列做成镜像队列,队里的原数据和消息都会存在到镜像队列中。采用的是主从复制原理,主从复制指的是读从,写主,这种模式下才能保证mq的高可用,当主实例挂掉时,选取最早创建的从实例变为主实例。 集群模式连接是使用的代理控制。
1.集群同步4中元数据
a. 队列元数据:队列名称和它的属性
b. 交换器元数据:交换器名称、类型和属性
c. 绑定元数据:一张简单的表格展示了如何将消息路由到队列
d. vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性
abbitmq集群采用原数据同步的原因
第一,存储空间。如果每个集群节点都拥有所有Queue的完全数据拷贝,那么每个节点的存储空间会非常大,集群的消息积压能力会非常弱(无法通过集群节点的扩容提高消息积压能力);
第二,性能。消息的发布者需要将消息复制到每一个集群节点,对于持久化消息,网络和磁盘同步复制的开销都会明显增加。
客户端连接队列所在节点
a. 如果有一个消息生产者或者消息消费者通过amqp-client的客户端连接至节点1进行消息的发布或者订阅,那么此时的集群中的消息收发只与节点1相关。
b.客户端连接的是非队列数据所在节点
非数据所在节点只会起到一个路由转发的作用,最终负责存储和消费的消息还是会在队列数据真正所在的节点上。
集群节点类型
磁盘节点:持久化存储原数据,如果是单点部署,那么一定是磁盘节点,如果磁盘节点挂掉,则不能创建队列,交换器等。
内存节点:将配置信息和原数据信息保存在内存中,通常负责整个集群与客户端的连接。
负载均衡模式
下图未将磁盘节点和内存节点进行区分,可理解为节点均需持久化原数据。
keepalived机制利用(虚拟路由冗余协议),已软件的形式实现服务的热备功能,(通常是两台linux服务器组成热备组,master和backup,同一时间只有master对外服务,master会虚拟出一个ip地址,简称VIP,这个VIP只存在于master上对外服务。如果keepalived检测到master故障,备份服务器backup自动接管VIP并成为master. keepalived将原master移除,当源master恢复后,会自动加入到热备组,默认再抢占,成为master。
rabbitmq镜像队列
镜像队列是基于普通的集群模式的,然后再添加一些策略,所以还是得先配置普通集群,然后才能设置镜像队列。镜像队列存在于多个节点。要实现镜像模式,需要先搭建一个普通集群模式,在这个模式的基础上再配置镜像模式以实现高可用。
镜像队列结构如下:
所有对mirror_queue_master的操作,会通过可靠组播GM的方式同步到各slave节点。
GM负责消息的广播,mirror_queue_slave负责回调处理,而master上的回调处理是由coordinator负责完成。mirror_queue_slave中包含了普通的BackingQueue进行消息的存储,master节点中BackingQueue包含在mirror_queue_master中由AMQQueue进行调用。
整体流程个人理解:mirror_queue_master当收到操作时,触发master对应的GM,GM收到消息并进行回调Coordinator. 同时AMQQueue也会调用BackingQueue进行master消息的持久化,
master的GM通过链表的方式传递消息,当slave的GM收到消息时,回调mirror_queue_slave进行处理,将消息持久化到或镜像队列所在BackingQueue。 此时可保证master节点在持久化数据时,数据还未刷到磁盘中,挂掉了,可通过镜像队列保证可靠性。
GM
GM模块实现的一种可靠的组播通讯协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到。
它的实现大致如下:
将所有的节点形成一个循环链表,每个节点都会监控位于自己左右两边的节点,当有节点新增时,相邻的节点保证当前广播的消息会复制到新的节点上;当有节点失效时,相邻的节点会接管保证本次广播的消息会复制到所有的节点。在master节点和slave节点上的这些gm形成一个group,group(gm_group)的信息会记录在mnesia中。不同的镜像队列形成不同的group。消息从master节点对于的gm发出后,顺着链表依次传送到所有的节点,由于所有节点组成一个循环链表,master节点对应的gm最终会收到自己发送的消息,这个时候master节点就知道消息已经复制到所有的slave节点了。
新增节点:
每当一个节点加入或者重新加入(例如从网络分区中恢复过来)镜像队列,之前保存的队列内容会被清空。
节点的失效
slave节点失效,系统处理做些记录外几乎啥都不做。
master节点失效:
1.与客户端连接全部断开
2.选取最老的slave节点作为master, 如果此时slave为同步master数据,则数据丢失。
3.新的master重新入队列所有为没有ack的消息,此时客户端可能会有重复消息。
kafka
Kafka基础架构
生产者
- 生产者有主线程和sender线程。
a. 主线程负责将消息,进行拦截,序列化、分区,分区器查看消息ProducerRecord的partition字段,分配分区,如果没有partition,则将key进行hash计算,然后计算分区,如果key为null,则通过轮训方式分配分区。
b. 主线程有消息累加器,将ProducerRecord存储为一个ProducerBatch。
c. Sender线程负责从消息累加器读取数据,然后封装消息为<Node, Request>的形式,发送到各个分区。其中发送方式利用的是selector,(一直轮训)。 - 生产者可靠性保证。
- ack=0,失败异常不会进行重试。
- 同步发送利用的是ProducerRecord.wait进行阻塞,处理完成回释放countDownLaunch,由于ack=0, request关联的response的body为null,不会等待broker反回,(其他需要等待有返回才能进行完成处理),selector处理已经完成的请求时,会countdownLaunch.countDown(同步的话,进行释放)。完成请求处理。
- 当ack为1或者-1时,如果发生异常,则进行重试。
- 源码整理
a.kafka如何保证消息可靠性
生产者:发送消息服务端返回ack有三种等级。ack=0,生产者发送完消息,就认为发送成功。
ack=1, 分区leader接收到消息并写入分区返回确认ack=1,仍有丢失消息的可能,未同步到follower节点,leader节点挂掉。ack=-1.在所有副本都同步完成后。leader通知生产者ack=-1。注意由leader返回相应消息。 异步:如果调整为异步模式,则有极大的可能丢失数据,因为kafka异步发送消息,会先将消息放到缓存队列里,有个定时将数据推送出去,在缓存队列这段时间,如果生产者挂掉,则消息丢失。
kafak本身通过多副本保证消息可靠。
消费者:关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性。
b.kafka保证消息一致性(leader和follower消息一致):
High Water Mark 机制。leader 的HW值也就是实际已提交消息的范围,每个replica都有HW值,但仅仅leader中的HW才能作为标示信息。什么意思呢,就是说当按照参数标准成功完成消息备份(成功同步给follower replica后)才会更新HW的值,代表消息理论上已经不会丢失,可以认为“已提交”。
一致性定义:若某条消息对client可见,那么即使Leader挂了,在新Leader上数据依然可以被读到HW-HighWaterMark: client可以从Leader读到的最大msg offset,即对外可见的最大offset, HW=max(replica.offset)对于Leader新收到的msg,client不能立刻消费,Leader会等待该消息被所有ISR中的replica同步后,更新HW,此时该消息才能被client消费,这样就保证了如果Leader fail,该消息仍然可以从新选举的Leader中获取。
对于来自内部Broker的读取请求,没有HW的限制。同时,Follower也会维护一份自己的HW,Follower.HW = min(Leader.HW, Follower.offset)
c.kafka的保证leader负载均衡
kafka一个topic具有多个分区能够实现负载均衡,当kafka的一个分区的leader挂掉后,会从ISR(每个分区的 leader 会维护一个 ISR 列表(针对每个分区一个ISR),ISR 列表里面就是 follower 副本的 borker 编号)选择第一个最新的broker作为leader节点。leader指的是broker节点。负载均衡避免了一个作为leader节点的broker管理多个分区。
d.ISR、OSR、AR 是什么
ISR:In-Sync Replicas 副本同步队列
OSR:Out-of-Sync Replicas
AR:Assigned Replicas 所有副本
ISR是由leader维护,follower从leader同步数据有一些延迟(具体可以参见 图文了解 Kafka 的副本复制机制),超过相应的阈值会把 follower 剔除出 ISR, 存入OSR(Out-of-Sync Replicas )列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR。这些信息都放到了zk中。
e.kafka分区的分配策略
同一个 Consumer Group 内新增消费者
消费者离开当前所属的Consumer Group,包括shuts down 或 crashes
订阅的主题新增分区
当发j生上面3中情况时,kafka会对分区重新进行分配。
kafka存在两种分区策略range和RoundRobin。
range策略:
将topic的分区进行排序并编号,除消费者的数量,11/3 一个消费者消费0.1.2,3 第二个消费者消费4,5,6,7 第三个消费8,9,10
RoundRobin strategy策略:
两个前提
1)同一个Consumer Group里面的所有消费者的num.streams必须相等;
2)每个消费者订阅的主题必须相同。
策略:RoundRobin策略的工作原理:将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序,然后通过轮询方式逐个将分区以此分配给每个消费者。如果不满足上面的策略,则会造成,消费者会消费多个分区。
https://blog.csdn.net/u013256816/article/details/81123625
d.kafka速度为什么快
1.支持顺序读写,零copy,数据不经过用户态,在内核态进行内存映射,批量处理。
2.生产者,写消息到leader节点会有cache缓存,单独启进程将消息刷到磁盘,批量处理。
3.消费者,顺序消费fowller,顺序读写磁盘, 零copy(数据不经过用户态,在内核态进行内存映射, 文件io到socket的io)
e.kakfa消费顺序性
https://www.jianshu.com/p/02fdcb9e8784
消息写到一个分区中,由一个消费者消费,消费者内部要开多线程的话,将消息分发到一个内存队列中,由一个线程进行消费
3.区别
kafka的broker无状态,不保留消费者消费到那个offset。
rabbitmq的broker会保留,消费者的确认到的消息信息。