公司是作棋牌遊戲的。前段時間接到一個後臺人工鑑定並處理通牌做弊玩家的需求,其中須要根據幾個玩家的遊戲ID查詢並計算他們在某段時間內彼此之間玩牌輸贏次數和輸贏總額。javascript
牌局數據是存儲在日誌中心的,他們把牌局數據分紅兩個表來存儲,一個表存儲牌局概況數據,例如牌局時間、牌局ID、桌子ID、用戶ID等信息,另外一個表則存儲每個牌局的詳情數據,例如,牌局有多少玩家參與,荷官在哪一輪發了什麼牌,玩家每一輪都有什麼動做等等。要想計算出幾個玩家在某段時間以內玩牌輸贏次數和輸贏總額,就須要知道每個牌局的詳情數據,因此須要針對每個玩家的遊戲ID,先查詢第一個表,查出全部牌局概況數據列表,而後遍歷這個列表,根據每一個牌局的牌局ID、桌子ID,從第二個表中查詢每一個牌局的詳情數據,全部玩家的全部牌局詳情數據都查詢完成以後再進行統計。php
日誌中心的同窗給出了查詢以上兩個表的接口,其中牌局詳情的查詢接口一次只能查詢一個牌局的數據(和他們使用的數據表設計有關)。剛開始個人作法是在js代碼中遍歷全部給出的玩家ID,先查詢出每一個玩家的牌局列表,而後使用第二層循環來調用接口請求每個牌局的詳情數據,但這樣作的問題是,有些用戶在某段時間內的牌局數量是很大的,儘管控制了查詢時間段的最大範圍,但仍是出現了一個用戶幾千個牌局的狀況,這就意味着瀏覽器須要幾乎在同一時間內對同一個域名的服務器發出幾千個請求,而瀏覽器是基於域名進行併發控制的,超過限制數量的請求會被阻塞,阻塞嚴重的時候常常致使頁面變成空白,好長時間才恢復,獲得查詢結果。這樣的體驗顯然是不行的。css
那怎麼辦呢?在老大的指導下,幾經思慮,決定採用PHP多線程結合socket.io來完成這個任務。總體思路是這樣的:首先js向PHP發起數據查詢請求,PHP收到請求以後不是直接進行數據查詢,而是在後臺掛載一個進程去處理請求,而後返回一個確認狀態值給js,這時js請求暫時結束了。這樣作好處有二:其一,js請求的PHP接口是php-fpm運行的,使用php-fpm來fork多進程不太穩定,而使用php比較穩定;其二,能夠避免數據查詢過程時間太長致使超時。html
掛載進程代碼示例:前端
1
2
3
4
5
6
7
|
<?php
$par
= [
'startTime'
=>
''
,
'endTime'
=>
''
,
'mids'
=>
$mid
, ...];
//牌局查詢參數
$pKey
=
'plog_proccess'
;
//傳給命令行的參數,做爲進程標識,便於查詢統計當前進程數量
$php
=
'/usr/local/php/bin/php'
;
//php執行文件路徑
$file
=
'/www/query.php'
;
//牌局查詢腳本文件
$cmd
=
$php
.
' '
.
$file
.
' '
.
$pKey
.
' '
.
base64_encode
(serialize(
$par
)).
' > /dev/null 2>&1 &'
;
//命令
system(
$cmd
);
//執行命令,掛載後臺進程執行查詢
|
接下來就要在進程運行的PHP腳本/www/query.php中進行數據查詢了。首先遍歷每個玩家ID,查出每一個玩家的全部牌局列表,而後遍歷每一個玩家的牌局列表,fork多個子進程進行每一個牌局詳情數據的查詢了,一個子進程負責查詢一個牌局的詳情數據,並將數據寫入文件中,代碼示例以下:(注意:如下代碼只是基本代碼框架,沒法直接運行)java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
<?php
$pKey
=
$argv
[1];
$par
= unserialize(
base64_decode
(
$argv
[2]));
$mids
=
$par
[
'mids'
];
$max_pnum
= 100;
//最大子進程數量,避免搶佔了過多的資源
for
(
$mids
as
$mid
) {
//遍歷查詢各個用戶的牌局數據
$list
= ...;
//這裏進行當前用戶牌局列表數據查詢
$num
=
count
(
$list
);
//牌局總數
$count
= 0;
//已有多少個牌局在查詢
while
(true) {
//fork多個子進程查詢各個牌局的詳情數據
$s
=
"ps aux|awk '"
. '/query.php/ && /
' . $pKey . '
/ && !/awk/
' . "'
|wc -l";
ob_start();
system(
$s
);
$pNum
= (int)ob_get_clean();
//當前查詢進程數量
if
(
$count
>=
$num
) {
//當前牌局列表都已經交給各個子進程查詢了
if
(
$pnum
> 1) {
//有子進程沒有完成,稍等
sleep(3);
continue
;
}
else
{
//全部子進程都已經完成,退出while循環,回到for循環中查詢下一個用戶的牌局數據
break
;
}
}
else
if
(
$pNum
>
$max_pnum
) {
//子進程數量超出限制,稍等
sleep(3);
continue
;
}
$rs
=
$list
[
$count
];
//從牌局列表中取出一個牌局來進行牌局詳情數據查詢
pcntl_signal(SIGCHLD, SIG_IGN);
$pid
= pcntl_fork();
//fork一個子進程,子進程會今後位置開始執行
if
(
$pid
< 0) {
//子進程建立失敗
//這裏能夠作一些日誌記錄
exit
(0);
}
if
(
$pid
) {
//子進程建立成功(主進程邏輯)
$count
++;
}
else
if
(
$pid
== 0) {
//進行牌局詳情數據查詢(子進程邏輯)
$pid
= posix_setsid();
//子進程ID
//這裏根據$rs中的牌局數據進行牌局詳情查詢,並將獲得的數據寫入當前子進程專屬文件(文件路徑+文件名要惟一,可使用時間戳、桌子ID和牌局ID組合表示)
exit
(0);
//當前子進程任務完成,退出
}
}
}
exit
(0);
//查詢完成,主進程退出
|
這個PHP後臺掛載進程執行完成以後,全部須要查詢的牌局數據就已經所有寫入文件中了。如今問題來了,PHP應該怎麼把這些數據傳給前端頁面呢?咱們知道http協議是單向協議,只能由前端向服務器主動發起請求,而服務器是沒法主動把數據發送給前端的,那怎麼辦呢?使用socket.io!能夠在全部子進程執行完成以後,經過socket.io使用當前sock鏈接通知js,js收到消息以後即發送請求給一個PHP接口,這個PHP接口的任務即是讀取上述多進程在文件中寫下的數據,返回給js進行頁面渲染。json
關於socket.io,沒有進行過多研究,使用的是公司框架封裝好的,固然也可使用原生的,簡單教程地址:http://www.workerman.net/phpsocket_io,這裏只是簡單介紹一下思路。
首先須要到上面這個地址中下載phpsocket,而後啓動一個服務端,注意,只能在命令行中啓動,一樣能夠做爲一個後臺掛載進程運行。瀏覽器
1
2
3
4
5
6
7
8
9
10
11
12
|
<?php
require_once
__DIR__ .
'/socketio/vendor/autoload.php'
;
use
Workerman\Worker;
use
PHPSocketIO\SocketIO;
//建立socket.io服務器,監聽2021端口
$io
=
new
SocketIO(2021);
//向客戶端發送消息,通知數據已查詢完成
$io
->emit(
'hello'
, json_encode([1 =>
'hello'
,
'aaa'
=>
'ewfewr'
]));
Worker::runAll();
|
而後在客戶端js中監聽這個消息:緩存
1
2
3
4
5
6
7
|
<script src=
'https://cdn.bootcss.com/socket.io/2.0.3/socket.io.js'
></script>
<script>
var
socket = io(
'http://127.0.0.1:2021'
);
socket.on(
'hello'
,
function
(par){
//這裏即是發送請求到PHP接口進行數據讀取了
});
</script>
|
如果以爲使用原生socket.io麻煩,也可使用封裝好的ElephantIO。服務器
固然,這裏有個問題,就是寫數據產生的文件會愈來愈多,能夠在每次掛載進程進行寫文件以前先把以前寫的文件(已經沒用了的)進行刪除:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
function
rmDataDir(
$dir
) {
if
(!
is_dir
(
$dir
))
return
;
$handle
= opendir(
$dir
);
while
(
$file
= readdir(
$handle
)) {
if
(in_array(
$file
, [
'.'
,
'..'
]))
continue
;
$str
=
$dir
.
$file
;
if
(
is_dir
(
$str
)) {
rmDataDir(
$str
.
'/'
);
}
else
{
unlink(
$str
);
}
}
closedir
(
$handle
);
$arr
= scandir(
$dir
);
//readdir()有時候沒有識別完全部文件就返回false了。。。
if
(
count
(
$arr
) <= 2) {
//只有.和..的時候能夠刪除
rmdir
(
$dir
);
}
}
|
同時,因爲在這個功能中,每次發送查詢數據請求的代價都是比較昂貴的,能夠考慮在js中對查詢過的數據進行緩存,例如,相同查詢條件下相同用戶ID,已經查詢過的就不須要查詢了,直接從js緩存中讀取數據進行頁面渲染就能夠了。
然而,儘管使用了PHP多進程,可是進行了不少的文件讀寫操做,磁盤IO也是很耗時間的,因此速度上並無提高多少,只是不會再出現瀏覽器頁面卡死的狀況了。這個功能中關於速度的提高不知還有什麼更好的方法呢???各位朋友,走過路過,別忘了給下建議哈~