1.为什么在分布式项目中需要一款消息中间件?
(这里主要讲服务的异步调用和消息的广播)
消息中间件能够实现一些Feign(同步调用)无法实现的效果:1、服务的异步调用
2、消息的广播(事件总线)
3、消息的延迟处理
4、分布式事务
5、请求削峰(处理高并发)
2.服务的异步调用:
同步服务是:两个服务之间传数据是通过feign调用的,当a服务被调用执行方法时消耗的时间,b服务是需要等待的。
异步服务是:当a服务传数据给b服务时只需要把数据传给mq,a服务就可以调用别的服务了,这个时候b服务有没有接收数据已经和a服务没有关系了
在一些需要返回数据的服务调用就用同步调用,不要数据返回的就用异步调用
- 创建交换机一般都是在启动类创建,需要配置@Configuration,表示声明当前类是一个配置类(相当于一个Spring配置的xml文件)要配合@Bean来使用。
2.发布消息到交换机,广播事件
本质上发送的是字节数组,写的时候是字符串,用这个方法convertAndSend会把任意对象自动转换成字节数组,前提是这个对象能被序列化
spring很喜欢做这种Templat模板类,里面封装了很多的方法给我们使用
@Autowired
private RabbitTemplate rabbitTemplate;
rabbitTemplate.convertAndSend(“hotal_exchange”,“hotal_insert”,entity);
3.依赖
org.springframework.boot
spring-boot-starter-amqp
4.还要配置yml文件,因为事件总线是提供者发布广播消费者监听广播,是属于不同的两个微服务,那消费者需要依赖这个广播和配置文件知道他的信息,就像数据库的配置文件一样的意思
5.spring管理的RabbitMQ对象,默认是懒加载,如果不发送消息,交换机就不会创建,所以消费者需要创建队列和交换机的同时并且进行绑定,这样做的好处就是提供者和消费者谁先启动就不重要了,如果不创建交换机,提供者没启动,就不会创建交换机,消费者就绑定不了,会报错。
@Configuration
public class RabbitMQConfig {
/**
* 创建队列和交换机
* @return
/
@Bean
public DirectExchange getExchange(){
return new DirectExchange(“hotal_exchange”,true,false);
}
@Bean
public Queue getQueue(){
return new Queue(“city_queue”,true,false,false);
}
/*
* 绑定队列和交换机
*/
@Bean
public Binding getBinding(Queue getQueue,DirectExchange getExchange){
// return new Binding(“city_queue”,Binding.DestinationType.QUEUE,“hotal_exchange”,“hotal_in>sert”,null);
return BindingBuilder.bind(getQueue).to(getExchange).with(“hotal_insert”);
可以用绑定的构建器,指定对象,但是类型是队列的对象,队列最终是注入到spring容器里面的怎么拿到这个队列呢?可以在 @Bean指定方法名,不指定那么他bean名字自动是方法名,可以把他们的对象通过传参的方式作为参数,传过来
}
}
5.监听队列,队列里面有消息我要去处理。一般写在启动类
@Configuration
public class RabbitMQListrn {
@Autowired
private ICityService cityService;
/**
* 监听队列
*/
@RabbitListener(queues = “city_queue”)
public void msgHandler(Hotal hotal){
//修改对应城市酒店的数量
cityService.updatecitynumber(hotal.getCid(),1);
}
}
rabbitmq 为什么要用模型4呢?
因为对比模型一:模型一只能一对一消费,万一别的服务对这个消息感兴趣呢那就得去监听,
就形成了模型二:多个服务一起监听队列但是模型二是轮询消费,就是一个消息消费者一消费完了,那消费者二,就消费不了了
这个时候就有了交换机的概念(模型三):交换机有4种类型fanout,direct,topic,header 模型3用的就是fanout,不含路由键的无条件广播,消费者一和消费者二所有消息都能监听到,但是这样会不有点浪费
由是就有了模型4:多了个路由键,交换机和队列绑定若干路由键,发布的消息可以指定路由键发送,就是发布者发布一个消息谁敢兴趣就来监听。
二:封装消息中间件
>对事件接收和事件发布者来说有n个异步请求都需要调用n次调用就有n创建交换机,创建队列和队列绑定。
当我们面对一个技术需要反复的调用这个时候我们可以考虑做个封装去简化一下调用的步骤这里我们可以去写一个服务把创建交换机,创建队列和队列绑定,监听都交给这个服务,我们只需要保证事件的处理交给开发者。
我们定义两个类为提供事件发布者调用的类为(1)Eventuil,事件接收者调用的接口为(2)EvenListener
(3).我们先定义一个接口EventConstact,为我们把发布者创建的交换机和接收者监听的类型的“行为”抽象成常量(方便接口名直接调用)而且随着业务的增加事件需要异步的服务也可以动态直接添加事件类型。(4)创建交换机EventPublishConfiguration (5).这个时候我们可以在写一个接口EvenListener,监听的事件类型(路由键)和监听的事件类型(路由键)发布事件接受者实现这里就保证了事件的处理交给开发者。
在创建EventConsumerConfigur这个类里要引用接口EvenListener实现事件接受者创建队列和交换机。在创建队列的时候需要知道队列的名称作为参数,这个时候队列的名称就要去实现动态的毕竟这是一个封装,从项目的开始到结束都需要保证事件的处理交给开发者所以我们可以把微服务独有的名称,作为队列的名称,不管什么微服务调用都能够区别开来。然后我们在绑定队列到交换机由路由键发送消息我们要实现spring允许找到EvenListener注入到List集合中,而EvenListener恰好事件接收者需要实现他,与是循环EvenListener有多少个接受者循环出多少个,但是with只能绑定一个,他的参数没有没有数组,与是我们可以动态将binding对象注册注册到spring容器中,新建一个类为(6)SpringContextUtil。
在实现动态将binding对象注册注册到spring容器时我们需要了解到spring 框架有一个核心容器叫IOC,IOC是于Bean工厂实现的,这个工厂本质是用Map<beanName,beanDefinition>存放bean的我们手动将Bean注册到IOC容器中其实就要做一件事情,把bean包装成beanDefinition
(7)新建一个类(RubbitMQListener)监听指定的队列,队列里面有消息要去处理,在没有封装前是写在启动类的, 但是当前的思想是事件的处理交给事件接受者,所以处理队列里面的消息就在这里去处理
(1)Eventuil 事件发布者调用的类
@Component
public class Eventuil {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发布消息到交换机,广播事件,这个给发布者调用声明他发布了什么消息
* 参数:当前的事件类型是什么,当前你要发送事件的消息是什么?消息不知道就用object代替
* @param eventType
* @param msg
*/
public void publishEvent(String eventType,Object msg){
rabbitTemplate.convertAndSend(EventConstact.EXCHANGE_NAME,eventType,msg);
}
}
(2)EvenListener 事件监听器的接口(事件接收者调用的接口),在这里事件类型为什么要被spring找到因为发布者有会发布很多事件,在这个业务我只需要这个业务的事件,不需要其他的事件。所以得让spring找到
```java
/**
* 事件监听器的接口
*/
public interface EvenListener<T> {
/**
* 监听的事件类型(路由键)
*/
String getEventType();
/**
* 事件的处理方法(这个方法在不同的微服务端做不同的实现就好了)
* 这里的参数如果给的是Object类型则需要强转,给个泛型就不需要强转了
*/
void eventHandler(T msg);
}
(3)EventConstact接口
public interface EventConstact {
/**
* 交换机的名称
* 这里定义常量可以让其他服务直接调用
*/
String EXCHANGE_NAME="event-exchange";
/**
* 事件类型常量
*/
String EVENT_LOGIN ="login"; //用户登录事件
String EVENT_HOTAL_INSERT="hotal_insert";//酒店新增事件
}
(4) EventPublishConfiguration 交换机
@Configuration
public class EventPublishConfiguration {
/**
* 创建了一个交换机
* 事件发布者需要创建
* 事件接收者也需要创建
* 参数一交互机的名字
* 参数二交互机的持久化
* 参数三自动删除
* @return
*/
@Bean
public DirectExchange getExchange(){
return new DirectExchange(EventConstact.EXCHANGE_NAME,true,false);
}
}
(5)EventConsumerConfigur 事件接受者创建队列和交换机
/**
* 事件接受者创建队列和交换机
*/
@Configuration
/**
* 酒店服务添加了消费者的代码,在监听指定队列,注入eventListeners的集合和这里都会产生影响,因为酒店服务是发布者,eventListeners是消费者才有的这个实现类,
* 这个时候就可以用这个注解
* 如果容器中包含EvenListener这个类的bean就会被管理起来
*如果不包含这个bean就会忽略这个bean
* 就知道你当前spring容器中有没有实现eventListeners这个类,实现了就是消费者,没实现就是发布者
*/
@ConditionalOnBean(EvenListener.class)
public class EventConsumerConfigur {
/**
* 队列的名称要实现动态的,每个微服务都有独有的名称,作为队列的名称,不管什么微服务调用都区别开来
*/
@Value("${spring.application.name}")
private String applicatiName;
@Autowired
private SpringContextUtil springContextUtil;
/**
* 这个写法的意思是spring允许找到EvenListener注入到List集合中,而EvenListener恰好事件接收者需要实现他,
* 与是下面就循环EvenListener有多少个接受者循环出多少个,但是with只能绑定一个,他的参数没有没有数组,由是我们可以动态将binding对象注册注册到spring容器中
*
*/
@Autowired
private List<EvenListener> eventListeners;
/**
* 创建队列
* @return
*/
@Bean
public Queue getQueue(){
return new Queue(applicatiName+"-queue",true,false,false);
}
/**
* 绑定队列到交换机叫什么路由键
* 只要实现List里面有几个eventListeners和什么类型来决定这绑定的路由键是什么
* --就是把binding注册到了spring容器里面去了,但是,
* 不能删@Bean没有@Bean这个方法都不触发但是这个返回值已经没有意义了,我要的是动态注册的过程
* @return
*/
@Bean
public Binding getBinding(Queue getQueue, DirectExchange getExchange){
//循环所有的EventListener实现类
eventListeners.stream().forEach(event -> {
//获得当前的处理器需要处理的事件类型-路由键
String eventType = event.getEventType();
Binding binding = BindingBuilder.bind(getQueue).to(getExchange).with(eventType);
//动态将binding对象注册到spring容器中,万一有两个不同的事件就用上hashcode或者uuid也可以,这里其实就是跟 @Bean的方法是一样的
springContextUtil.registerBean(eventType + event.hashCode(),binding);
});
return null;
}
}
(6) SpringContextUtil 相当于自定义的bean
/**
* @Component让spring扫到
* spring -> IOC -> Bean工厂
* 本质是用Map<beanName,beanDefinition>存放bean的
*手动将Bean注册到IOC容器中其实就要做一件事情,把bean包装成beanDefinition
*/
@Component
public class SpringContextUtil implements BeanDefinitionRegistryPostProcessor {
//注册bean的核心对象
private BeanDefinitionRegistry beanDefinitionRegistry;
/**
* 自定义工具方法-注册Bean对象
* 参数一:bean的名称
* 参数二。这个bean用来干嘛的
* @throws BeansException
*/
public void registerBean(String beanname,Object bean){
//将bean封装成beanDefinition对象
BeanDefinitionBuilder beanDefinition= BeanDefinitionBuilder.genericBeanDefinition(bean.getClass(), new Supplier() {
@Override
public Object get() {
return bean;
}
});
//将BeanDefintion注册到spring容器中
/**
* 为什么要new一个Supplier()因为当beanDefinition.getBeanDefinition()只给bean的类型并没有真正的用了bean对象,spring容器里面会
* 自动的调用改造方法去创建bean对象,但是我们现在不要他自己创建,我要他用我创建的对象所以我们就用了Supplier这个接口,他里面
* 重写了一个方法让我们返回Object
*/
beanDefinitionRegistry.registerBeanDefinition(beanname,beanDefinition.getBeanDefinition());
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
this.beanDefinitionRegistry=registry;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}
(7)监听指定队列 RubbitMQListener
/**
* 事件接收者监听队列
* 消费者的这些代码会不会对发布者产生影响,
*/
@Component
@ConditionalOnBean(EvenListener.class)
public class RubbitMQListener {
@Autowired
private List<EvenListener> eventListeners;
/**
* 监听指定的队列,队列里面有消息要去处理,在没有封装前是写在启动类的,
* 但是当前的思想是事件的处理交给事件接受者,所以处理队列里面的消息就在这里去处理
*/
@RabbitListener(queues = "${spring.application.name}-queue")
public void masHotal(Message message){
//获得发布消息的路由键-事件类型
String RoutingKey = message.getMessageProperties().getReceivedRoutingKey();
//交给对应的EventLister处理
eventListeners.stream().forEach(event ->{
if (event.getEventType().equals(RoutingKey)){
//直接调用当前event处理改消息
byte[] msgVytes=message.getBody();
//我不知道收到的是什么消息只知道收到的是byte数组
//序列化工具类有一个反序列数组得到了你想要的数据类型
event.eventHandler(SerializationUtils.deserialize(msgVytes));
}
});
}
}