2.框架结构
消息中间件组要有三部分组成:
1.生产者,生产消息发送给服务端
2.服务端,接收消息并通过配置规则发送给指定消费者,或者等消费者自己取。(即推和拉模式)
3.消费者,消费从队列过来的消息
注意:以上连线关系都是多对多的关系
3.功能及使用方法
主要介绍使用springmaqp框架来接入java中的使用
3.1引入springmaqp
使用文档地址:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
#rabbitmq
spring.rabbitmq.template.mandatory=true
spring.rabbitmq.host=49.235.142.29
spring.rabbitmq.port=5672
spring.rabbitmq.username=ajian
spring.rabbitmq.password=Unicom369
spring.rabbitmq.virtual-host=/sh
#消费完一条再取
spring.rabbitmq.listener.simple.prefetch=1
3.2创建交换机队列及其订阅关系
3.2.1代码配置
@Configuration
public class RabbitMqConfig {
//配置交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("ajian.fanout");
}
//队列
@Bean
public Queue fanoutQueue(){
return new Queue("ajian.queue");
}
//交换机和队列关系
@Bean
public Binding fanoutBinding(){
return BindingBuilder.bind(fanoutQueue()).to(fanoutExchange());
}
}
3.2.2注解配置
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "ajian.queue"),
exchange = @Exchange(name = "ajian.fanout",type = ExchangeTypes.FANOUT),
key = {"red","bule"}
))
public void getMessage2(String content){
System.out.println("1接收到消息:"+content);
}
3.3简单消息发送
@Autowired
private RabbitTemplate rabbitTemplate;
public void send() {
String name = "shtest1";
for(int i=1;i<30;i++){
String context = "hello world"+i;
this.rabbitTemplate.convertAndSend(name, context);
}
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "shtest1")
))
public void getMessage1(String content){
System.out.println("1接收到消息:"+content);
}
上代码是简单的从生产者——队列——消费者这过程,未经过交换机分配
注意:当发送map等实体时如不处理他会自动转为字节流传到队列中,这就可能导致消费失败,由于系列化方式不一样导致的,而且字节流占的空间也更大。
未处理时
转换方法:
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
</dependencies>
实例化转换器
@Bean
public MessageConverter JacksonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
其他代码不用做改变,因为发消息底层转换消息有该方法的使用,只要引入实现,就会在转换时自动使用该消息转换功能
3.4五种消息发送处理方式
3.4.1work工作模式
如图
即由多个消费者消费一个队列中的消息,适合一个消费者无法跟上生产者产生消息速度的情况下,实现方式和简单消息时相同,但消费者是多个。
注意:使用该模式时如果不配置的话默认是消费者轮询消费的,这就会出现性能好的消费者没完全利用
可在配置中配上属性,该值代表取几条消息并当这几条消息消费完成后再进行消费
spring.rabbitmq.listener.simple.prefetch=1
3.4.2Fanout广播模式
如图:
见名知意,就是创建广播交换机,拿到消息后会分发给每一台他绑定的队列
实现
//配置队列交换机关系
//配置交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("ajian.fanout");
}
//队列
@Bean
public Queue fanoutQueue(){
return new Queue("ajian.queue");
}
@Bean
public Queue fanoutQueue1(){
return new Queue("ajian.queue1");
}
//交换机和队列关系
@Bean
public Binding fanoutBinding(){
return BindingBuilder.bind(fanoutQueue()).to(fanoutExchange());
}
@Bean
public Binding fanoutBinding1(){
return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
}
//生产者
public void send1() {
String exchange = "ajian.fanout";
String context = "hello world";
this.rabbitTemplate.convertAndSend(exchange,null,context);
}
//消费者
@RabbitListener(queues = "ajian.queue1")
public void getMessage(String content){
System.out.println("接收到消息:"+content);
}
@RabbitListener(queues = "ajian.queue" )
public void getMessage1(String content){
System.out.println("1接收到消息:"+content);
}
3.4.3Direct定向模式
如图:
生产者发消息时添加一个key标识如red,交换机合队列绑定也添加一个key,当消息key与队列的key相同时就传入该队列进行消费
该模式为指定发送到那个队列,实现方式如下
//消费者创建定向交换机并绑定对应的队列
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "queue"),
exchange = @Exchange(name = "exchange",type = ExchangeTypes.DIRECT),
key = {"red","bule"}
))
public void getMessage2(String content){
System.out.println("1接收到消息:"+content);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "queue1"),
exchange = @Exchange(name = "exchange",type = ExchangeTypes.DIRECT),
key = {"red","bul"}
))
public void getMessage1(String content){
System.out.println("1接收到消息:"+content);
}
//生产者
public void send1() {
String exchange = "shtest1";
String context = "hello world";
this.rabbitTemplate.convertAndSend(exchange,"bul",context);
}
3.4.4Topic话题模式
如图
该模式是定向模式的升级版,把key的唯一匹配改成了相当于有规则的模糊匹配,如图中队列queue1的key=china.#,则发消息的key=china.news/china.weather/china.nanchang.news等都会传到该队列中
使用方法:
//配置topic交换机和key关系,消费消息
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topicQueue"),
exchange = @Exchange(name = "topicExchange",type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void topicMessage(String content){
System.out.println("1接收到消息:"+content);
}
//生产消息
public void send1() {
String exchange = "topicExchange";
String context = "hello world";
this.rabbitTemplate.convertAndSend(exchange,"china.news",context);
}
3.4.5延迟消息模式
1.前置
死信是什么:
队列中消息消费失败,且没配置其他消费方式,即消息被放弃了
消息是过期消息,没人消费
队列消息堆积满了,最早的消息
死信交换机:指专门用来处理死信消息的交换机
2.延迟消息的死信实现
如图队列一中消息没人消费,设置队列死信交换机属性,并设置过期时间,当队列1中的消息过期后变为死信,就会转发到死信交换机中,死信交换机收到消息把消息转发到队列中再进行消费。
使用场景:如用户下订单,订单消息进入队列1中,当用户30分钟内还不支付时,该信息变为死信,转发到死信交换机,再发到队列中,消费者进行取消订单操作。
使用
//按图配好交换机和队列,在队列1上如上图关联上死信交换机
//消费者
@RabbitListener(queues = "dlx.direct")
public void getMessage(String content){
System.out.println("接收到消息:"+content);
}
//生产者
public void send2() {
String exchange = "shtest1";
String context = "hello world";
this.rabbitTemplate.convertAndSend(exchange, "hi","hello world" ,new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置过期时间
message.getMessageProperties().setExpiration("1000");
return message;
}
});
}
3.插件实现
实现原理是创建一个能暂存数据的延时交换机,在消息没过期时保持在交换机中,超时后再发送到队列中消费
下载插件放到rabbitmq目录下
//durable = "true"表消息持久化。delayed = "true"表创建延时交换机
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "",durable = "true"),
exchange = @Exchange(name = "",type = ExchangeTypes.DIRECT ,delayed = "true"),
key = "delayed"
))
public void getMessage3(String content){
System.out.println("1接收到消息:"+content);
}
//生产者
public void send3() {
String exchange = "shtest1";
String context = "hello world";
this.rabbitTemplate.convertAndSend(exchange, "hi","hello world" ,new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置延迟时间
message.getMessageProperties().setDelay(1000);
return message;
}
});
}
3.5如何确保消息不丢失
3.5.1生产者
1.网络波动没连上mq服务
采用重连方式
#生产者配置
#rabbitmq
spring.rabbitmq.template.mandatory=true
spring.rabbitmq.host=49.235.142.29
spring.rabbitmq.port=5672
spring.rabbitmq.username=ajian
spring.rabbitmq.password=Unicom369
spring.rabbitmq.virtual-host=/sh
#连接超时时间
spring.rabbitmq.connection-timeout=1s
#开启重试
spring.rabbitmq.retry.enabled=true
#失败后初始等待时间
spring.rabbitmq.retry.initial-interval=1000ms
#失败后下次等待时长倍数
spring.rabbitmq.retry.multiplier=1
#最大重试次数
spring.rabbitmq.retry.max-attempts=3
2.生产者确认机制
持久化队列,建队列时durable = "true"表消息持久化
添加配置
#开启publisher-confirm并设置类型correlated异步回掉
spring.rabbitmq.publisher-confirm-type=correlated
#开启publisher-returns机制
spring.rabbitmq.publisher-returns=true
发送代码
@Configuration
//实例化容器后调用实现了通知接口的类,把上线文传过来做处理,统一指定
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
System.out.println("失败消息:"+message);
});
}
}
//发送代码
public void send4() {
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable throwable) {
System.out.println("消息回调失败"+throwable);
}
@Override
public void onSuccess(CorrelationData.Confirm confirm) {
if(confirm.isAck()){
System.out.println("成功收到ACK");
}else{
System.out.println("失败收到NACK");
}
}
});
String exchange = "shtest1";
this.rabbitTemplate.convertAndSend(exchange, "hi","hello world" ,correlationData);
}
3.5.2服务端
1.持久化
即配置持久属性,队列持久,发的消息持久
2.Lazy Queue(3.12版以后都是这种方式)
消息放入磁盘中,内存放一部分。
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "",durable = "true",arguments = @Argument(name="x-queue-mode",value = "lazy")),
exchange = @Exchange(name = "",type = ExchangeTypes.DIRECT ,delayed = "true"),
key = "delayed"
))
public void getMessage3(String content){
System.out.println("1接收到消息:"+content);
}
3.5.3消费者
1.消费者信息确认机制
与生产者确认机制相同:ack (服务端收到直接删除)。nack(重投) 。rejeck(失败删除)
配置
#消息消费确认回掉功能设置 auto自动调 none不调 手动调manual
spring.rabbitmq.listener.simple.acknowledge-mode=auto
#开启重试消息本地重试
spring.rabbitmq.listener.simple.retry.enabled=true
#失败后初始等待时间
spring.rabbitmq.listener.simple.retry.initial-interval=1000ms
#失败后下次等待时长倍数
spring.rabbitmq.listener.simple.retry.multiplier=1
#最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=3
#事物消息false表有状态
spring.rabbitmq.listener.simple.retry.stateless=true
新建错误信息队列交换机
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry",name = "enabled",havingValue = "true")
public class RabbitMqConfig {
//配置交换机
@Bean
public DirectExchange directExchange(){
return new DirectExchange("ajian.fanout");
}
//队列
@Bean
public Queue fanoutQueue(){
return new Queue("ajian.queue");
}
//交换机和队列关系
@Bean
public Binding fanoutBinding(Queue fanoutQueue,DirectExchange directExchange){
return BindingBuilder.bind(fanoutQueue).to(directExchange).with("error");
}
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate,"ajian.fanout","error");
}
}
该配置执行方式是消费者消费消费时,出错本地重新执行配置的次数,还是失败的话会把消息传到上面配置的错误信息队列中,通知开发人员关注错误信息。
3.6如何防止消息被重复消费
幂等性
一个id放redis中,消费了就删除,即不存在就不能消费
生产一个id,消费了就存起来,存在id即为已经消耗了
延迟计时的实现是每条消息都维护了一个时钟,与radis实现的区别