在Rabbitmq使用过程单中,由于网络波动,服务宕机等不稳定因素,可能导致消息丢失;如何解决上述问题,增强mq的可靠性?
查询官网说明:
RabbitMQ是由Erlang语言开发,所以在虚拟机上安装erlang。
erlang跟rabbitmq有版本对应关系,找到适配的版本安装,版本对应关系查看:
RabbitMQ Erlang Version Requirements — RabbitMQ
Erlang下载:wget https://github.com/rabbitmq/erlang-rpm/releases/tag/v23.3.4.4/erlang-23.3.4.4-1.el7.x86_64.rpm --no-check-certificate
rabbitmq下载:wget https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.8.27/rabbitmq-server-3.8.27-1.el7.noarch.rpm
本人选择的mq版本需要socat,下载socat:
wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
安装socat:
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
安装erlang:rpm -ivh erlang-23.3.4.4-1.el7.x86_64.rpm
安装rabbitmq:rpm -ivh rabbitmq-server-3.8.27-1.el7.noarch.rpm
启动mq并设置开机自启:
systemctl start rabbitmq-server && systemctl enable rabbitmq-server
客户端访问插件安装:
rabbitmq-plugins enable rabbitmq_management
添加用户:
Rabbitmqctl add_user root root1234,添加了一个账号名root 密码root1234的用户
rabbitmqctl set_user_tags root administrator 将root设置成管理员
客户端访问:
Exchange4种type解释(headers忽略):
springcloud上演示消息不丢失
springcloud 集成rabbitmq
演示服务:base-info
common模块下pom添加依赖
com.rabbitmq amqp-client org.springframework.boot spring-boot-starter-amqp RELEASE org.springframework.amqp spring-rabbit
base-info下yml文件修改:
common服务下创建连接配置类:
@Configuration public class RabbitMQConfig { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Value("${spring.rabbitmq.host}") private String host; @Value("${spring.rabbitmq.username}") private String username; @Value("${spring.rabbitmq.password}") private String password; @Value("${spring.rabbitmq.port}") private String port; @Bean public ConnectionFactory connectFactory(RabbitProperties rabbitProperties){ CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); connectionFactory.setUsername(username); connectionFactory.setPassword(password); connectionFactory.setHost(host); connectionFactory.setPort(Integer.parseInt(port)); connectionFactory.setVirtualHost("/"); connectionFactory.setPublisherConfirms(true);//发布确认,设置此参数后,消息发送后不管成功与否都会调用生产者的回调方法 connectionFactory.setPublisherReturns(true); return connectionFactory; } }
rabbitmq消息传递流程:
生产者产生消息->rabbitmq broker->exchange->queue->消费者消费消息
从上述流程可以看出,消息丢失的环节有:
a.生产者往mq推送时,丢失
b.mq本身将数据丢失
c.消费者消费失败了,mq直接删除了消息
保证生产者往mq推送方案:整体思路是,在生产者服务种记录推送的消息,将推送失败的消息通过计划任务的方式重新推送,如果超过一定的重试次数还未成功,人工干预查看错误原因
创建推送日志表:
CREATE TABLE `rabbit_msg_send_log` (
`msg_id` varchar(50) NOT NULL,
`msg_body` blob NOT NULL COMMENT '发送的消息体',
`send_response_msg` varchar(3000) DEFAULT NULL COMMENT 'mq应答信息',
`send_flag` int(2) NOT NULL DEFAULT '1' COMMENT '0:发送失败,1:发送成功',
`retry_times` int(2) DEFAULT '0' COMMENT '重试次数',
`created_date` datetime DEFAULT NULL COMMENT '创建时间',
`updated_time` datetime DEFAULT NULL,
PRIMARY KEY (`msg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
创建一个通用的消息发送类:
/** * 消息生产者 */ @Component public class RabbitMQSender { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired RabbitTemplate rabbitTemplate; @Autowired IRabbitMsgSendLogService rabbitMsgSendLogService; public void msgSend(Object msg) { try{ String msgId = UUID.randomUUID().toString(); logger.info("消息Id:{}",msgId); CorrelationData correlationData = new CorrelationData(msgId); rabbitTemplate.setMandatory(true); rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean ack, String reason) { QueryWrapperqueryWrapper = new QueryWrapper<>(); queryWrapper.eq("msg_id",correlationData.getId()); List rabbitMsgSendLogList = rabbitMsgSendLogService.listObjs(queryWrapper); RabbitMsgSendLog log = null; if(rabbitMsgSendLogList.size()>0){ log = rabbitMsgSendLogList.get(0); log.setUpdatedTime(new Date()); }else{ log = new RabbitMsgSendLog(); log.setCreatedDate(new Date()); } log.setMsgId(correlationData.getId()); log.setMsgBody(msg); if(ack){//mq成功接收消息 log.setSendFlag(1); }else{//给异常发送的数据在日志种打上标记,后面用定时任务扫描重新发送 log.setSendFlag(0); } rabbitMsgSendLogService.save(log); } }); //exchange到queue失败,调用 rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchangeName, String routerKey) { logger.info("消息{}", GsonUtil.obj2Json(message)); logger.info("code{}", replyCode); logger.info("回复消息体{}", replyText); logger.info("交换器{}", exchangeName); logger.info("路由key{}", routerKey); } }); rabbitTemplate.convertAndSend("jywtest","test.router.key",msg,correlationData); }catch (Exception e){ } } }
通过回调函数ConfirmCallback获取mq是否成功接收的状态。
创建计划任务找到重试次数小于等于6且推送失败的消息:
mq本身导致消息丢失:exchange、queue持久化,将交换器、队列、消息持久化到mq服务器磁盘上
方式一,客户端直接操作
public static void main(String[] args) throws Exception{ ConnectionFactory factory = new ConnectionFactory(); factory.setUsername("root"); factory.setPassword("root1234"); factory.setHost("192.168.1.119"); factory.setPort(5672); factory.setVirtualHost("/"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel();//创建信道 //声明交换器 参数1:交换器名称 参数2:交换器类型 参数3:是否持久化 channel.exchangeDeclare("exchangename1","topic",true); //声明消息队列 参数1:队列名称 参数2:是否持久化 参数3:是否排他 参数4:如未有绑定交换器是否删除 参数5:其他参数 channel.queueDeclare("queuename1",true,false, false, null); //交换器与队列绑定 参数1:消息队列名称 参数2:交换器名称 参数3:绑定的路由key channel.queueBind("queuename1","exchangename1","exchange_queue_bind_routerkey"); //创建完毕后,向此消息队列推送消息 String msg = "hello,rabbitmq "; //消息内容 String routing_key = "exchange_queue_bind_routerkey"; //发送消息使用的routing-key channel.basicPublish("exchangename1",routing_key,null,msg.getBytes()); channel.close(); connection.close(); }
消费者消息丢失:
mq默认是自动ack,即不管消费者是否成功消费消息,mq都会在内存中删除对应的消息。
改为手动ack后,消费者成功消费后,通知mq在内存中删除,没有通知就一直在unacked中等待
消费者ack改为手动yml配置:
/** * mq消息接受类 */ @Component public class RabbitMQReciver implements ChannelAwareMessageListener { private Logger logger = LoggerFactory.getLogger(this.getClass()); private MessageConverter messageConverter = new SimpleMessageConverter(); @Autowired RabbitTemplate rabbitTemplate; @Autowired RedisTemplateredisTemplate; @RabbitListener(queues = "queuesTest") @Override public void onMessage(Message message, Channel channel) throws Exception { //重复消费 丢弃 if(redisTemplate.hasKey(message.getMessageProperties().getMessageId())){ return ; } Object obj = messageConverter.fromMessage(message); logger.info("获取到的消息:{}", GsonUtil.obj2Json(obj)); logger.info("被消费的消息id:{}",message.getMessageProperties().getMessageId()); //手动ack channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); redisTemplate.opsForValue().set(message.getMessageProperties().getMessageId(),null,1800, TimeUnit.SECONDS); } }