RabbitMQ防止消息丢失,保证消息传递的可靠性,保证每条消息都正常传递,并最终至少消费一次。
背景:订单支付状态同步,微信、支付宝、银联等第三方平台异步回调之后,进入队列,为其他服务调用提供数据。为了保证支付状态同步业务的可用性,肯定不希望有订单在传递过程中丢失。
问题:什么情况下消息可能丢失呢?
角色:生产者、RabbitMQ服务、消费者 (显然,三大主角都有可能演砸)
华丽的下划线 —— 请开始你的表演
主角一出镜:
我要发送消息到 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 投递消息
namespace DemoQueue\Queue\Producer;
/**
* 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 PhpAmqpLib\Wire\AMQPTable;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Connection\AMQPStreamConnection;
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').' ]'."\r\n";
if($message){
$log_str .= '*** message *** :'.$message."\r\n";
}
if(gettype($data) == 'array')
{
$data = json_encode($data);
}
$log_str .= $data."\r\n";
$log_str .= '[end]'."\r\n";
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 消费消息
namespace DemoQueue\Queue\Consumers;
/**
* 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 PhpAmqpLib\Connection\AMQPStreamConnection;
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').'.txt';
$dir = dirname($filename);
if(!file_exists($dir))
{
@mkdir($dir,0777,true);
}
$log_str = '[ '.date('Y-m-d H:i:s').' ]'."\r\n";
$log_str .= '*** message *** :'.$message."\r\n";
$log_str .= '[end]'."\r\n";
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
namespace DemoQueue\Queue\Producer;
/**
* 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_test11';
$queue = 'demo_queue_test11';
$Producer = new ProducerClass($exchange,$queue);
$result = $Producer::Producer($data);
var_dump($result);
4、CClient.php 执行调用class
namespace DemoQueue\Queue\Consumers;
/**
* Created by PhpStorm.
* User: runBaby
* Date: 2019/5/6
* Time: 1:41 PM
*/
include_once __DIR__.'/ConsumersClass.php';
$exchange = 'demo_exchange_test11'; //延迟交换机
$queue = 'demo_queue_test11'; //延迟队列
$consumers = new ConsumersClass($exchange,$queue);
$consumers::consumersClient();
原文链接:https://blog.csdn.net/qq_37837134/java/article/details/90172003