最近在做的一个项目,为了应对压力测试,在项目中引入消息中间件,用来削峰填谷。本篇就简单的介绍一下我在这个过程中的一些实现和体会。
1、消息中间件
消息中间件有很多,目前比较流行的有 rabbitMq,rocketMq,activeMq,kafka等。鉴于rabbitMq 成熟、稳定、支持多种工作模式、消息持久化等特点,和Springboot 对 rabbitMq 的支持也比较友好。所以我这边选用的是rabbitMq。
众所周知,消息中间件一般有 业务解耦,流量削峰填谷等作用。在内外网的环境中,有时也可作为数据传递的较为安全的中转方式。
2、实现
闲言少叙,直接引入实现的过程。 因为引入 rabbitMq 的主要目的是流量控制,因此在这里的主要想法还是使用生产消费者模式,创建多个 Queue 用于业务数据存储。虽然,rabbitMq 拥有 direct、fallout、topic、headers 等工作模式,在此处,direct 方式就可以满足需要。
在Springboot 当中,直接在配置文件中,配置rabbitMq 信息,就可以初始化 rabbitTemplate。 最开始,我也是这么操作的。
# rabbitmq 配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#虚拟host 可以不设置 默认host
virtual-host: /
# 确认消息是否发送到交换机
publisher-confirm-type: correlated
# 确认消息是否发送到队列
publisher-returns: true
template:
# 消息发送失败返回到队列中,yml 文件中需要配置 publisher-returns: true
mandatory: false
然后就是创建 交换器(exchange )、队列(queue);然后 使用 rountingkey 绑定(binding)。
public enum ExchangeEnum {
EXCHANGE_LOGIN(MsgTypeEnum.login,ExchangeNameCons.EXCHANGE_NAME_LOGIN,ExchangeTypeEnum.DIRECT,true,false,false)
,EXCHANGE_STARTWORKFLOW(MsgTypeEnum.startworkflow,ExchangeNameCons.EXCHANGE_NAME_STARTWORKFLOW,ExchangeTypeEnum.DIRECT,true,false,false);
/** 交换机业务类型 */
private MsgTypeEnum msgTypeEnum;
/** 交换机名称 */
private String exchangeName;
/** 交换机类型 */
private ExchangeTypeEnum exchangeTypeEnum;
/** 是否持久化交换机 */
private boolean durable;
/** 是否自动删除 */
private boolean autoDel;
/** 是否延迟 */
private boolean delayed;
/** headers 参数 */
private Map<String, Object> arguments;
ExchangeEnum(MsgTypeEnum msgTypeEnum,String exchangeName, ExchangeTypeEnum exchangeTypeEnum, boolean durable,boolean autoDel,boolean delayed) {
this.msgTypeEnum = msgTypeEnum;
this.exchangeName = exchangeName;
this.exchangeTypeEnum = exchangeTypeEnum;
this.durable = durable;
this.autoDel = autoDel;
this.delayed = delayed;
}
public Map<String, Object> getArguments() {
return arguments;
}
public boolean isDelayed() {
return delayed;
}
public String getExchangeName() {
return exchangeName;
}
public ExchangeTypeEnum getExchangeTypeEnum() {
return exchangeTypeEnum;
}
public boolean isDurable() {
return durable;
}
public MsgTypeEnum getMsgTypeEnum() {
return msgTypeEnum;
}
public boolean isAutoDel() {
return autoDel;
}
public static List<ExchangeEnum> toList(){
List<ExchangeEnum> list = new ArrayList<>();
for (ExchangeEnum exchangeEnum: ExchangeEnum.values()
) {
list.add(exchangeEnum);
}
return list;
}
}
@Configuration
public class RabbitMqConfig {
/** 登陆 */
@Bean(name = "loginQueue")
public Queue loginQueue() {
return new Queue(QueueEnum.QUEUE_LOGIN.getName(),QueueEnum.QUEUE_LOGIN.isDurable(),
QueueEnum.QUEUE_LOGIN.isExclusive(), QueueEnum.QUEUE_LOGIN.isAutoDel());
}
@Bean(name = "loginExchange")
public Exchange loginExchange(){
return transExchage(ExchangeEnum.EXCHANGE_LOGIN);
}
@Bean(name = "loginBinding")
public Binding loginBinding(@Qualifier("loginQueue") Queue queue,@Qualifier("loginExchange") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(RountingKeyCons.LOGIN).noargs();
}
/** 启动工作流 */
@Bean(name = "startworkflowQueue")
public Queue startworkflowQueue() {
return new Queue(QueueEnum.QUEUE_STARTWORKFLOW.getName(),QueueEnum.QUEUE_STARTWORKFLOW.isDurable(),
QueueEnum.QUEUE_STARTWORKFLOW.isExclusive(), QueueEnum.QUEUE_STARTWORKFLOW.isAutoDel());
}
@Bean(name = "startworkflowExchange")
public Exchange startworkflowExchange(){
return transExchage(ExchangeEnum.EXCHANGE_STARTWORKFLOW);
}
@Bean(name = "startworkflowBinding")
public Binding startworkflowBinding(@Qualifier("startworkflowQueue") Queue queue,@Qualifier("startworkflowExchange") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(RountingKeyCons.STARTWORKFLOW).noargs();
}
/**
* @descriotion 根据配置信息确定 exchange 类型
* @param exchangeEnum [exchangeEnum]
* @return org.springframework.amqp.core.Exchange
*/
private Exchange transExchage(ExchangeEnum exchangeEnum) {
AbstractExchange exchange = null;
switch (exchangeEnum.getExchangeTypeEnum()){
//直连模式
case DIRECT:
exchange = new DirectExchange(exchangeEnum.getExchangeName(), exchangeEnum.isDurable(), exchangeEnum.isAutoDel());
break;
//广播模式:
case FANOUT:
exchange = new FanoutExchange(exchangeEnum.getExchangeName(), exchangeEnum.isDurable(), exchangeEnum.isAutoDel());
break;
//通配符模式
case TOPIC:
exchange = new TopicExchange(exchangeEnum.getExchangeName(), exchangeEnum.isDurable(), exchangeEnum.isAutoDel());
break;
case HEADERS:
exchange = new HeadersExchange(exchangeEnum.getExchangeName(), exchangeEnum.isDurable(), exchangeEnum.isAutoDel(),exchangeEnum.getArguments());
break;
}
exchange.setDelayed(exchangeEnum.isDelayed());
return exchange;
}
}
因为要保证生产者发送的消息能够正确的到达队列,所以需要引入消息确认机制。 消息确认机制的实现有多种方式。
- 配置文件 + 配置rabbitTemplate
# rabbitmq 配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#虚拟host 可以不设置 默认host
virtual-host: /
# 确认消息是否发送到交换机 --- 打开的设置
publisher-confirm-type: correlated
# 确认消息是否发送到队列 --- 打开的设置
publisher-returns: true
template:
# 消息发送失败返回到队列中,yml 文件中需要配置 publisher-returns: true
mandatory: false
-------java 代码
@Slf4j
@Configuration
public class RabbitmqConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback{
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) throws IOException {
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
rabbitTemplate.setMandatory(true);// 无论成功失败,都会确认信息
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
return rabbitTemplate;
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
log.info("【confirm】消息发送成功");
} else {
String id = correlationData==null? null: correlationData.getId();
log.error("【confirm】消息发送失败,相关数据:{},原因:{}", id, cause);
}
}
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.info("【ReturnCallback】消息:" + returnedMessage.getMessage());
log.info("【ReturnCallback】回应码:" + returnedMessage.getReplyCode());
log.info("【ReturnCallback】回应信息:" + returnedMessage.getReplyText());
log.info("【ReturnCallback】交换机:" + returnedMessage.getExchange());
log.info("【ReturnCallback】路由键:" + returnedMessage.getRoutingKey());
}
}
理论上来说,按照上述这种方式配置之后,就应该在 生产者发送消息到 exchange 时,触发confirm() 方法;在消息 通过routingKey 路由到指定的 queue 失败时,触发 returnedMessage() 方法。但是在我实际的实现当中,没有 成功!!! 后面我又测试了其他的几种方式,原理都是一样的,就是给rabbitTemlate 设置 setConfirmCallback()\setReturnsCallback(),不过在发送消息时,就是不生效。 这是为什么呀?经过打断点,去看rabbitTemplate 的属性,发现 connectionFactory 的setPublisherReturns()\setPublisherConfirmType()属性为空;这就可以解释原因了。难道说是配置文件没有生效?于是,我才用了接下来的方式,用java 类实现配置。
@Configuration
public class RabbitMqTemplateConfig {
/** 日志服务 */
public static final Logger log = LoggerFactory.getLogger(RabbitMqTemplateConfig.class);
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private int port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Value("${spring.rabbitmq.virtual-host}")
private String virtualHost;
@Value("${spring.rabbitmq.template.mandatory}")
private boolean mandatory;
@Value("${spring.rabbitmq.publisher-returns}")
private boolean publisherReturns;
@Value("${spring.rabbitmq.publisher-confirm-type}")
private CachingConnectionFactory.ConfirmType publisherConfirmType;
@Bean(name = "connectionFactory")
public CachingConnectionFactory connectionFactory () {
/** 以下 这段代码是为了 配置 set 中的参数 yml 配置文件没有生效 */
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setPort(port);
connectionFactory.setPassword(password);
connectionFactory.setUsername(username);
connectionFactory.setVirtualHost(virtualHost);
connectionFactory.setPublisherReturns(publisherReturns);
connectionFactory.setPublisherConfirmType(publisherConfirmType);
return connectionFactory;
}
@Bean(name = "rabbitTemplate")
public RabbitTemplate rabbitTemplate(@Qualifier("connectionFactory") CachingConnectionFactory cachingConnectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(cachingConnectionFactory);
// 设置开启 mandatory 才能触发回调函数
rabbitTemplate.setMandatory(true);
/** 消息到达 exchange 做一次判断 */
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if(ack){
log.info("消息成功发送,成功不做处理 "+ correlationData.getId());
}else{
log.info("消息发送失败:"+correlationData.getId() + ", 出现异常:"+cause);
}
});
/** exchage 与 queue 的binding 做一次判断 到达不做处理 出错开始执行 */
rabbitTemplate.setReturnsCallback(returnedMessage -> {
log.info("被退回的消息为:{}", returnedMessage.getMessage());
log.info("replyCode:{}", returnedMessage.getReplyCode());
log.info("replyText:{}", returnedMessage.getReplyText());
log.info("exchange:{}", returnedMessage.getExchange());
log.info("routingKey:{}", returnedMessage.getRoutingKey());
});
return rabbitTemplate;
}
}
这样就解决了,发送消息或者发送消息到queue 失败时 回调函数失效的问题。
??? 这里也存在一种可能,就是配置文件的指定可能出现了问题。
上面所说的是生产者端的消息确认,其实为了保证消费端能够正确的消费消息,避免因为网络波动等原因造成消息丢失,在消费端也应该引入消费确认机制。