前幾天有人在論壇提出了這樣一個問題:
問答:關於Laravel Cache原子鎖的問題
function () {
$lock = Cache::lock('foo', 10);
if ($lock->get()) {
dump(1);
sleep(10);
$lock->release();
} else {
dump(0);
}
他在測試程式碼中,大概是想同時開啟多個瀏覽器視窗測試併發,所以使用了 sleep(10)
新增了阻塞,來方便測試。
然後讓他感到疑惑的是為什麼在第一個視窗發生阻塞時,他建立的第二個視窗、第三個視窗… 這些視窗返回結果都發生了阻塞?按照他的設想,應該是第一個視窗發生阻塞時,已經取到了鎖,但是再開啟另外一個視窗時,因為沒有取到鎖,應該返回結果是 0
,但是當第一個視窗發生阻塞時,用瀏覽器的「無痕模式」再次訪問,卻可以立即返回結果。
我當時猜想了一下大概是 Session 問題導致的,自己也試了下,確實是這樣,好奇心驅使我去了解一下這背後的原理。
先說結論
這並不是程式碼的問題,和
sleep()
、session
也沒什麼關係,上面的程式碼是正確的,問題在於瀏覽器。
我使用的是 chrome,當時我測試的時候還以為是 sleep()
原因導致的,但其實這是 chrome 瀏覽器的特性:
- 當「視窗1」開啟
https://test.lock
地址時,已經建立了http連線,並且取到了鎖 - 當「視窗2」,輸入相同地址時,chrome 會去「http連線庫」(猜想) 中查詢是否已經與這個地址有過連線請求,如果有的話,會複用這個 http 連線。(複用 http 連線的好處是可以減少網路阻塞,加快響應速度,並且也會減少伺服器資源消耗)
- 但 chrome 複用 http 連線請求是「序列」的,也就是說,在「視窗1」沒有執行完畢之前「視窗2」要排隊等待。
這就解釋了為什麼開啟多個視窗時,同時發生了阻塞,那是因為最開始的「視窗1」因為指令碼中的 sleep()
發生了阻塞遲遲沒有返回結果,所以「視窗2」在等待「視窗1」的響應結束,「視窗2」才會再發起請求。
同時也解釋了為什麼使用「無痕視窗」就會立即返回結果,因為無痕模式下,會建立一個全新的 http 連線,不存在複用的問題。
最簡單的方法,我們直接使用 curl
來測試一下
curl -X GET https://test.lock > output1.txt &
curl -X GET https://test.lock > output2.txt &
我們可以看到返回結果中,獲取鎖失敗的那個請求並沒有發生阻塞,獲取到鎖的那個請求發生了阻塞。由此可見並不是 sleep()
問題導致的。
細說 sleep()
話又說回 sleep()
,說實話,我在開發中幾乎從來沒使用過這個函式,這個函式會導致 session 阻塞。
舉例:
Route::get('/session1', function () {
session_start();
$_SESSION['data'] = 'Test Session';
echo "Session 開啟...\n";
sleep(3);
echo "Session 資料: " . $_SESSION['data'] . "\n";
});
Route::get('/session2', function () {
session_start();
echo "Session 資料: " . $_SESSION['data'] . "\n";
});
上面定義了兩個路由,「session1」 和 「session2」,當開啟 session_start()
開啟會話後,如果 session_wirte_close()
關閉前的程式碼包含 sleep()
,那麼它就會造成阻塞,導致其他請求均無法訪問當前 session 資料。
PHP 的 Session I/O 是序列機制,原理參考上面的 chrome 複用 http 連線。當相同的 session 請求在 session_start()
開啟後,在沒有 session_wirte_close()
之前,其他請求只能等待它寫入關閉,才能再進行訪問。
所以在使用 sleep()
之前,我們要先確保已經關閉了 session 寫入:
Route::get('/session1', function () {
session_start();
$_SESSION['data'] = 'Test Session';
echo "Session 開啟...\n";
session_wirte_close(); // 關閉寫入
sleep(3);
echo "Session 資料: " . $_SESSION['data'] . "\n";
});
Route::get('/session2', function () {
session_start();
echo "Session 資料: " . $_SESSION['data'] . "\n";
});
如何確認到底有沒有複用 http 連線
我們來驗證一下這個問題,首先是 chrome 瀏覽器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chrome</title>
</head>
<body>
<script>
for (let i = 0; i < 6; i++) {
fetch('https://mixo2o.test/lock')
.then(response => response.text())
.then(data => console.log(`Response ${i}:`, data))
}
</script>
</body>
</html>
開啟開發者工具——網路
從阻塞排隊中就可以看出來,因為第一個請求有 sleep()
導致了阻塞,後續的請求都在排隊等待,響應時間都是 20s+,並且只有一條 http 請求。
再來看下 safari
時間線上看:這裡只有一條請求複用了 http 連線,就是靜態 HTML 載入和第一次 ajax 請求,後續的 ajax 請求都是獨立的
再看一下網路請求,沒有因為第一個請求的 sleep()
導致阻塞發生,第一個請求取到鎖後,其他請求都是瞬間返回。
Chrome 連線複用猜想
關於瀏覽器視窗複用 http 連線的問題,可以參考這篇文章
www.zhihu.com/question/554796551
本作品採用《CC 協議》,轉載必須註明作者和本文連結