本文目录导读:

在PHP项目中实现断线重连主要涉及数据库连接和长连接(如WebSocket) 两种情况,以下是基于PHP-FPM短连接模型和常驻进程场景的完整实现方案。
数据库连接断线重连
PHP-FPM通常是短连接,但连接池或常驻进程场景(如Swoole)需要重连机制。
PDO重连方案
<?php
class DatabaseReconnect
{
private $pdo;
private $config;
private $maxRetries = 3;
private $retryDelay = 100; // 毫秒
public function __construct(array $config)
{
$this->config = $config;
$this->connect();
}
private function connect(): void
{
try {
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
$this->config['host'],
$this->config['port'] ?? 3306,
$this->config['database']
);
$this->pdo = new PDO($dsn, $this->config['username'], $this->config['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_TIMEOUT => 5, // 超时5秒
]);
} catch (PDOException $e) {
throw new RuntimeException('Database connection failed: ' . $e->getMessage());
}
}
// 核心:重试执行SQL
public function query(string $sql, array $params = [])
{
$attempts = 0;
while ($attempts < $this->maxRetries) {
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
} catch (PDOException $e) {
// 只对连接相关错误重试
if ($this->isConnectionError($e)) {
$attempts++;
if ($attempts >= $this->maxRetries) {
throw $e; // 重试耗尽,抛出原始异常
}
// 等待后重连
usleep($this->retryDelay * 1000);
$this->reconnect();
} else {
throw $e; // 其他SQL错误直接抛出
}
}
}
}
private function isConnectionError(PDOException $e): bool
{
$errorMsg = $e->getMessage();
$connectionErrors = [
'server has gone away',
'lost connection',
'Error while reading',
'MySQL server has gone away',
'Connection refused',
'broken pipe',
];
foreach ($connectionErrors as $keyword) {
if (stripos($errorMsg, $keyword) !== false) {
return true;
}
}
return false;
}
private function reconnect(): void
{
$this->pdo = null; // 关闭旧连接
$this->connect();
}
}
// 使用示例
$config = [
'host' => 'localhost',
'port' => 3306,
'database' => 'test',
'username' => 'root',
'password' => 'secret',
];
$db = new DatabaseReconnect($config);
$result = $db->query('SELECT * FROM users WHERE id = ?', [1]);
Mysqli重连方案
<?php
class MysqliReconnect
{
private $mysqli;
private $config;
private $maxRetries = 3;
public function __construct(array $config)
{
$this->config = $config;
$this->connect();
}
private function connect(): void
{
$this->mysqli = new mysqli(
$this->config['host'],
$this->config['username'],
$this->config['password'],
$this->config['database'],
$this->config['port'] ?? 3306
);
if ($this->mysqli->connect_error) {
throw new RuntimeException('Connection failed: ' . $this->mysqli->connect_error);
}
$this->mysqli->set_charset('utf8mb4');
}
public function query(string $sql)
{
$attempts = 0;
while ($attempts < $this->maxRetries) {
$result = $this->mysqli->query($sql);
if ($result === false) {
// 检测是否连接断开
if ($this->mysqli->errno === 2006 || $this->mysqli->errno === 2013) {
$attempts++;
$this->reconnect();
continue;
}
throw new RuntimeException('Query error: ' . $this->mysqli->error);
}
return $result;
}
}
private function reconnect(): void
{
$this->mysqli->close();
$this->connect();
}
}
常驻进程场景(Swoole/Workerman)
Swoole协程MySQL断线重连
<?php
use Swoole\Coroutine\MySQL;
use Swoole\Coroutine;
class SwooleMySQLReconnect
{
private $mysql;
private $config;
private $lastPingTime = 0;
private $pingInterval = 60; // 秒
public function __construct(array $config)
{
$this->config = $config;
$this->connect();
}
private function connect(): void
{
$this->mysql = new MySQL();
$connected = $this->mysql->connect([
'host' => $this->config['host'],
'port' => $this->config['port'] ?? 3306,
'user' => $this->config['username'],
'password' => $this->config['password'],
'database' => $this->config['database'],
'charset' => 'utf8mb4',
'timeout' => 5, // 连接超时
]);
if (!$connected) {
throw new RuntimeException('Connect failed: ' . $this->mysql->connect_error);
}
}
public function query(string $sql, array $params = [])
{
$this->ping(); // 自动检测连接
$stmt = $this->mysql->prepare($sql);
if (!$stmt) {
// 如果prepare失败,可能是连接断开
$this->reconnect();
$stmt = $this->mysql->prepare($sql);
}
if (!$stmt) {
throw new RuntimeException('Prepare failed: ' . $this->mysql->error);
}
$result = $stmt->execute($params);
if ($result === false) {
// 如果是连接错误,重连后重试一次
if ($this->mysql->errno === 2006 || $this->mysql->errno === 2013) {
$this->reconnect();
$stmt = $this->mysql->prepare($sql);
$result = $stmt->execute($params);
}
}
return $stmt->fetchAll();
}
private function ping(): void
{
$now = time();
if ($now - $this->lastPingTime > $this->pingInterval) {
if (!$this->mysql->query('SELECT 1')) {
$this->reconnect();
}
$this->lastPingTime = $now;
}
}
private function reconnect(): void
{
$this->mysql->close();
$this->connect();
}
}
Redis断线重连
<?php
class RedisReconnect
{
private $redis;
private $config;
private $maxRetries = 3;
public function __construct(array $config)
{
$this->config = $config;
$this->connect();
}
private function connect(): void
{
$this->redis = new Redis();
if (!$this->redis->connect($this->config['host'], $this->config['port'], 5)) {
throw new RuntimeException('Redis connection failed');
}
if (!empty($this->config['password'])) {
$this->redis->auth($this->config['password']);
}
}
public function __call($method, $args)
{
$attempts = 0;
while ($attempts < $this->maxRetries) {
try {
return call_user_func_array([$this->redis, $method], $args);
} catch (RedisException $e) {
// 检测是否连接断开
if ($this->isConnectionError($e)) {
$attempts++;
$this->reconnect();
continue;
}
throw $e;
}
}
}
private function isConnectionError(RedisException $e): bool
{
$msg = $e->getMessage();
return stripos($msg, 'connection lost') !== false
|| stripos($msg, 'closed') !== false
|| stripos($msg, 'broken pipe') !== false;
}
private function reconnect(): void
{
$this->redis->close();
$this->connect();
}
}
WebSocket长连接断线重连
客户端JavaScript实现
class WebSocketReconnect {
constructor(url, options = {}) {
this.url = url;
this.options = {
maxRetries: 5,
retryDelay: 1000, // 初始延迟1秒
retryDelayMax: 30000, // 最大延迟30秒
...options
};
this.retries = 0;
this.ws = null;
this.connect();
}
connect() {
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.retries = 0; // 重置重试次数
this.options.onOpen?.();
};
this.ws.onclose = (event) => {
console.log('WebSocket disconnected:', event.code);
this.options.onClose?.(event);
// 非正常关闭时自动重连
if (event.code !== 1000 && event.code !== 1001) {
this.reconnect();
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.options.onError?.(error);
};
this.ws.onmessage = (message) => {
this.options.onMessage?.(message);
};
} catch (error) {
console.error('WebSocket connection failed:', error);
this.reconnect();
}
}
reconnect() {
if (this.retries >= this.options.maxRetries) {
console.error('Max retries reached, stop reconnecting');
this.options.onMaxRetries?.();
return;
}
this.retries++;
// 指数退避算法
const delay = Math.min(
this.options.retryDelay * Math.pow(2, this.retries - 1),
this.options.retryDelayMax
);
console.log(`Reconnecting in ${delay}ms (attempt ${this.retries}/${this.options.maxRetries})`);
setTimeout(() => {
this.connect();
}, delay);
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
console.warn('WebSocket not connected, message queued');
this.options.onMessageQueued?.(data);
}
}
close() {
this.retries = this.options.maxRetries; // 防止重连
this.ws?.close(1000, 'Client closed');
}
}
服务端PHP WebSocket重连处理
<?php
// 使用Workerman或Swoole实现
// 这里以Workerman为例
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
require_once __DIR__ . '/vendor/autoload.php';
$ws_worker = new Worker('websocket://0.0.0.0:8080');
$ws_worker->count = 4;
// 连接管理
$connections = new SplObjectStorage();
$ws_worker->onConnect = function(TcpConnection $connection) use ($connections) {
$connections->attach($connection);
echo "New connection: {$connection->id}\n";
};
$ws_worker->onMessage = function(TcpConnection $connection, $data) {
// 处理业务逻辑
echo "Received: $data\n";
// 模拟处理
$response = json_encode(['status' => 'ok', 'data' => $data]);
$connection->send($response);
};
$ws_worker->onClose = function(TcpConnection $connection) use ($connections) {
$connections->detach($connection);
echo "Connection closed: {$connection->id}\n";
// 可以在这里保存断线状态,等待客户端重连
// 保存用户会话到Redis,设置过期时间
};
// 运行worker
Worker::runAll();
最佳实践建议
连接池化
// 示例:使用Swoole连接池
$pool = new Swoole\Database\PDOPool(
(new Swoole\Database\PDOConfig())
->withHost('localhost')
->withPort(3306)
->withDbName('test')
->withUsername('root')
->withPassword('secret')
->withTimeout(5),
$size = 10 // 连接池大小
);
// 连接池会自动管理连接的创建和断开
心跳检测
- 对长连接定期发送PING命令
- 数据库:
SELECT 1 - Redis:
PING - WebSocket:自定义心跳包
指数退避重试
function exponentialBackoff(int $attempt): int
{
$baseDelay = 100; // 100ms
$maxDelay = 30000; // 30s
$delay = $baseDelay * pow(2, $attempt - 1);
return min($delay, $maxDelay) + random_int(0, 50); // 加随机抖动
}
错误分类处理
- 可重试错误:连接超时、服务器重启、网络闪断
- 不可重试错误:SQL语法错误、权限不足、参数错误
监控告警
// 记录重连事件到日志
function logReconnect(string $type, int $attempt, string $error): void
{
$log = sprintf(
"[%s] %s reconnect attempt %d: %s",
date('Y-m-d H:i:s'),
$type,
$attempt,
$error
);
error_log($log, 3, '/var/log/php-reconnect.log');
// 可以发送到监控系统
// sendAlert($log);
}
注意事项
- 事务处理:重连后需要重新开始事务,已执行部分自动回滚
- 数据一致性:确保不会因为重连导致数据重复执行
- 资源释放:重连前一定要关闭旧连接,避免资源泄露
- 并发控制:常驻进程环境注意协程间的连接隔离
- 超时设置:合理设置连接超时、查询超时时间
通过以上方案,可以确保PHP应用在面对网络抖动、数据库重启等情况时保持稳定运行,根据实际场景选择合适的重连策略,并做好监控告警。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播