实际生产中,我们经常会碰到这样的场景: 业务方触发了某些预料之中的bug,(比如项目中调用了第三方的服务,但是第三方的服务出问题导致无法访问,这类错,我们往往不会直接提示用户,而是选择屏蔽此类错误,写入错误日志),我们处理此类bug往往需要去生产导出日志记录,然后排查,最后找到第三方服务的提供者去解决问题.
那么,与其等“被动”业务反馈,能不能让这类问题“主动”推送给开发呢? 我们能不做个“错误预警”的服务.
消息推送技术,即是解决这类问题的良方.
消息队列,一般我们会简称它为MQ(Message Queue),再介绍消息队列前,我们还是先简单解释一下队列这种数据结构
队列是一种先进先出的数据结构
如图,数据从队尾(右)进,从队头(左)出.
消息队列可以简单的理解为:把要传输的数据放在队列中。
当我们需要使用消息的时候可以取出数据供自己使用。
从以上概念中我们不难看出有两个角色对队列至关重要,一个是放数据的,一个是取数据的.
当然,这两个角色都有是有规范的名字的,同时,消息队列有两种场景,在这两种不同的场景里,这两个角色名字是不同的:
生产者将生产消息放到队列里,多个消费者同时监听队列,谁先抢到消息谁就
会从队列中取走消息;即对于每个消息只能被最多一个消费者拥有。
包括三个角色:
生产消费者模式特点:
发布者将生产消息放到队列里,多个监听队列的消费者都会收到同一份消息;
即正常情况下每个消费者收到的消息应该都是一样的。
包括三个角色:
发布订阅模式特点:
消息队列为了实现实现高性能,高可用,可伸缩和最终一致性架构,主要可以解决如下问题:
多应用对消息队列中同一消息进行处理,应用间并发处理消息,相比串行处理,减少处理时间
场景举例:
用户注册后,需要发注册邮件和注册短信.
传统的做法有两种
将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。
以上三个任务全部完成后,返回给客户端
将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间
将注册信息写入数据库,再将任务写入消息队列后,立即返回成功给客户端,
则总的响应时间依赖于写入消息队列的时间,
而写入消息队列的时间本身是可以很快的,基本可以忽略不计,
因此总的处理时间相比串行提高了2倍,相比并行提高了一倍
多应用间通过消息队列对同一消息进行处理,避免调用接口失败导致整个过程失败
场景举例:
银行身份证人脸识别系统,用户上传身份证图片,人脸识别系统会对该图片进行人脸识别.
一般的做法是:
服务器接收到图片后,图片上传系统立即调用人脸识别系统,调用完成后再返回成功
该方法有如下缺点:
为了解决以上缺点,我们采用消息队列解决应用间的耦合问题:
消息队列的做法:
用户上传图片后,图片上传系统将图片信息顺序写入消息队列,直接返回成功;
人脸识别系统则定时从消息队列中取数据,完成对图片的识别。
图片上传系统并不需要关心人脸识别系统是否对这些图片信息的处理、以及何时对这些图片信息进行处理。事实上,由于用户并不需要立即知道人脸识别结果,人脸识别系统可以选择不同的调度策略,按照闲时、忙时、正常时间,对队列中的图片信息进行处理。
广泛应用于秒杀或抢购活动中,避免流量过大导致应用系统挂掉的情况
场景举例:
电商秒杀活动,常见的形式是数量极少的热门商品让大量的用户抢购
传统的做法是用户直接请求业务系统,但往往因为并发用户过大,或导致业务系统崩溃,或着出现超卖等等现象.
采用消息队列后,系统可以从消息队列中取数据,相当于消息队列做了一次缓冲
采用消息队列处理秒杀有如下优点:
使用消息队列有如下优点:
消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。目前使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ,这些消息中间件我们暂时不讲,本章,我们使用最为简单的方式REDIS来实现消息队列的发布订阅模式.
Redis从2.X版本开始,就支持一种基于非持久化消息的、使用发布/订阅模式实现的事件通知机制.
所谓基于非连接保持,是因为一旦消息订阅者由于各种异常情况而被迫断开连接,在其重新连接后,
其离线期间的事件是无法被重新通知的(一些Redis资料中也称为即发即弃).
而其使用的发布/订阅模式,意味着其机制并不是由订阅者周期性的从Redis服务拉取事件通知,
而是由Redis服务主动推送事件通知到符合条件的若干订阅者.
通俗的来讲,Redis实现的发布订阅模式有如下注意点:
org.springframework.boot
spring-boot-starter-data-redis
spring:
redis:
port: 6379
database: 0
host: 127.0.0.1
password: 123456
jedis:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
timeout: 5000ms
server:
port: 9999
package com.mrcoder.sbredispubsub.config.redis;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mrcoder.sbredispubsub.model.MessageSubscriber;
import com.mrcoder.sbredispubsub.utils.RedisPubSubUtil;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @Description: Redis配置类
*/
@Configuration
@ConditionalOnClass({RedisTemplate.class})
public class RedisConfig {
/**
* Redis操作模板配置
*
* @param connectionFactory
* @return
*/
@Bean
public RedisTemplate, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate template = new RedisTemplate();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public RedisTemplate, ?> redisTemplate(RedisConnectionFactory connectionFactory, Jackson2JsonRedisSerializer
package com.mrcoder.sbredispubsub.model;
import lombok.Data;
import java.util.Date;
@Data
public class SimpleMessage {
private String publisher;
private String content;
private Date createTime;
}
package com.mrcoder.sbredispubsub.utils;
import com.mrcoder.sbredispubsub.model.SimpleMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import java.util.Date;
/**
* @Description: Redis发布订阅
*/
public class RedisPubSubUtil {
private static final Logger logger = LoggerFactory.getLogger(RedisPubSubUtil.class);
private RedisTemplate redisTemplate;
public RedisPubSubUtil(RedisTemplate redisTemplate) {
super();
this.redisTemplate = redisTemplate;
}
public void publish(String publisher, String content) {
logger.info("message send {} by {}", content, publisher);
SimpleMessage simpleMessage = new SimpleMessage();
simpleMessage.setContent(content);
simpleMessage.setPublisher(publisher);
simpleMessage.setCreateTime(new Date());
ChannelTopic channelTopic = new ChannelTopic("redis.pubsub.msg");
redisTemplate.convertAndSend(channelTopic.getTopic(), simpleMessage);
}
}
package com.mrcoder.sbredispubsub.model;
import com.mrcoder.sbredispubsub.utils.FastJsonUtil;
import org.springframework.stereotype.Component;
/**
* @Description: 消息订阅类
*/
@Component
public class MessageSubscriber {
public void onMessage(SimpleMessage simpleMessage, String pattern) {
logger.info("topic {} received {}", pattern, FastJsonUtil.javaToJsonSnakeCase(simpleMessage));
}
}
package com.mrcoder.sbredispubsub.controller;
import com.mrcoder.sbredispubsub.utils.RedisPubSubUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.*;
@CrossOrigin
@RestController
public class RedisPubSubController {
@Autowired
private RedisPubSubUtil redisPubSubUtil;
@GetMapping("redisPubSub")
public void redisPubSub(){
redisPubSubUtil.publish("echo", "testMessage"));
}
}
以上已经实现了基于redis简单的发布订阅了.
那么,在此之上我们多做一点来更好的理解发布订阅这块的内容.
项目地址:
https://github.com/MrCoderStack/SpringBootDemo/tree/master/sb-redis-pubsub