訊息佇列應用在哪些場景裡 ,這篇文章講得很詳細

Laravel00發表於2021-12-08

我們在開發或者設計一個網站的時候,經常會遇到要簡訊群發,或者群發email,或者給系統的所有使用者傳送站內信,或者在訂單系統裡,我們要記錄大量的日誌。如果我們的系統是電商系統,在做搶購,秒殺的活動的設計的時候,伺服器在高併發下,根本就無法承受這種瞬間的壓力等等,很多例子。。。那如果遇到這些問題,如何保證系統能夠正常有效的執行,我們該如何去設計,如何去處理呢?

這個時候我們就要用到訊息佇列來處理這類問題。可以說訊息佇列是一箇中介軟體,用這種中介軟體來分流與解壓各種併發帶來的壓力。那麼什麼是訊息佇列呢?

耳熟能詳的訊息佇列(原理)
訊息佇列其實就是一個佇列結構的中介軟體,也就是說把訊息和內容放入到一個容器後,就可以直接的返回了,不理會等它後期處理的結果,容器裡的內容會有另一個程式按照順序進行逐個的去處理。

一個訊息佇列結果是這樣的過程:
由一個業務系統進行入隊,把訊息(內容)逐個插入訊息佇列中,插入成功之後直接返回成功的結果,然後後續有一個訊息處理系統,這個系統會把訊息佇列中的記錄逐個進行取出並且進行處理,進行出隊的操作。

訊息佇列有哪些應用場景
訊息佇列主要運用在冗餘,解耦,流量削峰,非同步通訊,還有一些擴充套件性,排序保證等,下面我們詳細來了解一下這些特性

資料冗餘
比如一個訂單系統,訂單很多的時候,到後續需要嚴格的轉換和記錄,這個時候訊息佇列就可以把這些資料持久化儲存在佇列中,然後由訂單處理程式進行獲取,後續處理完成之後再把這條記錄刪除,保證每條記錄都能處理完成。

系統解耦
訊息佇列分離了兩套系統:入隊系統和出隊系統,解決了兩套系統深度耦合的問題。使用訊息佇列後,入隊的系統和出隊的系統是沒有直接的關係的,入隊系統和出隊系統其中一套系統崩潰的時候,都不會影響到另一個系統的正常運轉。

我們用一個系統解耦的案例來詳細講解一下:佇列處理訂單系統和配送系統
場景:在網購的時候提交訂單之後,看到自己的訂單貨物在配送中,這樣就參與進來一個系統是配送系統,如果我們在做架構的時候,把訂單系統和配送系統設計到一起,就會出現問題。首先對於訂單系統來說,訂單系統處理壓力較大,對於配送系統來說沒必要對這些壓力做及時反映,我們沒必要在訂單系統出現問題的情況下,同時配送系統出現問題,這時候就會同時影響兩個系統的運轉,所以我們可以用解耦來解決。

這兩個系統分開之後,我們可以通過一個佇列表來實現兩個系統的溝通。首先,訂單系統會接收使用者的訂單,進行訂單的處理,會把這些訂單寫到佇列表中,這個佇列表是溝通兩個系統的關鍵,由配送系統中的定時執行的程式來讀取佇列表進行處理,配送系統處理之後,會把已經處理的記錄進行標記,這就是整個詳細流程。

具體細節設計如下(Mysql佇列舉例):

首先,我們用order.php的檔案接收使用者的訂單。
然後生成訂單號並對訂單進行處理,訂單系統處理完成之後會把配送系統需要的資料增加到佇列表中。

訂單表

