1 mq就是消息队列 其实就是一个队列 先进先出 只不过存放的是消息而已 是一种跨进程的通信机制 用于上下游传递消息 RabbitMq是常见的上下游解耦的消息通讯服务使用了MQ只有发送消息就只依赖于MQ不依赖于其它服务
使用mq的解释 就是 流量削峰 异步处理 应用解耦
流量削峰就是 比如说我一个系统能处理1w条消息 但是现在有两万条消息 那就用消息队列来 处理 可能处理某些用户的时候慢一些 但是相比于消息丢失 这种处理费方式还是相当合理的
异步处理 比如说 A调用B 但是B的执行需要非常长的一段时间 A不知道 B什么时候执行完任务 那么 就可以A把消息放在消息队列中 然后 直接向对象响应 已经执行成功 然后B再从消息队列中拿到这条消息去执行 整个过程就是一个异步的过程 当B处理完之后 发送消息给MQ 然后MQ再把这条消息发送给A 同时A还可以执行其它的事情 整个过程做到了异步执行
应用解耦 本来我们有很多的服务 然后执行逻辑的时候调用各个服务 比如一个服务挂掉之后就执行不了我们的逻辑 用户这个操作也就作废了 但是如果我们 使用 消息队列的话就如果哪个服务挂掉之后 这条消息就不被消费 然后 等我们把服务修好之后也可以执行完这条逻辑 这就是应用之间的解耦
那么RabbitMQ是采用erlang语言开发的 一款高性能 吞吐量能够达到万级 erlang语言有高并发性 MQ健壮 稳定 跨平台 支持多种语言 性能好时效性微秒级
kafka 大公司使用 高吞吐量 经常与大数据联系在一起 经常用于日志收集业务首选kafka
RocketMq阿里巴巴公司的 可以抗住双11大流量 电商系统经常使用(金融互联网)
把rabbitMq可以看做是一个快递驿站 生产者是商家 消费者是用户 中转就是通过连接管道
与快递点不同的就是它不是处理 而是 接收 存储 转发的功能 不做处理
交换机 与队列之间是绑定的关系 交换机 就是接收用户的消息 然后推送到队中去
不同的交换机有不同的作用
直接路由交换机 将消息传递到指定的路由键匹配的队列中去
主体路由交换机 : 将消息根据路由模式匹配的路由键发送到与之匹配的队列中去 可以进行通配符进行模式匹配
广播路由交换机 : 转发到所有和它绑定的队列中
头部路由 根据头部进行匹配并将消息发送到匹配的队列
队列受到主机的内存和磁盘的约束本质上是一个很大的内存缓冲区
实现一个发布者
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
/*
1 队列名称
2 是否需要持久化
3 是否可以进行消息共享
4 是否自动删除
*/
channel.queueDeclare(QUEUE_NAME, false,false,false, null );
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.next();
/*
1 发送到哪个交换机
2 路由的key是哪个 本次队列的名称
3 其它参数信息
4 发送的消息体
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("发送消息完成:"+ message);
}
}
封装了一个RabbitMqUtils 创建一个队列 然后给队列中发送消息 然后再写个消费者消费一下
public final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
DeliverCallback deliverCallback = (consumeTag, message) ->{
System.out.println(new String(message.getBody()));
};
CancelCallback cancelCallback = consumerTag->{
System.out.println("中断");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
消费同一个队列中的消息 消费的时候有四个参数 第一个是消费的队列 然后 是是否自动应答 然后再一个回调就是成功之后的回调 最后一个就是失败之后的回调
消息应答机制
这个机制就是 我接收到消息之后应答 分为自动应答和手动应答 自动应答有缺点 容易丢失消息 比如我一个消费者 如果用自动应答的话 我只要拿到一条消息我就应答一次 那么就会有问题 消费者万一挂机了 然后还自动应答 mq就会认为你已经把这条消息处理了 其实还没有处理完成 我挂机了 这条消息也被mq删除了 那么就造成了消息丢失的场景
手动应答 : 我在收到消息 并且把该执行的逻辑执行完成之后 再给你应答 发送
chnnel.basicAck();
用于确认应答 还有
chnnel.basicNack();
表示否定应答
chnnel.basicReject();
而这种表示否定应答并且删除消息
而chnnel.basicAck(var1, var2); var1表示编号 var2 表示是否自动应答全部 如果是true就是自动应答全部 如果是false的话就是只应答当前管道内执行的消息 如果用true的话就把还没有执行的消息也一起应答了
消息重新入队
消息重新入队: 指的是消费者由于某些原因 挂了 那么他拿走的消息导致没有发送ack给mq 那么这条消息就会重新入队 即使消费者把消息拿走了 然后挂了也可以被其它消费者消费 消息也不会丢失
持久化机制
持久化指的是把队列和消息都进行持久化 持久化队列非常简单 直接改队列参数 改成true就持久化队列
要想让消息实现持久化需要在消息生产者修改码,MessageProperties,PERSISTENT_TEXT_PLAIN添加这个属性。将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉RabbitMQ将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要更强有力的持久化策略 比如发布确认
发布确认 有三种情况 单条消息发布确认 分段确认 或者也叫批量发布确认 和异步发布确认
单条发布确认:
channel.confirmSelect(); 这个语句就是开启发布确认de
然后直接 channel.waitForConfirms();
这个就是单条消息发布确认 发送一条消息就会等待mq返回的ack来确认消息是否发布
批量确认方式就是每发布100条才确认一次但是我并不知道哪一次成功了 哪一次失败了
那么就要用异步确认 虽然有一点点复杂 但是一般都是使用的是异步发布确认
需要准备一个消息监听发布器
channel.addConfirmListener(ackCallback,nackCallback);
第一个ack就是确认发布的 第二是失败的 两个回调 也是非常简单的 需要用一个map把我们发布的消息存起来 然后 发布成功的就从我们的map中去掉 那么剩下的就是发布失败的 我们可以选择重新发布 还是其他的处理策略
public class ComfirmMessage {
// 批量发消息的个数
public static final int MESSAGE_COUNT = 1000;
public static void main(String[] args) throws Exception {
//3、异步批量确认
// 发布1000个异步确认消息,耗时36ms
ComfirmMessage.publicMessageAsync();
}
public static void publicMessageAsync() throws Exception{
Channel channel = RabbitMqUtils.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
// 开启发布确认
channel.confirmSelect();
// 开始时间
long begin = System.currentTimeMillis();
// 消息确认成功回调函数
ConfirmCallback ackCallback = (deliveryTag,multiply) -> {
System.out.println("确认的消息:"+deliveryTag);
};
// 消息确认失败回调函数
/*
* 参数1:消息的标记
* 参数2:是否为批量确认
* */
ConfirmCallback nackCallback = (deliveryTag,multiply) -> {
System.out.println("未确认的消息:"+deliveryTag);
};
// 准备消息的监听器,监听哪些消息成功,哪些消息失败
/*
* 参数1:监听哪些消息成功
* 参数2:监听哪些消息失败
* */
channel.addConfirmListener(ackCallback,nackCallback);
// 批量发送消息
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = "消息" + i;
channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
}
// 结束时间
long end = System.currentTimeMillis();
System.out.println("发布"+MESSAGE_COUNT+"个异步确认消息,耗时"+ (end - begin) + "ms");
}
}
不公平分发
能者多劳 采用不公平分发 我有一个消费者消费的很慢但是又一个消费者 消费的很快 那么就让消费能力快的消费者多处理 消费慢的少处理 要不然 默认是轮训 快的要等待慢的执行完之后再执行 就会有资源浪费 如果 不公平分发可以提高执行效率 设置 channel.basicQos(1)
预期值
本身消息的发送就是异步发送的,所以在任何时候,channel上肯定不止只有一个消息另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用basic.gos.方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,例如,假设在通道上有未确认的消息5、6、7,8,并且通道的预取计数设置为4,此时RabbitMQ.将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被ack。比方说tag=6这个消息刚刚被确认ACK,RabbitMQ将会感知这个情况到并再发送一条消息。消息应答和QoS预取值对用户吞吐量有重大影响。通常,增加预取将提高向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理的消息的数量也会增加,从而增加了消费者的RAM消耗(随机存取存储器)应该小心使用具有无限预处理的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同100到300范围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为1是最保守的。当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境中。对于大多数应用来说,稍微高一点的值将是最佳的
预期值的设置也是比较简单 把不公平分发的值设置为大于1 就可以设置 比如 设置成2 的话就是可以预期两个消息在同一个消息管道中
交换机
我们在发送给rabbitmq的消息并不会直接放在队列中 而是会先给交换机 然后再给队列 交换机的工作内容也是非常简单 只有接收消息和转发消息 两种 至于交换机必须确切知道如何处理收到的消息 是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们 这就的由交换机的类型来决定
交换机总共有四个类型 direct (直接) 主题(topic) 标题 (headers) 扇出 (fanout) 无名交换机 默认交换 比如在前面使用队列的时候 第一个参数是空的 就表示无名交换机
临时队列 每次我们创建队列的时候都需要一个全空的队列为此 我们可以创建一个随机名字的队列 或者让服务器给我们去选择一个随机名称队列 一旦我们的消费者断开连接之后就可以删除队列
创建临时队列的语句 :
String queueName = channel.queueDeclare().getQueue();
binding 就是绑定 把队列和交换机进行绑定
public class Emitlog {
// 交换机的名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.FANOUT);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发出的消息:"+ message);
}
}
}
生产者 通过扇出 的方式 消费者 写法
public class ReceiveLogs01 {
//交换机名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//声明一个队列,名称随机,当消费者断开与队列的连接时,队列自动删除
String queueName = channel.queueDeclare().getQueue();
//绑定交换机与队列
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("等待接受消息,把接受到的消息打印在屏幕上...");
DeliverCallback deliverCallback = (consumerTag,message) -> {
System.out.println("ReceiveLogs01控制台打印接受到的消息:" + new String(message.getBody()));
};
channel.basicConsume(queueName,true,deliverCallback,consumerTag -> {});
}
}
消费者2
public class ReceiveLogs02 {
//交换机名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//声明一个队列,名称随机,当消费者断开与队列的连接时,队列自动删除
String queueName = channel.queueDeclare().getQueue();
//绑定交换机与队列
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("等待接受消息,把接受到的消息打印在屏幕上...");
DeliverCallback deliverCallback = (consumerTag,message) -> {
System.out.println("ReceiveLogs02控制台打印接受到的消息:" + new String(message.getBody()));
};
channel.basicConsume(queueName,true,deliverCallback,consumerTag -> {});
}
}
这样 结果就是消费者1
和消费者 2 都可以获取到这条消息 相当于广播
第二种
Topic
就是通配符匹配 如果匹配通配符的话 就发送给队列 如果两个通配符同时匹配 消息只发送一次
生产者
public class FanoutDemo {
private final static String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 声明一个队列 名称随机 当有连接断开的时候 队列就消失了
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.HEADERS);
HashMap<String, String> map = new HashMap<>();
map.put("quick.orange.rabbit","被队列Q1Q2接收到");
map.put("quick.orange.fox","被队列Q1接收到");
map.put("lazy.brown.fox","被队列Q2接收到 ");
map.put("lazy.pink.rabbit","虽然满足队列Q2的两个绑定但是只会被接收一次");
map.put("quick.orange.male.rabbit","四个单词不匹配任何绑定会被丢弃");
for (Map.Entry<String, String> stringStringEntry : map.entrySet()) {
String routingKey = stringStringEntry.getKey();
String message = stringStringEntry.getValue();
channel.basicPublish(EXCHANGE_NAME, routingKey, null,message.getBytes("UTF-8"));
}
}
}
消费者1
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare("Q2", false, false, false, null);
channel.queueBind("Q2", EXCHANGE_NAME, "*.*.rabbit");
channel.queueBind("Q2", EXCHANGE_NAME, "*lazy.#");
System.out.println("等待接收消息:..........");
channel.basicConsume("Q2", true, (var1, var2)->{
System.out.println("接收到的消息是: "+ new String(var2.getBody()));
},(var1)->{});
}
}
消费者2
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare("Q1", false, false, false, null);
channel.queueBind("Q1", EXCHANGE_NAME, "*.orange.*");
System.out.println("等待接收消息:..........");
channel.basicConsume("Q1", true, (var1, var2)->{
System.out.println("接收到的消息是: "+ new String(var2.getBody()));
},(var1)->{});
}
}
第三种就是direct的方式
直接转发
名称匹配
public class DirectLogs {
// 交换机的名称
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"info",null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发出的消息:"+ message);
}
}
}
消费者1
private final static String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue, EXCHANGE_NAME, "");
System.out.println("等待接收消息:..........");
channel.basicConsume(queue, true, (var1, var2)->{
System.out.println("接收到的消息是: "+new String(var2.getBody()));
},(var1)->{});
}
private final static String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue, EXCHANGE_NAME, "");
System.out.println("等待接收消息:..........");
channel.basicConsume(queue, true, (var1, var2)->{
System.out.println("接收到的消息是: "+ new String(var2.getBody()));
},(var1)->{});
}
结果就是和发送消息匹配的才可以推送到队列中去
死信队列
造成死信队列总共有三种原因 1 队列已满 2 被拒绝 3 ttl过期 都会造成死信队列
死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer将消息投递到 broker或者直接到queue里了,consumer 从 queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效
这就是死信队列的逻辑过程
死信队列的实现还是比较简单的
public class Customer1 {
//普通交换机名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
public static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列名称
public static final String NORMAL_QUEUE = "normal_queue";
//死信队列名称
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
HashMap<String, Object> map = new HashMap<>();
map.put("x-message-ttl", 1000);
map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
map.put("x-dead-letter-routing-key","lisi");
channel.queueDeclare(NORMAL_QUEUE, false, false, false, null);
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE,"zhangsan");
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
System.out.println("等待接收消息>>>>>>>>>>>>>");
channel.basicConsume(NORMAL_QUEUE, true, (var, var2)->{
System.out.println("消息内容为 :"+new String(var2.getBody()));
},(var)->{
System.out.println(var);
});
}
}
死信队列
public class Customer2 {
//死信队列名称
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
System.out.println("等待接收消息>>>>>>>>>>>>>");
channel.basicConsume(DEAD_QUEUE, true, (var, var2)->{
System.out.println("消息内容为 :"+new String(var2.getBody()));
},(var)->{
System.out.println(var);
});
}
}
生产者
public class Producer {
private final static String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
AMQP.BasicProperties build = new AMQP.BasicProperties().builder().expiration("10000").build();
for (int i = 0; i < 10; i++) {
String mes = "第" + i + "条消息";
channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", build, mes.getBytes("UTF-8"));
}
}
}
第一种是因为 ttl过期导致的
第二种是因为 队列达到最大长度
第三种是因为消息被拒绝 代码都差不多 主要学的是一个思想 第三种 要关闭手动应答
延迟队列
延迟队列的实现有两种情况 第一种就是通过死信队列实现的 第二种就是通过延迟队列的插件来写的 当死信队列实现延迟队列的时候 就是等待普通队列的消息时间过期了 然后加入到死信队列中
这样就可以形成延迟队列的效果 但是非常不理想 就是比如我有一个20秒的过期时间的队列 另外一个队列的过期时间为2秒 那么它就会先执行20秒的那个任务 然后再往死信队列中加入那个两秒的任务 就是说 每次只能处理一个任务来加入死信队列 如果有下一个任务的话就得等上一个任务执行完成之后才可以 加入死信队列中 那么第二种的实现方式就是延迟队列的插件 即 rabbitmq实现了一个延迟队列的插件 这个插件就可以实现延迟队列的一个场景 这个插件就可以避免使用死信队列来实现延迟队列的那种场景 源码应该是按时间排序的 过期时间小的就先运行 相当于并发的实现 都可以进行进入延迟队列 那么就不需要等待第一个运行完之后再运行第二个的这种情况 唯一不同点就是可以设置延迟时间来操作
msg.getMessageProperties().setDelay(delayTime);
这样发送消息的时候就可以指定延迟的时间这样实现的时候只需要一个交换机和一个队列 然后就可以实现延迟队列了 延迟队列还有很多种选择 比如说 java中自带的DelayQueue可以实现 redis也可以实现延迟队列 或者kafka的时间轮也可以实现延迟队列 具体场景具体用法 复习一下redis延迟队列的实现
@Data
public class MassMailTask {
private Long taskId;
private Date startTime;//定时 调整
}
先定义一个工具类 来放加入的时间和编号
@Service
@Slf4j
public class MassMailTaskService {
@Resource
private RedisUtil redisUtil;
private static final String MASS_TASK_kEY = "massTaskMail";
public void pushMassMailTaskQUeue(MassMailTask massMailTask){
Date startTime = massMailTask.getStartTime();
if(startTime == null) {
return;
}
if(startTime.compareTo(new Date()) <= 0){ //当前的startTime小于等于这个当前的时间的话就是
return;
}
log.info("定时任务加入当前队列, massTask:{}", JSON.toJSONString(massMailTask));
redisUtil.zAdd(MASS_TASK_kEY,massMailTask.getTaskId().toString(),startTime.getTime());//zSet key value score值
}
public Set<Long> poolMassTaskQueue(){
Set<String> taskIdSet = redisUtil.rangeByScore(MASS_TASK_kEY, 0, System.currentTimeMillis());
if(CollectionUtils.isEmpty(taskIdSet)){
return Collections.emptySet();
}
redisUtil.removeZsetList(MASS_TASK_kEY,taskIdSet);
return taskIdSet.stream().map(n-> Long.parseLong(n)).collect(Collectors.toSet());
}
}
写一个service 实际上就是用一个redis的zset来存储我们的定时任务 然后用时间来作为分数 存入的定时任务的时间 然后用当前时间去获取任务 就是用rangeByScore来获取 获取到的应该是一个set集合 再写个test测试一下
@Test
public void pushTask() throws Exception {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
MassMailTaskTwo massMailTaskTwo = new MassMailTask();
massMailTaskTwo.setTaskId(1L);
massMailTaskTwo.setStartTime(simpleDateFormat.parse("2023-7-18 13:41:00"));
massMailTaskServiceTwo.pushMassMailTaskQUeue(massMailTaskTwo);
}
这样就可以把日期格式化 然后加入到定时任务队列中去
获取任务 就是调用
poolMassTaskQueue方法
@Test
public void deal() throws Exception {
String lockKey = "test.delay.task";
String requestId = UUID.randomUUID().toString();
try{
boolean lock = redisShareLockUtil.lock(lockKey,requestId,5L);
if(!lock) {
return;
}
Set<Long> longs = massMailTaskService.poolMassTaskQueue();
log.info("定时任务拉取{}", JSON.toJSONString(longs));
//拉取到锁就可以执行其他逻辑
}catch (Exception e){
}finally {
redisShareLockUtil.unlock(lockKey,requestId);
}
}
然后加锁防止有其它线程来把这个任务获取 加锁的方式使用分布式锁来加锁 这样就可以获取到定时任务 并且后面还可以执行相应的逻辑 然后就可以把消息一消费就好了
高级发布确认
mq在运行的时候可能挂机 然后之后进行重启 重启的过程中可能消息丢失 为了保证发布消息的可靠性 引入了消息确认发布高级的例子 (如果mq重启之后 消息丢失了 生产者也不知道消息发布成功没有 就会默认消息发布成功 ) 那就需要一个缓存的一个机制 生产者发布消息的时候会向缓存和队列中各发送一份 然后 在发送完成之后 而且交换机还确认发布完成之后 对这条消息确认之后就会给缓存中清除这条消息 那么剩下的就是没有发布的 利用缓存来做一个担保 最后生产者只需要知道缓存中有多少个没有发送出去就可以知道还剩下多少条消息 可以对未发布的消息进行重新发的发布
备份交换机 (报警队列)
有了mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置mandatory参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。在RabbitMQ.中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为 RabbitMQ中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为Fanout,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
幂等
为了防止消息被多次消费 要做一个幂等 可以用redis来保证原子性 或者利用全局唯一ID来保证消息的幂等性
MQ消费者的幂等性的解决一般使用全局ID或者写个唯一标识比如时间戳或者UUID或者订单消费者消费MQ中的消息也可利用MQ的该id来判断,或者可按自己的规则生成一个全局唯一id,每次消费消息时用该id先判断该消息是否已消费过。
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a.唯一ID+指纹码机制,利用数据库主键去重, b.利用redis.的原子性去实现
唯一ID+指纹码机制:
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个id是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
Redis原子性:
利用redis执行setnx命令,天然具有幂等性,从而实现不重复消费
优先级队列
设置消息优先级 优先级高的消息先执行 优先级低的消息后执行
在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如
果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用redis.,来存放的定时轮询,大家都知道redis,只能用List做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用RabbitMQ进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。
public class Producer {
// 队列名称
public static final String QUEUE_NAME="hello";
// 发消息
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 工厂IP连接RabbitMQ的队列
factory.setHost("192.168.163.128");
// 用户名
factory.setUsername("admin");
// 密码
factory.setPassword("123");
// 创建连接
Connection connection = factory.newConnection();
// 获取信道
Channel channel = connection.createChannel();
Map<String, Object> arguments = new HashMap<>();
//官方允许是0-255之间,此处设置10,允许优化级范围为0-10,不要设置过大,浪费CPU与内存
arguments.put("x-max-priority",10);
channel.queueDeclare(QUEUE_NAME,true,false,false,arguments);
// 发消息
for (int i = 0; i < 10; i++) {
String message = "info" + i;
if(i == 5){
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes(StandardCharsets.UTF_8));
}else {
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
}
}
System.out.println("消息发送完毕!");
}
}
这条优先级为5的队列就先执行 其它的后执行
惰性队列
它会把消息放在磁盘中 当需要消费的时候才把消息取出来 但是 io操作耗时太高 所以使用惰性队列的时候需要注意时间问题
RabbitMQ从 3.6.0版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到RabbitMQ的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当RabbitMQ需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。
rabbitMq集群
由于我们没有那么多台服务器 所以使用集群的时候应该通过修改端口号来达到集群的一个概念