rabbitmq如何保证消息不丢失_RabbitMQ如何防止消息丢失

b9671ea67870b77f718900bf81f3dfca.png

RabbitMQ防止消息丢失,保证消息传递的可靠性,保证每条消息都正常传输,并最终至少消费一次。

背景:订单支付状态同步,微信、支付宝、银联等第三方平台异步回调之后,进入队列,为其他服务调用提供数据。为了保证支付状态同步业务的可用性,肯定不希望有订单在传递过程中丢失。

问题:什么情况下消息可能丢失呢?

角色:

  • 生产者、
  • RabbitMQ服务、
  • 消费者

(显然,三大主角都有可能演砸)

50c2586982ae463edc1bb80f63482757.png

MQ流程

华丽的下划线 —— 请开始你的表演


主角一出镜:(我是小白:消息生产者)

我要发送消息到 RabbitMQ服务,但是在去的路上(网络),太过颠簸(网络抖动),把自己丢了。(有时候我自己都佩服我我自己)

解决:

1、采用事务机制,要么成功,要么失败。但是这样吞吐量会降低,影响性能。一般不建议采用(所以也不再提供伪代码)事务同步等待。

2、采用confirm 模式,ack 消息确认,收到nack 者消息重发,做补偿处理。

在生产者设置开启 confirm 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ack 消息,告诉你消息接收成功。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你消息接收失败,你需要重新发送。在开启持久化之后,消息先到达交换机、队列、并持久化之后,才会回传ack。异步回调信息确认。可以单条信息回复确认一次,也可以多条信息,回复确认一次。

调用的API:(截取代码,详细代码看下文。不要着急尝试)

 //4.1 选择为 confirm 模式(此模式不可以和事务模式 兼容) self::$channel->confirm_select(); //4.2 设置异步回调消息确认 (生产者 防止信息丢失) self::$channel->set_ack_handler( function (AMQPMessage $message) { echo "Message acked with content " . $message->body . PHP_EOL; self::apiResponse(self::$rabbit_success_code, 'success', $message->body); } ); self::$channel->set_nack_handler( function (AMQPMessage $message) { echo "Message received failed,Please try again:" . $message->body . PHP_EOL; self::apiResponse(self::$rabbit_err_code, 'Message received failed,Please try again', $message->body); } ); //4.3 阻塞等待消息确认 (生产者 防止信息丢失) self::$channel->wait_for_pending_acks();

主角二 MQ 服务:

开始我的表演了,如果告诉我消息要持久化,那么我就记录到磁盘,如果不告诉我,我可默认为你不需要持久化,我要是重启,消息可不恢复,我是老实人,你告诉我干啥,我就干啥。

怎么告诉我需要持久化:(两步走)

  • 声明交换机、队列的时候,参数 durable = true ,让元数据保存
  • 发送消息的时候,设置 deliveryMode = 2
第一步、 //1、声明 交换机 self::$channel->exchange_declare(self::$cache_exchange, self::EXCHANGE_MODEL, false, true, false); //2、声明队列 self::$channel->queue_declare(self::$cache_queue, false, true, false, false, false);第二步、 $msg = new AMQPMessage($data, array( //参数 发送消息的时候将消息的 deliveryMode 设置为 2 (RabbitMQ 持久化 步骤二:消息) 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT )); self::$channel->basic_publish($msg, self::$cache_exchange, self::$cache_queue);
声明:设置之后,我一定保证我接到的数据,肯定会持久化到磁盘,你可以完全放心,我可是著名表演艺术家,从未失误。

主角三、消费者(我是小红,消费是我职责)

MQ 服务:“我把消息给你了啊”

消费者:“接到了”

MQ 服务:“好,我把消息删了啊”

然后,消费者拿到信息之后,开始工作,突然~~~一命呜呼,任务没执行完,消息丢失了,应该完成的任务到此为止。