CREATE TABLE `order_queue` (  
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '訂單的id號',  
  `order_id` int(11) NOT NULL,  
  `mobile` varchar(20) NOT NULL COMMENT '使用者的手機號',  
  `address` varchar(100) NOT NULL COMMENT '使用者的地址',  
  `created_at` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '訂單建立的時間',  
  `updated_at` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '物流系統處理完成的時間',  
  `status` tinyint(2) NOT NULL COMMENT '當前狀態,0 未處理,1 已處理,2處理中',  
  PRIMARY KEY (`id`)  
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

然後有一個定時指令碼,每分鐘啟動配送處理程式,配送處理程式:goods.php用來處理佇列表中的資料,當處理完成之後,會把佇列表中的欄位狀態改為處理完成,這樣就結束了整個流程。

具體程式碼如下:
1 處理訂單的order.php檔案

<?php   
include 'class/db.php';  

if(!empty($_GET['mobile'])){  
    $order_id = rand(10000,99999).date("YmdHis").'688';  
    $insert_data = array(  
        'order_id'=>$order_id,  
        'mobile'=>$_GET['mobile'],      //記得過濾
        'created_at'=>date('Y-m-d H:i:s',time()),  
        'order_id'=>$order_id,  
        'status'=>0,    //0,未處理狀態
    );  
    $db = DB::getIntance();  
//把資料放入佇列表中
    $res = $db->insert('order_queue',$insert_data);  
    if($res){  
        echo $insert_data['order_id']."儲存成功";  
    }else{  
        echo "儲存失敗";  
    }  
}else{  
    echo "1";  
}  
?>

配送系統處理訂單的檔案goods.php

<?php   
//配送系統處理訂單並進行標記
include 'class/db.php';  
$db = DB::getIntance();  
//1:先要把要處理的資料狀態改為待處理
$waiting = array('status'=>0,);  
$lock = array('status'=>2,);  
$res_lock = $db->update('order_queue',$lock,$waiting,2);  
//2:選擇出剛剛更新的資料,然後進行配送系統處理
if($res_lock){  
    //選擇出要處理的訂單內容
    $res = $db->selectAll('order_queue',$lock);  
     //然後由配貨系統進行處理.....等操作
    //3:把處理過的改為已處理狀態
    $success = array(  
        'status'=>1,  
        'updated_at'=>date('Y-m-d H;i:s',time()),    
    );  
    $res_last = $db->update('order_queue',$success,$lock);  
    if($res_last){  
       echo "處理成功:".$res_last;   
    }else{  
        echo "處理失敗:".$res_last;  
    }  
}else{  
    echo "全部處理完成";  
}  
?>

定時執行指令碼的goods.sh,每分鐘執行一次

#!/bin/bash  
date "+%G-%m-%d %H:%M:%S"    //當前年月日
cd /data/wwwroot/default/mq/  
php goods.php  

然後crontab任務定時執行指令碼,並建立日誌檔案,然後指定輸出格式

*/1 * * * * /data/wwwroot/default/mq/good.sh >> /data/wwwroot/default/mq/log.log 2>&1 //指定指令碼目錄並格式化輸出//當然要建立log.log檔案

監控日誌

tail -f log.log  //監控日誌

這樣訂單系統和配送系統j就是相互獨立的咯,並不影響另一個系統的正常執行,這就是系統解耦處理.

流量削峰
這種場景最經典的就是秒殺和搶購,這種情況會出現很大的流量劇增,大量的需求集中在短短的幾秒內,對伺服器的瞬間壓力非常大,我們配合快取redis使用訊息佇列來有效的解決這種瞬間訪問量,防止伺服器頂不住而崩潰。

我們也用一個案例來了解了解:使用Redis的List型別實現秒殺。
我們會用到redis的這些函式:

RPUSH/RPUSHX:將值插入到連結串列的尾部。同上,位置相反
LPOP:移除並獲取連結串列中的第一個元素。
RPOP:移除並獲取連結串列中最後一個元素。
LTRIM:保留指定區間內的元素。
LLEN:獲取連結串列的長度。
LSET:用索引設定連結串列元素的值。
LINDEX:通過索引獲取連結串列中的元素。
LRANGE:獲取連結串列指定範圍內的元素

場景
記錄哪個使用者參與了秒殺,同時記錄時間,這樣方便後續處理,使用者的ID會儲存到【Redis】的連結串列裡進行排隊,比如打算讓前10個人秒殺成功,後面的人秒殺失敗,這樣讓redis連結串列的長度保持為10就可以了,10個以後如果再到redis請求追加資料,那麼程式上拒絕請求,在redis存取之後,後面的程式會對redis進行取值,因為資料不能長久放在快取,後面有一個程式遍歷處理redis的值,放入資料庫永久儲存,因為秒殺本來不會太長,可以用指令碼迴圈掃描。

詳細說明:
首先Redis程式會把使用者的請求資料放入redis,主要是uid和微秒時間戳;然後檢查redis連結串列的長度,超出長度就放棄處理;死迴圈資料讀取redis連結串列的內容,入庫。

秒殺記錄表設計:

CREATE TABLE `redis_queue` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `uid` int(11) NOT NULL DEFAULT '0',
  `time_stamp` varchar(24) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

接收使用者請求的程式:

<?php

$redis = new Redis();
$redis->connect('127.0.0.1',6379);
$redis-_name = 'miaosha';

//秒殺使用者湧入模擬,500個使用者
for ($i =0; $i < 500; $i++) {
    $uid = rand(1000000,99999999);
}
//檢查redis連結串列長度(已存在數量)
$num = 10;
if ($redis->lLen($redis_name) < 10 ) {
    //加入連結串列尾部
    $redis->rPush($redis_name, $uid.'%'.microtime());
} else {  //如果達到10個
    //秒殺結束
}
$redis->close();

處理程式(拿到redis資料寫入資料表裡)

<?php
//從佇列頭部讀一個值,判斷這個值是否存在,如果存在則切割出時間、uid儲存到資料庫中。(對於redis而言,如果從redis取出這個值,那麼這個值就不在redis佇列裡了,如果出現問題失敗了,那麼我們需要有一個機制把失敗的資料重新放入redis連結串列中)
$redis = new Redis();
$redis->connect('127.0.0.1',6379);
$redis-_name = 'miaosha';

//死迴圈檢測redis佇列
while(1) {
    $user = $redis->lpop($redis_name);
    if (!$user || $user == 'null') {  //如果沒有資料跳出迴圈
        //如果一直執行,速度是非常快的,那麼伺服器壓力大,這裡2秒一次
        sleep(2);
        //跳出迴圈
        continue;
    } 
    //拿出微秒時間戳和uid
    $user_arr = explode('%', $user);
    $insert_data = array(
        'uid' => $user_arr[0];
        'time_stamp' => $user_arr[1];
    );
    $res = $db->insert('redis_queue', $insert_data);
    //如果插入失敗
    if (!$res) {
        //從哪個方向取出,從哪個方向插回
        $redis->lpush($redis_name, $user);
        sleep(2);
    }
} 
$redis->close();

測試的話,可以先執行迴圈檢測指令碼,然後再執行秒殺指令碼開始測試,監測Mysql資料庫的變化。

非同步通訊
訊息本身可以使入隊的系統直接返回,所以實現了程式的非同步操作,因此只要適合於非同步的場景都可以使用訊息佇列來實現。

下面來看具體案例:
基本知識點
重點用到了以下命令實現我們的訊息推送。

  • brpop 阻塞模式 從佇列右邊獲取值之後刪除
  • brpoplpush 從佇列A的右邊取值之後刪除,從左側放置到佇列B中

邏輯分析

  • 在普通的任務指令碼中寫入push_queue佇列要傳送訊息的目標,併為目標設定一個要推送的內容,永不過期
  • RedisPushQueue中brpoplpush處理,處理後的值放到temp_queue,主要防止程式崩潰造成推送失敗
  • RedisAutoDeleteTempqueueItems處理temp_queue,這裡用到了brpop

程式碼: 普通任務指令碼

<?php
foreach ($user_list as $item) {
  //命名規則 業務型別_操作_ID_隨機6位 值 自定義 我自定義的是"推送內容"
  $k_name = 'rabbit_push_' . $item['uid'].'_'.rand(100000,999999);
  $redis->lPush('push_queue',$k_name);//左進佇列
  $redis->set($k_name, '推送內容');
}

RedisPushQueue

<?php
//訊息佇列處理推送~
//
 // 守護程式執行
 // nohup php YOURPATH/RedisPushQueue.php & 開啟守護程式執行,修改檔案之後需要從新啟動
// blpop 有值則回去 沒值則阻塞 主要就是這個函式在起作用 不過並不安全,程式在執行過程中崩潰就會導致佇列中的內容
 // 永久丟失~
 // BRPOPLPUSH 阻塞模式 右邊出 左邊進 在填寫佇列內容的時候要求從左進入
 //
ini_set('default_socket_timeout', -1); //不超時
require_once 'YOURPARH/Rongcloud.php';

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(2);//切換到db2
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);

