RabbitMQ防止消息丢失PHP实现

15 篇文章 0 订阅

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  投递消息

<?php
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  消费消息

<?php
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

<?php
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

<?php
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();

如果喜欢,请githup 给个start,谢谢!

源代码 githup Code

 

 

 

 

我为人人,人人为我,美美与共,天下大同。

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值