本文目录导读:

在 PHP 中实现异步日志写入,主要有以下几种方案,我按推荐程度和使用场景来说明:
使用消息队列(推荐)
Redis 队列方案
<?php
class AsyncLogger {
private $redis;
private $queueKey;
public function __construct($redisConfig, $queueKey = 'php_logs') {
$this->redis = new Redis();
$this->redis->connect($redisConfig['host'], $redisConfig['port']);
$this->queueKey = $queueKey;
}
// 写入日志(异步,立即返回)
public function log($level, $message, array $context = []) {
$logData = [
'timestamp' => date('Y-m-d H:i:s'),
'level' => $level,
'message' => $message,
'context' => json_encode($context),
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'uri' => $_SERVER['REQUEST_URI'] ?? ''
];
// 推送到 Redis 队列,LPUSH 后立即返回
$this->redis->lPush($this->queueKey, json_encode($logData));
return true;
}
}
// 使用示例
$logger = new AsyncLogger(['host' => '127.0.0.1', 'port' => 6379]);
$logger->log('error', '数据库连接失败', ['db' => 'user', 'code' => 500]);
消费者脚本(单独进程运行)
<?php
// consumer.php - 通过 cron 或 supervisor 运行
class LogConsumer {
private $redis;
private $queueKey;
private $logFile;
private $batchSize = 100;
public function __construct($redisConfig, $logFile) {
$this->redis = new Redis();
$this->redis->connect($redisConfig['host'], $redisConfig['port']);
$this->queueKey = 'php_logs';
$this->logFile = $logFile;
}
public function consume() {
while (true) {
$logs = [];
// 批量取出日志
for ($i = 0; $i < $this->batchSize; $i++) {
$log = $this->redis->rPop($this->queueKey);
if ($log === false) break;
$logs[] = json_decode($log, true);
}
if (empty($logs)) {
sleep(1); // 队列为空时等待
continue;
}
$this->writeToFile($logs);
}
}
private function writeToFile(array $logs) {
$content = '';
foreach ($logs as $log) {
$content .= sprintf(
"[%s] %s: %s %s\n",
$log['timestamp'],
strtoupper($log['level']),
$log['message'],
$log['context'] ? ' | Context: ' . $log['context'] : ''
);
}
// 使用 flock 防止并发写入冲突
$fp = fopen($this->logFile, 'a');
if (flock($fp, LOCK_EX)) {
fwrite($fp, $content);
flock($fp, LOCK_UN);
}
fclose($fp);
}
}
// 运行消费者
$consumer = new LogConsumer(
['host' => '127.0.0.1', 'port' => 6379],
'/var/log/app/application.log'
);
$consumer->consume();
基于 RabbitMQ(更可靠)
<?php
class RabbitMQLogger {
private $connection;
private $channel;
public function __construct($config) {
$this->connection = new AMQPConnection($config);
$this->channel = new AMQPChannel($this->connection);
// 创建交换机
$exchange = new AMQPExchange($this->channel);
$exchange->setName('logs_exchange');
$exchange->setType(AMQP_EX_TYPE_TOPIC);
$exchange->declareExchange();
// 创建队列
$queue = new AMQPQueue($this->channel);
$queue->setName('logs_queue');
$queue->declareQueue();
$queue->bind('logs_exchange', 'log.*');
}
public function log($level, $message) {
$data = [
'timestamp' => time(),
'level' => $level,
'message' => $message
];
$exchange = new AMQPExchange($this->channel);
$exchange->publish(
json_encode($data),
'log.' . $level,
AMQP_NOPARAM,
['delivery_mode' => 2] // 持久化消息
);
return true;
}
public function __destruct() {
$this->channel->close();
$this->connection->close();
}
}
使用 Swoole 协程(高性能)
<?php
// 需要安装 Swoole 扩展
class SwooleAsyncLogger {
private $logDir;
public function __construct($logDir) {
$this->logDir = $logDir;
Swoole\Runtime::enableCoroutine();
}
public function log($level, $message, array $context = []) {
$logData = [
'timestamp' => date('Y-m-d H:i:s'),
'level' => $level,
'message' => $message,
'context' => $context
];
// 使用协程异步写入
go(function() use ($logData) {
$file = $this->logDir . '/' . date('Y-m-d') . '.log';
$content = json_encode($logData) . PHP_EOL;
// 使用异步文件写入
$fp = fopen($file, 'a');
if (flock($fp, LOCK_EX)) {
fwrite($fp, $content);
flock($fp, LOCK_UN);
}
fclose($fp);
});
return true;
}
}
简化版:使用 fastcgi_finish_request()
如果只是想在前端快速响应,但仍在同一请求周期内写入:
<?php
class NonBlockingLogger {
public function log($message) {
// 使用 fastcgi_finish_request() 后仍在执行
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request(); // 先发送响应给客户端
}
// 这里在响应发送后才执行
usleep(100000); // 模拟耗时操作
error_log($message);
return true;
}
}
使用第三方服务
日志文件守护进程方案
<?php
class FileAsyncLogger {
private $logFile;
public function __construct($logFile) {
$this->logFile = $logFile;
}
public function log($level, $message) {
// 使用非阻塞文件写入
$fp = fopen($this->logFile, 'a');
stream_set_blocking($fp, false); // 设置为非阻塞
$data = sprintf(
"[%s] [%s] %s\n",
date('Y-m-d H:i:s'),
strtoupper($level),
$message
);
fwrite($fp, $data);
fclose($fp);
return true;
}
}
最佳实践建议
日志级别配置
<?php
class LoggerConfig {
const DEBUG = 100;
const INFO = 200;
const WARNING = 300;
const ERROR = 400;
const CRITICAL = 500;
private $minLevel = self::INFO;
public function shouldLog($level) {
return $level >= $this->minLevel;
}
}
内存缓冲区(批量写入)
<?php
class BufferedLogger {
private $buffer = [];
private $maxBufferSize = 50;
private $flushThreshold = 10; // 秒
public function log($level, $message) {
$this->buffer[] = [
'timestamp' => microtime(true),
'level' => $level,
'message' => $message
];
// 达到缓冲区大小立即刷新
if (count($this->buffer) >= $this->maxBufferSize) {
$this->flush();
}
// 注册关闭时的刷新
register_shutdown_function([$this, 'flush']);
return true;
}
private function flush() {
if (empty($this->buffer)) return;
// 批量写入 Redis 或文件
$logs = $this->buffer;
$this->buffer = [];
// 实际的写入操作...
error_log(json_encode($logs));
}
}
总结对比
| 方案 | 可靠性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| Redis 队列 | 高 | 高 | 中 | 生产环境首选 |
| RabbitMQ | 很高 | 高 | 中高 | 大规模分布式系统 |
| Swoole 协程 | 高 | 很高 | 高 | 高性能应用 |
| fastcgi_finish_request | 低 | 中 | 低 | 简单场景 |
| 文件非阻塞 | 低 | 中 | 极低 | 开发环境 |
推荐方案:生产环境中,使用 Redis 队列 + 消费者进程 是最成熟可靠的方案,对于简单的应用,可以先使用 error_log + 文件系统监控,然后逐步演进。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播