一个有趣的锁问题
前几天有人在论坛提出了这样一个问题:
问答:关于 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://lock.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 协议》,转载必须注明作者和本文链接
我在写这篇帖子的时候,也用到了其他浏览器,发现只有 chrome 会这样,感兴趣的可以自己测试下,比如 safari。
推荐文章: