10.微服务-分布式事务最终一致性(三)


对应的步骤
在这里插入图片描述
看看代码实现流程,支付成功后,首先是pay网关收到支付成功的消息,调用rpc接口,预发送消息,更新订单状态,存储发送消息,发送消息到中间件,由中间件把消息传递给消费端的监听队列,最终增加用户的积分,更新订单状态和增加积分虽然不是同时进行,但是最终的结果是一致的,就是分布式事务的最终一致性

存储预发送消息(主动方应用系统)

先看看pay网关的order函数,预发送消息,然后通过rpc调用messge微服务的prepareMsg方法预存储消息,然后rpc调用order微服务的update方法更新订单状态,如果成功就rpc调用messge微服务的confirmTosend方法确认并发送消息到消息中间件

public function order()
    {

        //预发送消息(消息状态子系统)
        $msg_id=session_create_id(md5(uniqid()));
        $data=[
            'msg_id'=>uniqid(),
            'version'=>1,
            'create_time'=>time(),
            'message_body'=>['order_id'=>12133,'shop_id'=>2],
            'notify_url'=>'http://127.0.0.1:9804/notify/index',//通知地址
            'notify_rule'=>[1=>5,2=>10,3=>15],//单位为秒
            'notify_retries_number'=>0, //重试次数,
            'default_delay_time'=>1,//毫秒为单位
            'status'=>1, //消息状态
        ];
//        var_dump($this->notifyService->publish($data));
//        return ['1'];


        $prepareMsgData=[
            'msg_id'=>$msg_id,
            'version'=>1,
            'create_time'=>time(),
            'message_body'=>['order_id'=>12133,'shop_id'=>2],
            'consumer_queue'=>'order', //消费队列(消费者)
            'message_retries_number'=>0, //重试次数,
            'status'=>1, //消息状态
         ];

        //预存储消息
        $result = $this->messageService->prepareMsg($prepareMsgData);

        $data=[
            'order_id'=>1,
            'msg_id'=>$msg_id
        ];
        //调用订单服务更新状态
        if ($result['status'] == 1) {  //消息恢复子系统(查询未确认消息)          确认并且投递
               $this->orderService->update($data)['status']==1 && $this->messageService->confirmMsgToSend($msg_id,1);//更新订单
        }
        //确认并且投递消息
        return [$result];
    }
查询消费确认超时的消息(消息恢复子系统)

然后在message微服务设置一个监听事件,用做恢复消息子系统,定义一个定时器,查询超时未确认的任务,消息状态为2表示已经投递给消息中间件,但是没有被消费,判断一下这条消息的投递次数,如果超过两次就不用让它继续投递了,因为如果消息一直失败,会对服务器造成很大压力,把这条消息从redis删掉,存到某个队列当中,可以人工手动进行恢复,如果消息没有超过投递次数,就继续投递消息;如果消息的状态为1,表示已经进入消息子系统但是未投递的,这时候确认订单状态,如果订单状态更新了,就继续投递消息,如果订单状态没有更新成功,直接删除这条消息


public function handle(EventInterface $event): void
{
 //多进程自己实现下
 $time=1; //超时任务
 swoole_timer_tick(10000,function ()use($time){

     sgo(function()use($time){
         //自动初始化一个Context上下文对象(协程环境下)
         $context = ServiceContext::new();
         Context::set($context);
         try{
             //查询超时未确认的任务
             $service=$this->connection->zRangeByScore('message_system_time', "-inf", (string)(time()-$time));
             var_dump(conunt($service));
             foreach ($service as $v){
                 $data=$this->connection->hget('message_system',(string)$v);
                 if(!empty($data)){
                     $data=json_decode($data,true);
                     if($data['status']==2){ //状态为2代表的是已投递,超时没有被正确消费(消息恢复系统)
                         //尝试重新投
                         //如果投递的次数超过最大值,删除任务,并且存到单独存到redis一个队列当中
                         if($data['message_retries_number']>=2){
                              //var_dump($data['message_retries_number'],"投递失败,手动重试");
                              //可以封装成服务
                             $this->connection->transaction(function (\Redis $redis) use ($v,$data) {
                                 $redis->hdel("message_system", (string)$v);
                                 $redis->zrem("message_system_time", (string)$v);
                                 //放在某个队列当中,在消息管理子系统当中可以手动恢复
                                 $redis->lPush("message_system_dead",json_encode($data));
                             });
                         }
                         $this->messageService->confirmMsgToSend($v,2); //投递业务
                     }elseif($data['status']==1){ //消息状态子系统(已经进入消息子系统但是未投递的)
                         $stateJob=$this->orderService->confirmStatus($v);
                         //1.查询任务结果(主动方任务是成功的,第一次投递到被动方的服务)
                         if($stateJob['status']==1){
                             $this->messageService->confirmMsgToSend($v,1); //投递业务
                         }elseif($stateJob['status']==0){ //当前任务是失败的任务,删掉
                             //3.任务失败(删除任务)
                             $this->messageService->ackMsg($v);
                         }
                     }
                 }
             }
             //判断任务的状态是预发送,并且确认消息状态,如果主动方任务成功,我们就投递,否则删除
             //确认业务状态,业务成功投递,业务失败删除
         }catch (\Exception $e){
              var_dump($e->getMessage());
         }

     });

 });

}
确认并发送消息(主动方应用系统)

