我們在開發或者設計一個網站的時候,經常會遇到要簡訊群發,或者群發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
執行失敗了,將值還lpush
到push_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);
}
}
更專業的訊息佇列,你可以使用:RabbitMQ
,ctiveMq
,eroMq
,Kafka
,這裡就不過多的去介紹這些了。
本作品採用《CC 協議》,轉載必須註明作者和本文連結