RabbitMQ 是现较流行的分布式消息中间件,其重要的设计核心是为项目复杂的业务逻辑实现削峰、异步、解耦。
相信大家已经有一些MQ 的基础了,MQ 如何安装 以及基本的消息模型就不做介绍了,本文将从SpringBoot 整合MQ 开始,逐步简单介绍如何实现用户登录异步写日志。
为何登录之后不直接将日志记录到DB 而要多此一举将信息发送到队列中再异步记录到DB 呢?
当然是为了专注于处理核心业务逻辑,把边缘的业务逻辑推到队列中异步处理提升性能和业务处理的效率。登录模块最核心的业务是验证该用户是否存在以及用户所拥有的权限再把验证信息返回,至于记录日志我们可放在队列中再异步消费。
流程图:
talk is cheap just show the code 多说无益直接上代码。
下边我会先把比较重要和比较核心的代码放在最前面,至于一些实体类和最基本响应类会放在文末
Step 1 : 添加依赖
<!--MQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>1.3.3.RELEASE</version>
</dependency>
step 2 : 在配置文件添加配置
#rabbitmq 配置
spring.rabbitmq.virtual-host=/
# 选择本地
spring.rabbitmq.host=127.0.0.1
# 端口号
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 登录写log 的mq 配置
mq.login.queue.name=${mq.env}.middleware.login.queue
mq.login.exchange.name=${mq.env}.login.exchange
mq.login.routing.key.name=${mq.env}.login.routing.key
step 3 : 创建配置类 RabbitmqConfig
package com.example.demo.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
/**
* @description: MQ 配置类
* @author: Edison
* @create: 2021-04-05 17:28
**/
@Configuration
public class RabbitmqConfig {
private static final Logger log= LoggerFactory.getLogger(RabbitmqConfig.class);
/**
* 读取环境变量
*/
@Autowired
private Environment env;
/**
* 连接工厂实例
*/
@Autowired
private CachingConnectionFactory connectionFactory;
/**
* set 消息监听器所在容器工厂的配置实例
*/
@Autowired
private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;
/**
* 单一消费者
* @return
*/
@Bean(name = "singleListenerContainer")
public SimpleRabbitListenerContainerFactory listenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
//初始消息的数量
factory.setConcurrentConsumers(1);
// 最大消费数量
factory.setMaxConcurrentConsumers(1);
// 每个实例获取消息的数量
factory.setPrefetchCount(1);
return factory;
}
/**
* 多个消费者
* @return
*/
@Bean(name = "multiListenerContainer")
public SimpleRabbitListenerContainerFactory multiListenerContainer(){
// 监听消息所在的工厂
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
//set 工厂所在的容器
factoryConfigurer.configure(factory,connectionFactory);
// 消息传输的格式
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setConcurrentConsumers(10);
factory.setMaxConcurrentConsumers(15);
factory.setPrefetchCount(10);
factory.setAcknowledgeMode(AcknowledgeMode.NONE);
return factory;
}
/**
* RabbitMQ发送消息的操作组件实例
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate(){
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause);
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message);
}
});
return rabbitTemplate;
}
/**用户登录成功写日志消息模型创建**/
/**
* 创建交换机
* @return
*/
@Bean(name = "loginQueue")
public Queue loginQueue(){
return new Queue(env.getProperty("mq.login.queue.name"),true);
}
/**
* 创建交换机
* 这里采用topic 消息模型
* @return
*/
@Bean
public TopicExchange loginExchange(){
return new TopicExchange(env.getProperty("mq.login.exchange.name"),true,false);
}
/**
* 创建绑定
* @return
*/
@Bean
public Binding loginBinding(){
return BindingBuilder.bind(loginQueue()).to(loginExchange()).with(env.getProperty("mq.login.routing.key.name"));
}
}
Step 3 : 创建MQ 的消息提供者
package com.example.demo.config;
import com.example.demo.dto.UserDto;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.AbstractJavaTypeMapper;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
/**
* @description:
* @author: Edison
* @create: 2021-05-13 23:01
**/
@Component
public class LogPublisher {
private static final Logger log= LoggerFactory.getLogger(LogPublisher.class);
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private Environment env;
@Autowired
private ObjectMapper objectMapper;
/**
* 发送登录成功后的用户相关信息入队列
* @param loginDto
*/
public void sendLogMsg(UserDto loginDto){
try {
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
rabbitTemplate.setExchange(env.getProperty("mq.login.exchange.name"));
rabbitTemplate.setRoutingKey(env.getProperty("mq.login.routing.key.name"));
rabbitTemplate.convertAndSend(loginDto, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties=message.getMessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
messageProperties.setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME,UserDto.class);
return message;
}
});
log.info("系统日志记录-生产者-发送登录成功后的用户相关信息入队列-内容:{} ",loginDto);
}catch (Exception e){
log.error("系统日志记录-生产者-发送登录成功后的用户相关信息入队列-发生异常:{} ",loginDto,e.fillInStackTrace());
}
}
}
step 4 : 创建消息的消费者
package com.example.demo.config;
import com.example.demo.dto.UserDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
/**
* @description: 日志消费者
* @author: Edison
* @create: 2021-05-13 23:19
**/
@Component
public class LogConsumer {
private static final Logger log= LoggerFactory.getLogger(LogConsumer.class);
/**
* 监听消费并处理用户登录成功后的消息
* @param loginDto
*/
@RabbitListener(queues = "${mq.login.queue.name}",containerFactory = "singleListenerContainer")
public void consumeMsg(@Payload UserDto loginDto){
try {
log.info("系统日志记录-消费者-监听消费用户登录成功后的消息-内容:{}",loginDto);
// 此处把记录到DB 的操作操作给省略了,它不是重点
// to do 监听到日志信息之后进行消费把log 写进DB
log.info(" 把消息记录到DB...................................");
}catch (Exception e){
log.error("系统日志记录-消费者-监听消费用户登录成功后的消息-发生异常:{} ",loginDto,e.fillInStackTrace());
}
}
}
step 5 : 创建一个简单的业务逻辑处理类
package com.example.demo.business;
import com.example.demo.common.BaseResponse;
import com.example.demo.config.LogPublisher;
import com.example.demo.dto.UserDto;
import com.example.demo.enums.StatusCode;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @description: user 业务逻辑层
* @author: Edison
* @create: 2021-05-12 23:11
**/
@Component
public class UserBusiness {
@Autowired
private LogPublisher logPublisher;
public BaseResponse<UserDto> login(UserDto userDto){
// 此处hard code了 假设要验证的用户存在
Boolean isExist = true;
BaseResponse response = new BaseResponse(StatusCode.Success);
if (isExist) {
response = new BaseResponse(StatusCode.Success.getCode(),"登录成功");
logPublisher.sendLogMsg(userDto);
} else {
response = new BaseResponse(StatusCode.Fail.getCode(),"登录失败");
}
return response ;
}
}
step 6 : 一个简单的controller
package com.example.demo.controller;
import com.example.demo.business.UserBusiness;
import com.example.demo.common.BaseResponse;
import com.example.demo.dto.UserDto;
import com.example.demo.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @description:
* @author: Edison
* @create: 2021-05-11 22:06
**/
@RestController
@RequestMapping("user")
public class UserController {
private static final Logger log= LoggerFactory.getLogger(UserController.class);
@Autowired
private UserBusiness userBusiness;
@PostMapping("/login")
public BaseResponse<UserDto> login(@RequestBody @Validated UserDto userDto , BindingResult result){
return userBusiness.login(userDto);
}
}
step 7 : 补充一下一些基础类
(1) UserDto
package com.example.demo.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* @description:
* @author: Edison
* @create: 2021-05-11 21:52
**/
@Data
public class UserDto implements Serializable {
@NotNull
@Size(min = 6, max = 16)
private String userName ;
@NotNull
@Size(min = 8, max = 16)
private String passWord;
private Integer userId;
}
(2) BaseResponse 基础的响应类
package com.example.demo.common;
import com.example.demo.enums.StatusCode;
/** 基础响应类
* @description:
* @author: Edison
* @create: 2021-05-12 23:18
**/
public class BaseResponse<T> {
/**
* 状态码
*/
private Integer code;
/**
* 响应信息
*/
private String msg;
/**
* 响应的数据类型
*/
private T data;
/**
* 重载构造方法
* @param code
* @param msg
*/
public BaseResponse(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
/**
* 重载构造方法
* @param statusCode
*/
public BaseResponse(StatusCode statusCode) {
this.code = statusCode.getCode();
this.msg = statusCode.getMsg();
}
/**
* 重载构造方法
* @param code
* @param msg
* @param data
*/
public BaseResponse(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
(3) 枚举类 StatusCode
package com.example.demo.enums;
/**
* @description: 基本状态码
* @author: Edison
* @create: 2021-05-12 23:14
**/
public enum StatusCode {
//以下是暂时设定的几种状态码类
Success(200,"成功"),
Fail(-1,"失败"),
InvalidParams(201,"非法的参数!"),
InvalidGrantType(202,"非法的授权类型");
/**
* 状态码
*/
private Integer code;
/**
* 描述
*/
private String msg;
/**
* 重载构造方法
* @param code
* @param msg
*/
StatusCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
最后用postman测试一下
看看idea 打印的日志
可以看到消息发送成功且消费者成功接收到了消息
本文只是简单介绍了SrpingBoot 整合RabbitMQ 实现用户登录异步记录日志,MQ 的使用场景还有很多,最核心的思想还是 削峰、异步、解耦。关于MQ 还有几个需要思考的小问题,比如如何保证消息传输过程中不丢失,如何保证消息不被重复消费、以及当服务器崩溃的时候如何保证队列中的消息不丢失等等,这些同事们可以自行去了解一下。
共同学习,共同进步!