7个实战案例彻底解决Bernard队列处理难题:从入门到精通的PHP任务调度指南

7个实战案例彻底解决Bernard队列处理难题:从入门到精通的PHP任务调度指南

【免费下载链接】bernard Bernard is a multi-backend PHP library for creating background jobs for later processing. 【免费下载链接】bernard 项目地址: https://gitcode.com/gh_mirrors/be/bernard

引言:为什么你的PHP任务队列总是出问题?

你是否遇到过这些场景:消息发送后神秘消失、消费者突然停止工作、不同驱动间切换困难、队列堆积导致系统崩溃?作为PHP开发者,实现可靠的后台任务处理(Background Job Processing)一直是个挑战。Bernard作为一款多后端PHP队列库,提供了灵活的任务调度解决方案,但许多开发者在实际使用中仍会遇到各种棘手问题。

本文将通过7个真实场景案例,深入剖析Bernard队列系统的常见问题与解决方案,涵盖从基础配置到高级优化的全流程。读完本文,你将能够:

  • 快速定位并解决Bernard队列的常见错误
  • 正确配置多种驱动以满足不同场景需求
  • 优化消息处理性能与可靠性
  • 实现高级功能如消息优先级、延迟执行和失败重试
  • 构建可监控、可扩展的生产级队列系统

案例一:消息路由失败导致任务无法执行

问题现象

使用PlainMessage发送任务后,消费者虽然运行正常但始终不处理消息,也不报错。

原因分析

Bernard通过消息路由(Message Routing)机制将消息分发到对应的处理器(Receiver)。当消息名称与处理器映射关系不正确时,会导致消息无法被正确路由。

解决方案

1. 理解消息命名规则

Bernard默认使用消息名称(通过getName()方法获取)进行路由。对于PlainMessage,队列名称会自动从消息名称生成:在大写字母前插入下划线,然后转为小写。

<?php
// 消息名称"SendNewsletter"会生成队列名"send_newsletter"
$message = new PlainMessage('SendNewsletter', ['newsletterId' => 12]);
2. 正确配置Receiver映射

确保Receiver正确注册到路由器:

<?php
use Bernard\Router\ReceiverMapRouter;

// 错误示例:消息名称与映射键不匹配
$router = new ReceiverMapRouter([
    'send_newsletter' => new NewsletterMessageHandler(), // 错误的键名
]);

// 正确示例:使用消息名称作为键
$router = new ReceiverMapRouter([
    'SendNewsletter' => new NewsletterMessageHandler(), // 正确的键名
]);
3. 实现自定义路由策略

对于复杂场景,可使用基于类名的路由:

<?php
use Bernard\Router\ClassNameRouter;

// 基于消息类的路由
$router = new ClassNameRouter([
    SendNewsletterMessage::class => new NewsletterMessageHandler(),
]);
4. 调试路由问题

添加日志记录未路由的消息:

<?php
use Symfony\Component\EventDispatcher\EventDispatcher;

$eventDispatcher->addListener('bernard.message.reject', function($event) {
    $envelope = $event->getEnvelope();
    error_log("未找到处理器的消息: " . $envelope->getMessage()->getName());
});

验证与测试

<?php
// 创建测试消息
$message = new PlainMessage('SendNewsletter', ['newsletterId' => 12]);

// 直接调用处理器验证逻辑
$handler = new NewsletterMessageHandler();
$handler->sendNewsletter($message); // 注意方法名应与消息名匹配(首字母小写)

// 验证路由配置
$router = new ReceiverMapRouter([
    'SendNewsletter' => $handler,
]);
var_dump($router->route($message)); // 应返回对应的处理器

案例二:Doctrine驱动配置导致的数据库锁问题

问题现象

使用Doctrine DBAL驱动时,消费者经常卡顿,数据库连接数飙升,甚至出现死锁。

原因分析

Doctrine驱动使用数据库事务确保消息处理的原子性。若表结构未正确创建或连接配置不当,会导致事务无法正常提交,引发锁竞争和连接泄漏。

解决方案

1. 正确初始化数据库表结构

必须使用Bernard提供的命令创建正确的表结构:

# 创建Doctrine数据库表结构(推荐)
php bin/console bernard:doctrine:create

