Redis连接报错套接字地址只允许使用一次
Redis连接报错套接字地址只允许使用一次
报错信息:
[2025-08-17 07:57:38] custom.ERROR: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379] {"exception":"[object] (Predis\Connection\ConnectionException(code: 10048): 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379] at D:\path\to\project\server\vendor\predis\predis\src\Connection\AbstractConnection.php:160)"}
从错误信息看,应该是四元组信息(客户端的IP、客户端使用的端口、服务器的IP、服务器使用的端口)重复了,四元组信息中只有“客户端使用的端口”是动态的,
四元组信息:
客户端的IP:127.0.0.1
客户端使用的端口:创建连接时随机取一个未使用的动态端口
服务器的IP:127.0.0.1
服务器使用的端口:6379
通过 netstat -ano | findstr 6379 检查端口使用情况,有很多状态是 TIME_WAIT 的连接:
TCP 127.0.0.1:50436 127.0.0.1:6379 TIME_WAIT 0
查了下 TIME_WAIT 状态的主要作用:Windows 上的连接断开之后,其本地端口(尤其是动态端口)处于 TIME_WAIT 状态,并且持续一段时间,
- 确保最后一个 ACK 被可靠接收(防止迷失的 ACK 导致问题)
- 允许网络中旧的、延迟的数据包“过期”(防止旧连接的数据包污染新连接)
TIME_WAIT 状态会导致端口的消耗增加,特别是在并发比较高的时候,会在短时间内快速占用大量的动态端口,Windows Server 默认的动态端口数是 16384:
D:\zhpj\Desktop>netsh int ipv4 show dynamicportrange tcp
协议 tcp 动态端口范围
---------------------------------
启动端口 : 49152
端口数 : 16384
D:\zhpj\Desktop>
考虑到可能是动态端口耗尽导致的,就需要再具体排查一下了,在 Redis 连接中,将四元组信息中的客户端信息打印出来,具体位置:vendor/predis/predis/src/Connection/StreamConnection.php 的 createStreamSocket 方法中,连接建立之后,获取一下客户端的连接信息:
protected function createStreamSocket(ParametersInterface $parameters, $address, $flags)
{
$timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0);
if (!$resource = @stream_socket_client($address, $errno, $errstr, $timeout, $flags)) {
$this->onConnectionError(trim($errstr), $errno);
}
if (isset($parameters->read_write_timeout)) {
$rwtimeout = (float) $parameters->read_write_timeout;
$rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1;
$timeoutSeconds = floor($rwtimeout);
$timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000;
stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds);
}
if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) {
$socket = socket_import_stream($resource);
socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay);
}
// 获取客户端的四元组信息
$localAddress = stream_socket_get_name($resource, false);
\Log::info('localAddress = ' . $localAddress);
return $resource;
}
编写一个压测的文件:
<?php
require __DIR__ . '/bootstrap/app.php';
use \Illuminate\Support\Facades\Cache;
$processId = getmypid();
$requestsPerProcess = 20; // 每个线程请求次数
for ($i = 0; $i < $requestsPerProcess; $i++) {
$key = "test_key_{$processId}_{$i}";
try {
Cache::has($key);
} catch (Exception $e) {
echo "进程 {$processId} 出错: " . $e->getMessage() . "\n";
}
usleep(10000); // 10ms
}
echo "进程 {$processId} 完成\n";
先打印下当前 redis 连接的端口状态:
D:\zhpj\Desktop>netstat -anob | findstr 6379
TCP 127.0.0.1:6379 0.0.0.0:0 LISTENING 6428
D:\zhpj\Desktop>
执行测试脚本:
D:\path\to\project\server> php redis_stress.php
进程 4896 完成
D:\path\to\project\server>
再次打印 redis 连接的端口状态:
D:\zhpj\Desktop>netstat -anob | findstr 6379
TCP 127.0.0.1:6379 0.0.0.0:0 LISTENING 6428
TCP 127.0.0.1:55700 127.0.0.1:6379 TIME_WAIT 0
D:\zhpj\Desktop>
日志文件中打印出来的客户端连接信息:
[2025-08-18 13:46:10] custom.INFO: localAddress = 127.0.0.1:55700
连接确实使用的是 55700,脚本执行完之后,端口的连接状态变成 TIME_WAIT。
PHP 中没有多线程,多进程也仅在 Linux 环境下可以支持。Windows 环境下使用批处理文件来模拟多个PHP进程:
@echo off
set THREAD_COUNT=10
set PHP_SCRIPT="redis_stress.php"
for /l %%i in (1,1,%THREAD_COUNT%) do (
start /B php %PHP_SCRIPT%
)
执行批处理文件后,检查 redis 连接的端口状态:
D:\zhpj\Desktop>netstat -anob | findstr 6379
TCP 127.0.0.1:6379 0.0.0.0:0 LISTENING 6428
TCP 127.0.0.1:56205 127.0.0.1:6379 TIME_WAIT 0
TCP 127.0.0.1:56206 127.0.0.1:6379 TIME_WAIT 0
TCP 127.0.0.1:56207 127.0.0.1:6379 TIME_WAIT 0
TCP 127.0.0.1:56208 127.0.0.1:6379 TIME_WAIT 0
TCP 127.0.0.1:56209 127.0.0.1:6379 TIME_WAIT 0
TCP 127.0.0.1:56210 127.0.0.1:6379 TIME_WAIT 0
TCP 127.0.0.1:56211 127.0.0.1:6379 TIME_WAIT 0
TCP 127.0.0.1:56212 127.0.0.1:6379 TIME_WAIT 0
TCP 127.0.0.1:56213 127.0.0.1:6379 TIME_WAIT 0
TCP 127.0.0.1:56221 127.0.0.1:6379 TIME_WAIT 0
D:\zhpj\Desktop>
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56205
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56206
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56207
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56208
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56209
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56210
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56212
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56213
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56211
[2025-08-18 13:50:18] custom.INFO: localAddress = 127.0.0.1:56221
默认的动态端口数 16384 个,打印太多日志不太方便观察,考虑将动态端口数调整小一点:
D:\zhpj\Desktop>netsh int ipv4 set dynamicport tcp start=49152 num=100
参数错误。
D:\zhpj\Desktop>
D:\zhpj\Desktop>
D:\zhpj\Desktop>netsh int ipv4 set dynamicport tcp start=49152 num=255
确定。
D:\zhpj\Desktop>
D:\zhpj\Desktop>netsh int ipv4 show dynamicportrange tcp
协议 tcp 动态端口范围
---------------------------------
启动端口 : 49152
端口数 : 255
D:\zhpj\Desktop>
起初想将动态端口数改为 100,执行时报错了。改为设置成最小值 255 个。
根据微软文档:TCP/IP 的默认动态端口范围,动态端口范围的起始端口(start)必须在1025到65535之间,而端口数量(num)必须至少是255。此外,起始端口加上端口数量不能超过65535。
将批处理脚本中的 THREAD_COUNT 改为 500:
@echo off
set THREAD_COUNT=500
set PHP_SCRIPT="redis_stress.php"
for /l %%i in (1,1,%THREAD_COUNT%) do (
start /B php %PHP_SCRIPT%
)
执行批处理脚本后,日志中复现了错误:通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
脚本中的
echo "进程 {$processId} 出错: " . $e->getMessage() . "\n";改成了Log::info("进程 {$processId} 出错: " . $e->getMessage());echo 直接输出在终端里,批处理脚本执行之后直接退出了。
[2025-08-18 14:01:13] custom.INFO: localAddress = 127.0.0.1:49158
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49194
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49213
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49214
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49216
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49217
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49218
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49219
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49227
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49274
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49292
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]
将动态端口数恢复为默认值:
D:\zhpj\Desktop>netsh int ipv4 set dynamicport tcp start=49152 num=16384
确定。
D:\zhpj\Desktop>
D:\zhpj\Desktop>netsh int ipv4 show dynamicportrange tcp
协议 tcp 动态端口范围
---------------------------------
启动端口 : 49152
端口数 : 16384
D:\zhpj\Desktop>
恢复后再执行批处理脚本是正常的,日志文件中没有产生报错。
方案一:调整动态端口数:(cmd 管理员身份执行,不需要重启服务器)
# 常规应用服务器,使用默认的 16384
netsh int ipv4 set dynamicport tcp start=49152 num=16384
# 高并发服务器,可以考虑设置到 30000+
netsh int ipv4 set dynamicport tcp start=35535 num=30000
# 极端高负载,最大 55535
netsh int ipv4 set dynamicport tcp start=10000 num=55535
start 默认值是 49152,num 的默认值是16384,这里的 start + num <= 65535
方案二:调整 TIME_WAIT 时间(通过修改注册表,将时间从默认 120 秒缩短到 30 秒),可能存在潜在风险,且必须重启服务器才能生效,最好是阶梯式调整:
# 先改为 60 秒
reg add HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters /v TcpTimedWaitDelay /t REG_DWORD /d 60 /f
# 观察 1 周后改为 30 秒
reg add HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters /v TcpTimedWaitDelay /t REG_DWORD /d 60 /f

浙公网安备 33010602011771号