消息投递到消息中间件的监听队列,是通过confirmToSend这个函数,用的是rabbitmq这个中间件,创建队列,创建交换机,绑定消息交换机和队列,从redis获取这条消息的相关数据,如果flag为2,说明是被恢复过的消息,投递次数+1,发布消息到交换机当中,并绑定好路由关系,设置消息在redis中的状态,并将消息投递给MQ(实时消息队列),成功则投递成功,失败或者消息数据为空则投递失败

public function confirmMsgToSend($msg_id, $flag): array
    {
        try {
            $connection = $this->rabbit->connect();
            $connectionRabbit = $connection->connection;
            $exchangeName = 'tradeExchange';
            $routeKey = '/trade';
            $queueName = 'trade';
            $channel = $connectionRabbit->channel();
            /**
             * 创建队列(Queue)
             * name: hello         // 队列名称
             * passive: false      // 如果设置true存在则返回OK,否则就报错。设置false存在返回OK,不存在则自动创建
             * durable: true       // 是否持久化,设置false是存放到内存中的,RabbitMQ重启后会丢失
             * exclusive: false    // 是否排他,指定该选项为true则队列只对当前连接有效,连接断开后自动删除
             *  auto_delete: false // 是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除
             */
            $channel->queue_declare($queueName, false, true, false, false);
            /**
             * 创建交换机(Exchange)
             * name: vckai_exchange// 交换机名称
             * type: direct        // 交换机类型,分别为direct/fanout/topic,参考另外文章的Exchange Type说明。
             * passive: false      // 如果设置true存在则返回OK,否则就报错。设置false存在返回OK,不存在则自动创建
             * durable: false      // 是否持久化,设置false是存放到内存中的,RabbitMQ重启后会丢失
             * auto_delete: false  // 是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除
             */
            $channel->exchange_declare($exchangeName, \PhpAmqpLib\Exchange\AMQPExchangeType::DIRECT, false, true, false);

            // 绑定消息交换机和队列
            $channel->queue_bind($queueName, $exchangeName);
            $data = $this->connectionRedis->hget("message_system", (string)$msg_id);
            if (!empty($data)) {
                $data = json_decode($data, true);
                $data['status'] = 2;
                if ($flag == 2) {
                    //被消息恢复子系统投递的任务
                    $data['message_retries_number'] = $data['message_retries_number'] + 1;
                }
                $data = json_encode($data);
                /**
                 * 创建AMQP消息类型
                 * delivery_mode 消息是否持久化
                 * AMQPMessage::DELIVERY_MODE_NON_PERSISTENT  不持久化
                 * AMQPMessage::DELIVERY_MODE_PERSISTENT      持久化
                 */
                $msg = new \PhpAmqpLib\Message\AMQPMessage($data, ['delivery_mode' => \PhpAmqpLib\Message\AMQPMessage:: DELIVERY_MODE_NON_PERSISTENT]);
                //发布消息到交换机当中,并且绑定好路由关系
                if ($this->connectionRedis->hset("message_system", (string)$msg_id, $data) == 0 && $channel->basic_publish($msg,$exchangeName, $routeKey) == null) {
                    //将消息投递给MQ(实时消息服务)
                    $data = ['status' => 1, 'result' => '确认并且投递成功'];
                } else {
                    $data = ['status' => 0, 'result' => '确认投递失败'];
                }
            } else {
                $data = ['status' => 0, 'result' => '确认投递失败'];
            }
            $channel->close();
            $connection->release(true);
            return $data;
        } catch (\Exception $e) {
            var_dump($e->getFile(), $e->getLine(), $e->getMessage());
        }
    }
确认消息已被成功消费(被动方应用系统)

消息投递到实时消息队列后,定义一个消费端的监听事件,来消费这些消息,从而增加用户的积分,先连接rabbitmq,队列绑定交换机跟路由,调用basic_consume取到消息队列中的数据,根据消息的id从redis中取到这条消息的积分消息状态,如果状态为2,说明消费成功了,如果状态为1,说明正在进行中,如果没有相关的状态,先设置消息在redis中的积分消息状态为1(setex消费幂等:同一个任务执行10次跟执行一次的效果是一样),这是模拟让我在执行中,然后设置消息状态为2,调用message微服务消费成功的方法ackMsg从redis中删除消息并返回消费成功,最后basic_ack确认这条消息,表示这条已经被消费,wait表示消息的消费是阻塞等待的

public function handle(EventInterface $event): void
{
 //注册服务
 $config = bean('config')->get('provider.consul');
 bean('consulProvider')->registerServer($config);
 $callBack = function () {
     go(function () {
         $context = ServiceContext::new();
         \Swoft\Context\Context::set($context);
         $exchangeName = 'tradeExchange';
         $routeKey = '/trade';
         $queueName = 'trade';
         $connection=$this->rabbit->connect();
         $connectionRabbit=$connection->connection;
         $channel = $connectionRabbit->channel();

         $channel->queue_declare($queueName, false, true, false, false);
         $channel->exchange_declare($exchangeName, \PhpAmqpLib\Exchange\AMQPExchangeType::DIRECT, false, true, false);

         //队列绑定交换机跟路由
         $channel->queue_bind($queueName, $exchangeName, $routeKey);

         $channel->basic_consume($queueName, '', false, false, false, false, function ($message) {
             $data = json_decode($message->body, true);
             //1.记录消息任务是否完成(消费幂等:同一个任务执行10次跟执行一次的效果是一样)
             $statusJob = $this->connectionRedis->get("integrating_message_job", (string)$data['msg_id']);
             if ($statusJob == 2) { //已经消费成功了
                 $this->messageService->ackMsg($data['msg_id']); //这个任务已经消费成功了
             } elseif ($statusJob == 1) { //任务正在执行
                 var_dump("任务正在执行当中");
                 return;
             }else{
                 //执行任务当中,并且设置释放的时间
                 $this->connectionRedis->setex("integrating_message_job:".$data['msg_id'],10,1);
                 //sleep(5);//任务正在执行当中

                 //2.操作mysql更新积分(业务逻辑执行完毕)
                 $this->connectionRedis->set("integrating_message_job:".$data['msg_id'] , 2);//执行任务完毕
                 $this->messageService->ackMsg($data['msg_id']); //这个任务已经消费成功了
                 //回应ack
                 $message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag']);
                 var_dump($data);
             }
         });

         // Loop as long as the channel has callbacks registered
         while ($channel->is_consuming()) {
             $channel->wait(); //阻塞消费
         }

         /**
          * @param \PhpAmqpLib\Channel\AMQPChannel $channel
          * @param \PhpAmqpLib\Connection\AbstractConnection $connection
          */
         function shutdown($channel, $connection)
         {
             $channel->close();
             $connection->close();
         }
          register_shutdown_function('shutdown', $channel, $connection);
     });


 };
 //        //防止进程意外崩溃,回收子进程
 \Swoole\Process::signal(SIGCHLD, function ($sig) use ($callBack) {
     while ($ret = \Swoole\Process::wait(false)) {
         $p = new  \Swoole\Process($callBack);
         $p->start();
     }
 });

 $p = new  \Swoole\Process($callBack);
 $p->start();
 }

看看ackMsg方法,从redis中删除该条消息,并返回消费成功

/**
     * 消息消费成功
     * @return array
     */
    public function ackMsg($msg_id): array
    {
        //删除已确认消费的消息

        $result = $this->connectionRedis->transaction(function (\Redis $redis) use ($msg_id) {
            $redis->hdel("message_system", (string)$msg_id);
            $redis->zrem("message_system_time", (string)$msg_id);
        });
        if ($result[0] !== false) {
            $data = ['status' => 1, 'result' => '任务消费成功'];
        } else {
            $data = ['status' => 0, 'result' => '任务消费失败'];
        }
        return $data;
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值