# 或手动创建(不推荐)
<?php
use Bernard\Driver\Doctrine\MessagesSchema;
use Doctrine\DBAL\Schema\Schema;

$schema = new Schema();
MessagesSchema::create($schema);

$sql = $schema->toSql($connection->getDatabasePlatform());
foreach ($sql as $query) {
    $connection->exec($query);
}
2. 优化数据库连接配置
<?php
// doctrine.php 配置示例
$connection = DriverManager::getConnection([
    'dbname'   => 'bernard',
    'user'     => 'root',
    'password' => 'secret',
    'driver'   => 'pdo_mysql',
    'charset'  => 'utf8mb4',
    // 添加以下优化参数
    'attributes' => [
        // 减少长事务导致的锁竞争
        PDO::ATTR_TIMEOUT => 30,
        // 启用持久连接复用
        PDO::ATTR_PERSISTENT => true,
    ],
    'driverOptions' => [
        // MySQL特定:减少元数据查询
        1002 => 'SET NAMES utf8mb4, time_zone = "+00:00"',
    ],
]);
3. 实现消息预取机制

对于高吞吐量场景,使用预取机制减少数据库查询次数:

<?php
use Bernard\Driver\AbstractPrefetchDriver;

// 使用支持预取的驱动
$driver = new Bernard\Driver\Doctrine\Driver($connection);
$driver->setPrefetchCount(10); // 一次预取10条消息
4. 添加连接监控

实现连接监听器监控连接状态:

<?php
use Bernard\Driver\Doctrine\ConnectionListener;

$connection->getConfiguration()->setSQLLogger(new class implements \Doctrine\DBAL\Logging\SQLLogger {
    public function startQuery($sql, array $params = null, array $types = null) {
        // 记录慢查询
        $this->startTime = microtime(true);
    }
    
    public function stopQuery() {
        $duration = microtime(true) - $this->startTime;
        if ($duration > 1) { // 记录超过1秒的查询
            error_log("Slow query detected: " . $duration . "s");
        }
    }
});

验证与测试

<?php
// 测试数据库表结构
$schemaManager = $connection->getSchemaManager();
$tables = $schemaManager->listTables();
$tableNames = array_map(function($table) { return $table->getName(); }, $tables);

// 验证必要的表是否存在
assert(in_array('bernard_messages', $tableNames), 'Bernard消息表不存在');
assert(in_array('bernard_queues', $tableNames), 'Bernard队列表不存在');

案例三:消费者内存泄漏导致进程崩溃

问题现象

消费者进程运行一段时间后内存占用持续增长,最终被系统终止或崩溃。

原因分析

长时间运行的PHP进程容易产生内存泄漏,尤其是在循环处理大量消息时。常见原因包括:未释放的资源、循环引用、全局状态累积等。

解决方案

1. 设置合理的消费者运行时间

使用max-runtime选项限制消费者单次运行时间:

<?php
// 消费者配置
$consumer->consume($queueFactory->create('send-newsletter'), [
    'max-runtime' => 300, // 运行5分钟后自动退出
]);

结合进程管理工具实现自动重启:

; 进程管理配置示例
[program:bernard_consumer]
command=php console bernard:consume send-newsletter --max-runtime=300
autostart=true
autorestart=true
user=www-data
numprocs=4
process_name=%(program_name)s_%(process_num)02d
2. 实现消息处理后的资源清理

在消息处理器中显式释放资源:

<?php
class NewsletterMessageHandler {
    private $dbConnection;
    private $mailer;
    
    public function __construct(PDO $dbConnection, Mailer $mailer) {
        $this->dbConnection = $dbConnection;
        $this->mailer = $mailer;
    }
    
    public function sendNewsletter(PlainMessage $message) {
        try {
            // 处理消息逻辑
            $newsletterId = $message->get('newsletterId');
            // ...发送邮件逻辑...
            
            return true;
        } finally {
            // 清理资源
            $this->dbConnection->rollBack(); // 确保事务已回滚或提交
            $this->mailer->clear(); // 清空邮件队列
            unset($newsletterId); // 解除变量引用
        }
    }
}
3. 使用内存监控与分析

添加内存使用监控:

