小貼士:我們這裡討論的是多程式單佇列這種資料型別,順序性主要是針對消費者消費資料一方。
前言
在我們日常的開發中,經常會使用佇列來處理一些非及時性業務,佇列具有解耦、非同步、削峰等特性,但是同步也會業務要滿足冪等性、順序性等要求。對於單程式來消費佇列的資料是不存在順序性的問題的,因為程式是串聯執行的。但是我們為了加快佇列的消費速度一般都是會使用多程式來消費(比如說 Laravel 的佇列)。
多程式消費問題
有這樣一個應用場景,redis 佇列中有 100 條資料。我們為了提供消費資料的速度以多程式的形式去消費佇列的資料,然後寫入檔案中(假設消費很費力哦),我們簡單使用程式碼來模擬一下整個過程。
往佇列中寫入資料
// 寫入資料 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); for($i=3;$i<100;$i++) { $redis->Rpush('we_queue','data is '. $i); }
消費者消費資料指令碼
// 消費資料 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $pid = getmypid(); while(1) { if ($data = $redis->Lpop('we_queue')) { file_put_contents('1.log', "[$pid]".$data. PHP_EOL, 8); } }
supervisor啟動四個程式消費
# 佇列程式 [program:helloword] process_name=%(program_name)s_%(process_num)02d command=php 1.php numprocs=4 directory=/data/cmd
程式碼執行完成我們來看一下最後輸出到
1.log
的檔案內容(資料是從 3 開始的哦)#cat /data/cmd/1.log [1465]data is3 [1463]data is4 [1465]data is5 [1463]data is7 [1465]data is8 [1464]data is6 [1466]data is9 [1463]data is10 [1465]data is11 [1464]data is12 [1463]data is14 [1465]data is15 [1464]data is16 [1466]data is17 [1465]data is18 [1463]data is19 [1466]data is21 [1464]data is20 [1465]data is22 [1463]data is23 [1466]data is24 [1463]data is27 [1465]data is26 [1464]data is25 [1466]data is29 [1464]data is31 [1465]data is30 [1464]data is34 [1463]data is32 [1465]data is35 [1466]data is33 [1464]data is36 [1463]data is37 [1465]data is38 [1466]data is39 [1464]data is40 [1465]data is42 [1464]data is44 [1463]data is41 [1466]data is43 [1465]data is45 [1466]data is48 [1463]data is47 [1464]data is46 [1463]data is51 [1465]data is49 [1466]data is50 [1463]data is53 [1465]data is55 [1466]data is54 [1464]data is56 [1465]data is58 [1464]data is60 [1466]data is59 [1463]data is57 [1465]data is61 [1464]data is62 [1463]data is64 [1466]data is65 [1463]data is68 [1464]data is67 [1463]data is71 [1465]data is70 [1464]data is72 [1466]data is69 [1463]data is73 [1466]data is76 [1465]data is74 [1464]data is75 [1463]data is77 [1466]data is78 [1464]data is80 [1465]data is79 [1464]data is83 [1466]data is82 [1463]data is81 [1465]data is84 [1464]data is85 [1463]data is87 [1465]data is88 [1464]data is89 [1466]data is86 [1465]data is91 [1464]data is92 [1463]data is90 [1466]data is93 [1463]data is96 [1465]data is94 [1464]data is95 [1466]data is97 [1463]data is98 [1465]data is99
可以簡單的看一下上面輸出的檔案,和我們想象的檔案內容不一樣,主要有以下幾個現象:
資料並不是按照我們想的那樣是按照順序輸出的 3-99。
這就是佇列順序性的表現,並沒有按我們所預想的一樣,究其原因就是消費者消費資料的業務處理的時候並沒有保證順序性,入上面的例子所示,也就是我們寫入檔案並沒有順序性執行
程式並不是想我們想的那樣4個程式應該平均拿到前4條資料。
根本原因是由於我們的測試資料只有100條。每個程式都有不確定的因素導致並沒有真正的四個程式都啟動了(比如說網路連線等),當我們把資料測試到 100000 的時候時候,資料基本上都是每一個程式都能獲取到(這不是我們今天的主角,大家知道就行了)。
以上我們可知,要想滿足順序性多程式消費,必須消費者的業務處理來保證順序性。可是我們也知道業務處理許多不確認的因素(有網路請求等等),沒辦法保證強順序性。
強順序性
前面我們說了,業務的不確定性因素無法保證強順序性,走的遠了我們不能忘了我們為了什麼出發,前面我們說了單程式消費資料的時候沒有順序性問題,為了提供佇列消費資料的速度使用多程式引來了順序性的問題。換句話說就是我們可以對需要順序性執行的資料使用一個程式來消費。
上面這句話大家可能會有些不懂,我們以如下圖的應用場景描述,我們佇列中有如下的 6 條資料,id
代表的商品主鍵,action
代表了對商品的操作。我們有 4 個程式去消費資料。這樣就像我們前面所說的那樣,無法保證順序性,也就是說對於 id=1
的商品來說有可能會出現先 delete
在 update
,好像這樣沒問題問題,反正最終都是刪除了。但是對於 id=2
的商品可沒有那麼幸運了,它不能出現先update
在 add
。
舞臺已經佈置好了,我們來具體描述一下 需要順序性執行的資料使用一個程式來消費 的含義。我們只需要是的 id=1
的商品在同一個程式執行,id=2
的商品在同一個程式執行,依此類推就好了。對於這個應用場景來說相同 id 的訊息必須順序執行。是的,我們只需要把 id 相同的訊息分發到一個程式就好了。這樣我們就能滿足強順序性的要求。
我們看一下修改的圖,我們加入了一箇中間層它的職責就是需要順序性執行的資料使用一個程式來消費。這樣我們就達到了順序性也能使用多程式來加快去消費資料。也無需去關心業務的那些可變因素導致無法滿足順序性。
專案應用
對於我們平常使用的 Laravel 佇列來說,我們與第三方通過訊息中介軟體來對接的時候,我們分為以下兩種情況給大家描述:
不需要強順序性業務
對於這樣的業務,多程式消費的時候我們業務無需太關注順序性的問題,我們一般都會把資料持久化,一般持久化的軟體都給這種情況有所考慮(其實這就是解決併發問題,Mysql 通過排他鎖來解決,Elasticsearch 可以通過版本號來解決)。
必須強順序性業務
對於必須強順序性的業務,我們只需要本著需要順序性執行的資料使用一個程式來消費目的就能解決順序性問題。可有如下幾種方案:
- 多佇列單程式,用多佇列數來加快消費資料速度,用單程式來解決順序性
- 實現一個佇列進行分發(master-work型別,多 work 加快消費資料速度,master進行有規則分發解決順序性,規則的依舊就是需要順序性執行的資料使用一個程式來消費,就是就是我們上面畫的圖)
每種方案都有各自的優缺點,應結合專案本身去選擇合理的方案。
本作品採用《CC 協議》,轉載必須註明作者和本文連結