本文目录导读:

在 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 + 文件系统监控,然后逐步演进。