<?php
$eventDispatcher->addListener('bernard.message.processed', function() {
    static $initialMemory = null;
    $currentMemory = memory_get_usage(true);
    
    if ($initialMemory === null) {
        $initialMemory = $currentMemory;
    }
    
    $memoryGrowth = $currentMemory - $initialMemory;
    if ($memoryGrowth > 10485760) { // 超过10MB增长
        error_log("Memory growth detected: " . number_format($memoryGrowth / 1024 / 1024, 2) . "MB");
    }
});
4. 优化消息处理循环

避免在循环中创建大型对象,使用对象池模式复用资源:

<?php
// 反模式:在循环中创建新对象
foreach ($messages as $message) {
    $processor = new NewsletterProcessor(); // 每次创建新实例
    $processor->process($message);
}

// 优化:复用对象实例
$processor = new NewsletterProcessor();
foreach ($messages as $message) {
    $processor->process($message);
    $processor->reset(); // 重置内部状态
}

验证与测试

# 使用内存监控工具跟踪进程
php -d memory_limit=512M console bernard:consume send-newsletter &
pid=$!
while true; do
    ps -p $pid -o rss,vsize,etime
    sleep 10
done

案例四:消息重复处理导致数据不一致

问题现象

同一条消息被多次处理,导致重复发送邮件、重复创建订单等数据一致性问题。

原因分析

Bernard依赖驱动实现消息的可见性控制(Visibility Control)。当消息处理时间超过可见性超时时间,驱动会将消息重新置为可见状态,导致被其他消费者处理。

解决方案

1. 正确设置消息可见性超时

根据消息处理时间调整可见性超时:

<?php
// 不同驱动的可见性超时设置示例

// Redis驱动
$driver = new Bernard\Driver\PhpRedis\Driver($redis);
$driver->setVisibilityTimeout(300); // 5分钟超时

// Doctrine驱动
$driver = new Bernard\Driver\Doctrine\Driver($connection);
$driver->setVisibilityTimeout(300);

// SQS驱动
$driver = new Bernard\Driver\Sqs\Driver($connection, [], 5);
$driver->setVisibilityTimeout(300);
2. 实现消息幂等处理

设计幂等的消息处理器,确保重复处理不会导致副作用:

<?php
class NewsletterMessageHandler {
    private $db;
    
    public function __construct(PDO $db) {
        $this->db = $db;
    }
    