解决:消费者,消费完之后,回复ack ,确认已消费完。如果超时未回复,那么重新发放消息。也有可能一条消息,被多个消费者消费,这里业务代码要保证幂等性。(幂等性:执行多次等于执行一次,多次执行不影响业务数据)

 $callback = function ($msg){ //console log : Received message self::consoleLog(" [x] Received".$msg->body,0); //执行业务操作 (根据生产者,设定的路由 Http 访问) $data = json_decode($msg->body,true); if(isset($data['url'])) { $url = $data['url'].'?data='.$msg->body; if(strpos($url,'http') !== false) { $result = file_get_contents($url); }else{ $result = 'HTTP not found'; } self::consoleLog($result); }else{ self::consoleLog('Undefined URL'); } //手动 回复队列,message已消费 $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']); }; //只有consumer已经处理,并确认了上一条message时queue才分派新的message给它(非公平分配,如果存在耗时操作,那么也一直等待。现在是空闲领取消息) self::$channel->basic_qos(null, 1, null); //第四个参数 是否自动回应 ack,false 手动回应 self::$channel->basic_consume(self::$rabbit_queue,'',false,false,false,false,$callback);

三个演员的精湛表演,叹为观止,让他们同台演出,享受一番视觉盛宴吧!

1、ProducerClass 投递消息

<?phpnamespace DemoQueueQueueProducer;/** * Created by PhpStorm. * User: runBaby * Date: 2019/5/13 * Time: 11:13 AM */date_default_timezone_set("Asia/Shanghai");require_once __DIR__.'/../../vendor/autoload.php';use PhpAmqpLibWireAMQPTable;use PhpAmqpLibMessageAMQPMessage;use PhpAmqpLibConnectionAMQPStreamConnection;class ProducerClass{ static protected $rabbit_host; static protected $rabbit_port; static protected $rabbit_login; static protected $rabbit_pwd; static protected $rabbit_vhost; static protected $connection; static protected $channel; static protected $rabbit_err_code = 500; static protected $rabbit_success_code = 200; static protected $cache_exchange; static protected $cache_routing; static protected $cache_queue; static protected $config; const EXCHANGE_MODEL = 'fanout'; //交换机模式 public function __construct($cache_exchange,$cache_queue) { self::$config = include_once __DIR__."/../Config/config.php"; self::$config = self::$config['rabbitmq']; self::$rabbit_host = self::$config['host']; self::$rabbit_port = self::$config['port']; self::$rabbit_login = self::$config['login']; self::$rabbit_pwd = self::$config['password']; self::$rabbit_vhost = self:: $config['vhost']; self::$connection = new AMQPStreamConnection(self::$rabbit_host, self::$rabbit_port,self::$rabbit_login, self::$rabbit_pwd, self::$rabbit_vhost); if(!self::$connection->isConnected()) { self::apiResponse(self::$rabbit_err_code,'建立连接失败'); } self::$channel = self::$connection->channel(); if(!self::$channel->is_open()) { self::apiResponse(self::$rabbit_err_code,'通道连接失败'); } if(!$cache_exchange) { self::apiResponse(self::$rabbit_err_code,'请设置交换机名称'); }else{ self::$cache_exchange = $cache_exchange; } if(!$cache_queue) { self::apiResponse(self::$rabbit_err_code,'请设置队列名称'); }else{ self::$cache_queue = $cache_queue; //路由 (同名 队列 借用) self::$cache_routing = self::$cache_queue; } } /** * Explain: 向队列 投递数据 * @param array $send_info * User: runBaby * Date: 2019/5/13 * Time: 11:34 AM * @return bool */ public static function Producer($send_info = array()) { //参数 json 转化 $data = json_encode($send_info); //1、声明 交换机 self::$channel->exchange_declare(self::$cache_exchange, self::EXCHANGE_MODEL, false, true, false); //2、声明队列 self::$channel->queue_declare(self::$cache_queue, false, true, false, false, false); //3、队列绑定 交换机 self::$channel->queue_bind(self::$cache_queue, self::$cache_exchange, self::$cache_routing); //4.1 设置异步回调消息确认 (生产者 防止信息丢失) self::$channel->set_ack_handler( function (AMQPMessage $message) { echo "Message acked with content " . $message->body . PHP_EOL; self::apiResponse(self::$rabbit_success_code, 'success', $message->body); } ); self::$channel->set_nack_handler( function (AMQPMessage $message) { echo "Message received failed,Please try again:" . $message->body . PHP_EOL; self::apiResponse(self::$rabbit_err_code, 'Message received failed,Please try again', $message->body); } ); //4.2 选择为 confirm 模式(此模式不可以和事务模式 兼容) self::$channel->confirm_select(); //5、发送消息 到队列 $msg = new AMQPMessage($data, array( //参数 发送消息的时候将消息的 deliveryMode 设置为 2 (RabbitMQ 持久化 步骤二:消息) 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT )); self::$channel->basic_publish($msg, self::$cache_exchange, self::$cache_queue); //4.3 阻塞等待消息确认 (生产者 防止信息丢失) self::$channel->wait_for_pending_acks(); //请求相应 返回 self::apiResponse(self::$rabbit_success_code, 'success', $data); return true; } /* * 资源返回 */ public static function apiResponse($code= 200 ,$message='默认描述信息',$data=[]) { if(empty($data)){ $data = (object)$data; self::producersLog($message); }else{ self::producersLog($data,$message); } header('Content-Type:application/json; charset=utf-8'); exit(json_encode(['code' => $code, 'message' => $message, 'data' => $data],JSON_UNESCAPED_UNICODE)); } /* * 日志记录 */ public static function producersLog($data = array(),$message = '') { $filename = __DIR__.'/Log/producers/'.date('Y').'/'.date('m').'/'.date('Y-m-d').'.txt'; $dir = dirname($filename); if(!is_dir($dir)) { mkdir($dir,0777,true); } $log_str = '[ '.date('Y-m-d H:i:s').' ]'.""; if($message){ $log_str .= '*** message *** :'.$message.""; } if(gettype($data) == 'array') { $data = json_encode($data); } $log_str .= $data.""; $log_str .= '[end]'.""; file_put_contents($filename,$log_str,FILE_APPEND); return true; } /* * 关闭连接 */ public function __destruct() { // TODO: Implement __destruct() method. self::$channel->close(); self::$connection->close(); }}

2、ConsumersClass 消费消息

<?phpnamespace DemoQueueQueueConsumers;/** * Created by PhpStorm. * User: 奔跑吧笨笨 * Date: 2019/5/6 * Time: 1:04 PM */date_default_timezone_set("Asia/Shanghai");require_once __DIR__.'/../../vendor/autoload.php';use PhpAmqpLibConnectionAMQPStreamConnection;class ConsumersClass{ static protected $rabbit_host; static protected $rabbit_port; static protected $rabbit_login; static protected $rabbit_pwd; static protected $rabbit_vhost; static protected $connection; static protected $channel; static protected $config; static protected $rabbit_err_code = 500; static protected $rabbit_success_code = 200; static protected $rabbit_exchange; static protected $rabbit_queue; static protected $rabbit_routing; const EXCHANGE_MODEL = 'fanout'; //交换机模式 (广播模式) public function __construct($rabbit_exchange,$rabbit_queue) { self::$config = include_once __DIR__."/../Config/config.php"; self::$config = self::$config['rabbitmq']; self::$rabbit_host = self::$config['host']; self::$rabbit_port = self::$config['port']; self::$rabbit_login = self::$config['login']; self::$rabbit_pwd = self::$config['password']; self::$rabbit_vhost = self::$config['vhost']; self::$connection = new AMQPStreamConnection(self::$rabbit_host, self::$rabbit_port,self::$rabbit_login, self::$rabbit_pwd, self::$rabbit_vhost); if(!self::$connection->isConnected()) { self::apiResponse(self::$rabbit_err_code,'建立连接失败'); } self::$channel = self::$connection->channel(); if(!self::$channel->is_open()) { self::apiResponse(self::$rabbit_err_code,'通道连接失败'); } if($rabbit_exchange) { self::$rabbit_exchange = $rabbit_exchange; }else{ self::apiResponse(self::$rabbit_err_code,'请选择Exchange'); } if($rabbit_queue) { self::$rabbit_queue = $rabbit_queue; //同名 借用 路由 self::$rabbit_routing = $rabbit_queue; }else{ self::apiResponse(self::$rabbit_err_code,'请选择Queue'); } } /* * 消费者:客户端 * 消费队列消息,并基于HTTP API路由转发到相应业务代码 */ public static function consumersClient() { //1、声明交换机 self::$channel->exchange_declare(self::$rabbit_exchange, self::EXCHANGE_MODEL,false,true,false); //2、声明队列 self::$channel->queue_declare(self::$rabbit_queue,false,true,false,false,false); //3、交换机和队列 绑定 self::$channel->queue_bind(self::$rabbit_queue, self::$rabbit_exchange,self::$rabbit_routing); //console log : Start to work self::consoleLog(); $callback = function ($msg){ //console log : Received message self::consoleLog(" [x] Received".$msg->body,0); //执行业务操作 (根据生产者,设定的路由 Http 访问) $data = json_decode($msg->body,true); if(isset($data['url'])) { $url = $data['url'].'?data='.$msg->body; if(strpos($url,'http') !== false) { $result = file_get_contents($url); }else{ $result = 'HTTP not found'; } self::consoleLog($result); }else{ self::consoleLog('Undefined URL'); } //手动 回复队列,message已消费 $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']); }; //只有consumer已经处理,并确认了上一条message时queue才分派新的message给它(非公平分配,如果存在耗时操作,那么也一直等待。现在是空闲领取消息) self::$channel->basic_qos(null, 1, null); //第四个参数 是否自动回应 ack,false 手动回应 self::$channel->basic_consume(self::$rabbit_queue,'',false,false,false,false,$callback); //进入等待状态 while (count(self::$channel->callbacks)) { self::$channel->wait(); } return true; } /* * Console log */ protected static function consoleLog($message = ' [*] Waiting for message. To exit press CTRL+C ',$type = 1) { $message = date('Y-m-d H:i:s').$message; //输出到 控制台 echo $message.PHP_EOL; //是否记录文件日志 if($type === 1) { self::Logs($message); } return true; } /* * API 返回 */ protected static function apiResponse($code= 200 ,$message='默认描述信息',$data=[]) { if(empty($data)){ $data = (object)$data; self::Logs($message); }else{ self::Logs($data); } header('Content-Type:application/json; charset=utf-8'); exit(json_encode(['code' => $code, 'message' => $message, 'data' => $data],JSON_UNESCAPED_UNICODE)); } /* * 日志记录 */ protected static function Logs($message = '') { $filename = __DIR__.'/Log/consumers/'.date('Y').'/'.date('m').'/'.date('Y-m-d').'.log'; $dir = dirname($filename); if(!file_exists($dir)) { @mkdir($dir,0777,true); } $log_str = '[ '.date('Y-m-d H:i:s').' ]'.""; $log_str .= '*** message *** :'.$message.""; $log_str .= '[end]'.""; file_put_contents($filename,$log_str,FILE_APPEND); return true; } /* * 销毁连接 */ public function __destruct() { // TODO: Implement __destruct() method. self::$channel->close(); self::$connection->close(); }}

3、PClient.php 执行调用class

<?phpnamespace DemoQueueQueueProducer;/** * Created by PhpStorm. * User: runBaby * Date: 2019/5/13 * Time: 11:37 AM */include_once __DIR__.'/ProducerClass.php';$data['type'] = 1;$data['data'] = 'Hello world!88888';$exchange = 'demo_exchange';$queue = 'demo_queue';$Producer = new ProducerClass($exchange,$queue);$result = $Producer::Producer($data);var_dump($result);

4、CClient.php 执行调用class

<?phpnamespace DemoQueueQueueConsumers;/** * Created by PhpStorm. * User: runBaby * Date: 2019/5/6 * Time: 1:41 PM */include_once __DIR__.'/ConsumersClass.php';$exchange = 'demo_exchange'; //延迟交换机$queue = 'demo_queue'; //延迟队列$consumers = new ConsumersClass($exchange,$queue);$consumers::consumersClient();

这里看代码不舒服,请异步githup :欢迎star

https://github.com/runBaby/RabbitMQ_Delay/tree/master/App/Queue

a2b5481d4c0923e78297046b83d953a7.png

左手代码,右手诗,趣味学习新知识

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值