// temp_queue臨時佇列防止程式崩潰導致佇列中內容丟失 0代表永不超時!
While ($key = $redis->brpoplpush('push_queue', 'temp_queue', 0)) {
  if ($val = $redis->get($key)) {
    //rabbit_push_20_175990
    $arr = explode('_', $key);
    if (count($arr) != 4) {
      continue;
    }
    $id = $arr[2];
    push($id, $val);
    //刪除key內容
    $redis->del($key);
  } 
}
function push($id, $v)
{
 //推送操作~
}

RedisAutoDeleteTempqueueItems
自動處理temp_queue中的元素,這個操作是防止RedisPushQueue崩潰的時候做處理。

處理思路是 使用brpop 命令阻塞處理temp_queue這個佇列中的值,如果能獲取到”值”對應的”值”,說明RedisPushQueue執行失敗了,將值還lpushpush_queue中,以備從新處理

至於為什麼使用brpop命令,是因為在RedisPushQueue中我們使用的是brpoplpushnohup php YOURPATH/RedisAutoDeleteTempqueueItems.php & 開啟守護程式執行,修改檔案之後需要從新啟動

<?php

ini_set('default_socket_timeout', -1); //不超時
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->select(2);//切換到db2
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
  while($key_arr = $redis->brPop('temp_queue',0)){
  if(count($key_arr) != 2){
    continue;
  }
  $key =$key_arr[1];
  if($redis->get($key)){//能獲取到值 說明RedisPushQueue執行失敗
    $redis->lPush('push_queue',$key);
  }
}

更專業的訊息佇列,你可以使用:RabbitMQctiveMqeroMqKafka,這裡就不過多的去介紹這些了。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
溫馨提示:微信搜尋並關注   學Laravel  裡面有兩套Laravel課程:【laravel7.x 從入門到核心架構講解】 與 【Laravel高階實戰教程42集】,直接獲取就可以了

相關文章