    public function sendNewsletter(PlainMessage $message) {
        $newsletterId = $message->get('newsletterId');
        $recipientId = $message->get('recipientId');
        
        // 检查消息是否已处理
        $stmt = $this->db->prepare("SELECT id FROM sent_newsletters 
                                  WHERE newsletter_id = :nid AND recipient_id = :rid");
        $stmt->execute([':nid' => $newsletterId, ':rid' => $recipientId]);
        
        if ($stmt->fetch()) {
            // 已处理,直接返回
            return true;
        }
        
        // 处理消息...
        $mailer->send($recipientId, $newsletterId);
        
        // 记录处理状态
        $stmt = $this->db->prepare("INSERT INTO sent_newsletters 
                                  (newsletter_id, recipient_id, sent_at) 
                                  VALUES (:nid, :rid, NOW())");
        $stmt->execute([':nid' => $newsletterId, ':rid' => $recipientId]);
        
        return true;
    }
}
3. 使用消息ID跟踪处理状态

利用Bernard的消息信封(Envelope)ID实现去重:

<?php
$eventDispatcher->addListener('bernard.message.process', function($event) use ($db) {
    $envelope = $event->getEnvelope();
    $messageId = $envelope->getId(); // 获取唯一消息ID
    
    // 检查消息是否已处理
    $stmt = $db->prepare("SELECT id FROM processed_messages WHERE message_id = :id");
    $stmt->execute([':id' => $messageId]);
    
    if ($stmt->fetch()) {
        // 已处理,停止处理流程
        $event->stopPropagation();
        return false;
    }
    
    // 记录消息开始处理
    $stmt = $db->prepare("INSERT INTO processed_messages 
                        (message_id, status, started_at) 
                        VALUES (:id, 'processing', NOW())");
    $stmt->execute([':id' => $messageId]);
});

$eventDispatcher->addListener('bernard.message.acknowledge', function($event) use ($db) {
    $envelope = $event->getEnvelope();
    $messageId = $envelope->getId();
    
    // 更新消息处理状态为成功
    $stmt = $db->prepare("UPDATE processed_messages 
                        SET status = 'completed', finished_at = NOW() 
                        WHERE message_id = :id");
    $stmt->execute([':id' => $messageId]);
});
4. 实现消息延期重试机制

对于处理失败的消息,实现可控的重试机制:

<?php
use Bernard\EventListener\FailureSubscriber;

// 配置失败处理订阅器
$subscriber = new FailureSubscriber([
    'max-attempts' => 3, // 最多重试3次
    'delay' => 60, // 重试延迟60秒
]);
$eventDispatcher->addSubscriber($subscriber);

验证与测试

<?php
// 模拟消息处理超时测试
class SlowNewsletterHandler {
    public function sendNewsletter(PlainMessage $message) {
        // 故意设置比可见性超时更长的处理时间
        sleep(360); // 6分钟,超过5分钟的可见性超时
        // 实际处理逻辑...
    }
}

案例五:多驱动配置与切换策略

问题现象

开发环境使用InMemory驱动工作正常,但部署到生产环境切换到Redis驱动后出现各种兼容性问题。

原因分析

不同驱动对Bernard功能的支持程度不同,且各有特定的配置要求。直接切换驱动而不调整代码和配置会导致兼容性问题。

解决方案

1. 了解各驱动特性与限制
驱动类型事务支持持久化分布式消息优先级延迟消息推荐场景
InMemory不支持支持支持开发、测试
Doctrine支持不支持支持中小规模应用
Redis不支持支持支持高吞吐量应用
RabbitMQ支持支持支持复杂企业应用
SQS支持不支持支持云环境部署
2. 实现环境无关的驱动配置

使用依赖注入和环境变量实现驱动的动态切换:

<?php
// config/bernard.php
use Bernard\Driver;
use Bernard\QueueFactory\PersistentFactory;

return [
    'driver' => function() {
        $driverType = getenv('BERNARD_DRIVER') ?: 'in_memory';
        
        switch ($driverType) {
            case 'redis':
                $redis = new Redis();
                $redis->connect(getenv('REDIS_HOST'), getenv('REDIS_PORT'));
                return new Driver\PhpRedis\Driver($redis);
                
            case 'doctrine':
                $connection = DriverManager::getConnection([
                    'dbname' => getenv('DB_NAME'),
                    'user' => getenv('DB_USER'),
                    'password' => getenv('DB_PASSWORD'),
                    'driver' => getenv('DB_DRIVER'),
                ]);
                return new Driver\Doctrine\Driver($connection);
                
            case 'sqs':
                $client = SqsClient::factory([
                    'key' => getenv('AWS_KEY'),
                    'secret' => getenv('AWS_SECRET'),
                    'region' => getenv('AWS_REGION'),
                ]);
                return new Driver\Sqs\Driver($client);
                
            default:
                return new Driver\InMemory\Driver();
        }
    },
];
3. 实现驱动适配层

为不同驱动提供统一接口,封装特定驱动的差异:

<?php
class DriverAdapter {
    private $driver;
    private $type;
    
    public function __construct(Driver $driver, string $type) {
        $this->driver = $driver;
        $this->type = $type;
    }
    
    public function setVisibilityTimeout(int $seconds) {
        // 适配不同驱动的方法名差异
        if (method_exists($this->driver, 'setVisibilityTimeout')) {
            $this->driver->setVisibilityTimeout($seconds);
        } elseif ($this->type === 'redis' && method_exists($this->driver, 'setTimeout')) {
            // Redis驱动使用不同的方法名
            $this->driver->setTimeout($seconds);
        }
    }
    
    // 其他适配方法...
}
4. 编写环境无关的测试

使用抽象工厂模式确保测试在不同驱动下都能运行:

<?php
abstract class BernardTestCase extends PHPUnit\Framework\TestCase {
    abstract protected function createDriver();
    
    public function testMessageProductionConsumption() {
        $driver = $this->createDriver();
        $factory = new PersistentFactory($driver, new Serializer());
        $producer = new Producer($factory);
        
        // 测试逻辑...
        $message = new PlainMessage('TestMessage', ['data' => 'test']);
        $producer->produce($message);
        
        $queue = $factory->create('test_message');
        $envelope = $queue->dequeue();
        
        $this->assertNotNull($envelope);
        $this->assertEquals('TestMessage', $envelope->getMessage()->getName());
    }
}

// 不同驱动的测试实现
class RedisDriverTest extends BernardTestCase {
    protected function createDriver() {
        $redis = new Redis();
        $redis->connect('localhost');
        $redis->flushDB(); // 测试前清空
        return new Driver\PhpRedis\Driver($redis);
    }
}

class DoctrineDriverTest extends BernardTestCase {
    protected function createDriver() {
        $connection = DriverManager::getConnection([
            'driver' => 'pdo_sqlite',
            'memory' => true,
        ]);
        // 创建测试表结构
        $schema = new Schema();
        MessagesSchema::create($schema);
        foreach ($schema->toSql($connection->getDatabasePlatform()) as $sql) {
            $connection->exec($sql);
        }
        return new Driver\Doctrine\Driver($connection);
    }
}

验证与测试

# 针对不同驱动运行测试
BERNARD_DRIVER=in_memory composer test
BERNARD_DRIVER=redis composer test
BERNARD_DRIVER=doctrine composer test

案例六:消息优先级与延迟执行实现

问题现象

需要实现紧急任务优先处理、定时发送等功能,但Bernard默认不直接支持消息优先级和延迟执行。

原因分析

Bernard核心设计中没有内置消息优先级和延迟执行功能,这些特性需要依赖具体驱动实现或通过额外逻辑模拟。

解决方案

1. 使用多队列实现优先级

创建不同优先级的队列,消费者按优先级顺序处理:

<?php
// 1. 创建多优先级队列
$highQueue = $factory->create('send_newsletter_high');
$normalQueue = $factory->create('send_newsletter_normal');
$lowQueue = $factory->create('send_newsletter_low');

// 2. 使用RoundRobinQueue合并多队列
$roundRobinQueue = new RoundRobinQueue([
    $highQueue,    // 高优先级队列
    $normalQueue,  // 普通优先级队列
    $lowQueue,     // 低优先级队列
]);

// 3. 按优先级发送消息
$highPriorityMessage = new PlainMessage('SendNewsletter', [
    'newsletterId' => 1,
    'priority' => 'high'
]);
$producer->produce($highPriorityMessage, 'send_newsletter_high');

// 4. 消费者从合并队列消费
$consumer->consume($roundRobinQueue);
2. 实现延迟消息处理

利用驱动特性或额外存储实现延迟消息:

<?php
// 方案一:使用Redis的过期特性
class DelayedRedisDriver extends Bernard\Driver\PhpRedis\Driver {
    public function enqueue($queue, $message) {
        $delay = $message->get('delay', 0);
        if ($delay > 0) {
            // 使用Redis的ZADD添加延迟消息
            $this->redis->zadd(
                $this->prefix . $queue . '_delayed',
                time() + $delay,
                $message
            );
            return;
        }
        parent::enqueue($queue, $message);
    }
    
    // 定期检查并移动到期的延迟消息
    public function processDelayedMessages() {
        $queues = $this->redis->keys($this->prefix . '*_delayed');
        foreach ($queues as $delayedQueue) {
            $queue = str_replace('_delayed', '', $delayedQueue);
            $messages = $this->redis->zrangebyscore(
                $delayedQueue,
                0,
                time(),
                ['limit' => [0, 100]]
            );
            
            foreach ($messages as $message) {
                $this->redis->lpush($queue, $message);
                $this->redis->zrem($delayedQueue, $message);
            }
        }
    }
}
3. 结合事件调度系统实现定时任务
<?php
// 创建定时消息
class ScheduledMessage extends PlainMessage {
    private $executeAt;
    
    public function __construct(string $name, array $parameters, \DateTimeInterface $executeAt) {
        parent::__construct($name, $parameters);
        $this->executeAt = $executeAt;
    }
    
    public function getExecuteAt(): \DateTimeInterface {
        return $this->executeAt;
    }
}

// 调度处理器
class SchedulerProcessor {
    private $producer;
    
    public function __construct(Producer $producer) {
        $this->producer = $producer;
    }
    
    public function schedule(ScheduledMessage $message) {
        $now = new \DateTime();
        $delay = $message->getExecuteAt()->getTimestamp() - $now->getTimestamp();
        
        if ($delay > 0) {
            // 使用延迟队列
            $this->producer->produce($message, 'scheduled_tasks');
        } else {
            // 立即执行
            $this->producer->produce($message);
        }
    }
}

验证与测试

<?php
// 优先级测试
$highMessage = new PlainMessage('HighPriority', ['priority' => 'high']);
$normalMessage = new PlainMessage('NormalPriority', ['priority' => 'normal']);

$producer->produce($normalMessage, 'normal_queue');
$producer->produce($highMessage, 'high_queue');

$roundRobinQueue = new RoundRobinQueue([
    $factory->create('high_queue'),
    $factory->create('normal_queue'),
]);

// 应该先取出高优先级消息
$firstEnvelope = $roundRobinQueue->dequeue();
$this->assertEquals('HighPriority', $firstEnvelope->getMessage()->getName());

// 延迟消息测试
$executeAt = (new \DateTime())->modify('+1 minute');
$message = new ScheduledMessage('DelayedMessage', [], $executeAt);
$producer->produce($message, 'scheduled_tasks');

// 立即检查应该没有消息
$queue = $factory->create('delayed_message');
$this->assertNull($queue->dequeue());

// 等待一分钟后再次检查
sleep(60);
$envelope = $queue->dequeue();
$this->assertNotNull($envelope);

案例七:Bernard与框架集成最佳实践

问题现象

在Symfony/Laravel等框架中集成Bernard时,面临依赖注入、配置管理、命令行集成等挑战。

原因分析

Bernard设计为独立库,没有针对特定框架的原生支持。直接在框架中使用需要手动处理服务注册、配置加载、事件集成等工作。

解决方案

1. Symfony框架集成
1.1 服务配置
# config/services.yaml
services:
    Bernard\Driver\PhpRedis\Driver:
        arguments:
            - '@Redis'
    
    Bernard\QueueFactory\PersistentFactory:
        arguments:
            - '@Bernard\Driver\PhpRedis\Driver'
            - '@Bernard\Serializer'
    
    Bernard\Producer:
        arguments:
            - '@Bernard\QueueFactory\PersistentFactory'
    
    # 消息处理器
    App\MessageHandler\NewsletterHandler:
        tags:
            - { name: bernard.receiver, message: SendNewsletter }
1.2 命令行集成
<?php
// src/Command/BernardConsumeCommand.php
namespace App\Command;

use Bernard\Command\ConsumeCommand as BaseConsumeCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class BernardConsumeCommand extends BaseConsumeCommand {
    protected static $defaultName = 'bernard:consume';
    
    protected function execute(InputInterface $input, OutputInterface $output) {
        // 框架特定的初始化逻辑
        $this->getContainer()->get('monolog.logger.bernard')->info('Starting consumer');
        
        return parent::execute($input, $output);
    }
}
1.3 使用PSR-11容器解析处理器
<?php
use Bernard\Router\ContainerReceiverResolver;
use Bernard\Router\ReceiverMapRouter;

$resolver = new ContainerReceiverResolver($container);
$router = new ReceiverMapRouter([
    'SendNewsletter' => 'App\MessageHandler\NewsletterHandler',
    'ProcessOrder' => 'App\MessageHandler\OrderHandler',
], $resolver);
2. Laravel框架集成
2.1 服务提供者
<?php
// app/Providers/BernardServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Bernard\Driver\PhpRedis\Driver;
use Bernard\QueueFactory\PersistentFactory;
use Bernard\Producer;

class BernardServiceProvider extends ServiceProvider {
    public function register() {
        $this->app->singleton(Driver::class, function ($app) {
            $redis = $app->make('redis')->connection()->client();
            return new Driver($redis);
        });
        
        $this->app->singleton(PersistentFactory::class, function ($app) {
            return new PersistentFactory(
                $app->make(Driver::class),
                $app->make(\Bernard\Serializer::class)
            );
        });
        
        $this->app->singleton(Producer::class, function ($app) {
            return new Producer($app->make(PersistentFactory::class));
        });
    }
    
    public function boot() {
        if ($this->app->runningInConsole()) {
            $this->commands([
                \App\Console\Commands\BernardConsumeCommand::class,
            ]);
        }
    }
}
2.2 门面(Facade)封装
<?php
// app/Facades/Bernard.php
namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class Bernard extends Facade {
    protected static function getFacadeAccessor() {
        return \Bernard\Producer::class;
    }
}
2.3 使用示例
<?php
// 在控制器中使用
use App\Facades\Bernard;
use Bernard\Message\PlainMessage;

class NewsletterController extends Controller {
    public function send(Request $request) {
        $newsletterId = $request->input('newsletter_id');
        
        $message = new PlainMessage('SendNewsletter', [
            'newsletterId' => $newsletterId,
            'userId' => $request->user()->id,
        ]);
        
        Bernard::produce($message);
        
        return response()->json(['status' => 'queued']);
    }
}

验证与测试

# Symfony命令测试
php bin/console bernard:consume send-newsletter --max-runtime=300

# Laravel命令测试
php artisan bernard:consume send-newsletter --max-runtime=300

总结与最佳实践

通过以上7个案例,我们深入探讨了Bernard队列系统的常见问题与解决方案。以下是一些通用的最佳实践总结:

1. 开发环境配置

  • 使用InMemory驱动进行单元测试
  • 开发环境可使用FlatFile驱动,无需额外服务
  • 为不同环境创建独立配置文件

2. 生产环境配置

  • 优先选择Redis或RabbitMQ驱动,提供更好的性能和可靠性
  • 配置合理的可见性超时和消费者运行时间
  • 实现消息去重和幂等处理机制
  • 使用进程管理工具管理消费者进程

3. 性能优化策略

  • 根据消息类型使用不同优先级队列
  • 实现消息批处理减少数据库连接开销
  • 定期重启消费者进程防止内存泄漏
  • 监控队列长度,实现自动扩缩容

4. 监控与维护

  • 记录队列长度、处理延迟等关键指标
  • 实现消息处理失败的告警机制
  • 定期清理过期消息和失败任务
  • 建立完整的日志系统,包括消息生命周期各阶段

Bernard作为一款灵活的PHP队列库,虽然没有提供开箱即用的所有功能,但通过合理的设计和配置,可以构建出可靠、高效的任务调度系统。关键在于理解各组件的工作原理,针对具体场景选择合适的驱动和配置,并遵循本文介绍的最佳实践。

附录:Bernard常用驱动配置速查表

Redis驱动

<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->setOption(Redis::OPT_PREFIX, 'bernard:');

$driver = new Bernard\Driver\PhpRedis\Driver($redis);
$driver->setVisibilityTimeout(300); // 5分钟
$driver->setPrefetchCount(10); // 预取10条消息

Doctrine驱动

<?php
$connection = \Doctrine\DBAL\DriverManager::getConnection([
    'dbname' => 'bernard',
    'user' => 'root',
    'password' => 'secret',
    'driver' => 'pdo_mysql',
]);

// 创建表结构
$schema = new \Doctrine\DBAL\Schema\Schema();
\Bernard\Driver\Doctrine\MessagesSchema::create($schema);
$sqlArray = $schema->toSql($connection->getDatabasePlatform());
foreach ($sqlArray as $sql) {
    $connection->exec($sql);
}

$driver = new Bernard\Driver\Doctrine\Driver($connection);
$driver->setVisibilityTimeout(300);

RabbitMQ驱动

<?php
$connection = new \PhpAmqpLib\Connection\AMQPStreamConnection(
    'localhost', 5672, 'guest', 'guest'
);

$driver = new \Bernard\Driver\PhpAmqpDriver(
    $connection, 
    'bernard_exchange',
    ['content_type' => 'application/json', 'delivery_mode' => 2]
);

【免费下载链接】bernard Bernard is a multi-backend PHP library for creating background jobs for later processing. 【免费下载链接】bernard 项目地址: https://gitcode.com/gh_mirrors/be/bernard

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值