1. Fastadmin与think-queue的整合背景
在Fastadmin项目中集成think-queue队列系统,本质上是为了解决Web应用中常见的同步处理瓶颈问题。当用户提交一个耗时操作(如Excel导出、大批量数据处理)时,传统同步处理会导致浏览器长时间等待响应,而队列系统能将这类任务转移到后台异步执行,立即返回"任务已接收"的提示,显著提升用户体验。
Fastadmin作为基于ThinkPHP的快速开发框架,天然支持think-queue这个官方队列组件。实际项目中,我们最常用的是Redis驱动方案,因为它兼具性能与可靠性。相比数据库驱动,Redis的高吞吐量更适合任务密集场景;而对比更复杂的RabbitMQ,Redis的部署和维护成本更低,对中小型项目更为友好。
2. 环境准备与基础配置
2.1 组件安装与依赖检查
首先通过Composer安装必要组件:
bash复制composer require topthink/think-queue
composer require predis/predis # Redis客户端
检查Fastadmin的config/queue.php配置文件,核心参数如下:
php复制return [
'default' => 'redis',
'connections' => [
'redis' => [
'type' => 'redis',
'queue' => 'default',
'host' => '127.0.0.1',
'port' => 6379,
'password' => '',
'select' => 0,
'timeout' => 0,
'persistent' => false,
'expire' => 60,
],
],
];
注意:生产环境务必设置Redis密码并启用持久化连接,避免服务重启导致任务丢失。
2.2 队列进程管理方案
推荐使用Supervisor管理队列进程,配置示例:
ini复制[program:fastadmin_queue]
command=php think queue:listen --queue default --tries 3 --sleep 3
directory=/path/to/your/project
autostart=true
autorestart=true
user=www
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/supervisor/fastadmin_queue.log
关键参数说明:
--tries 3:任务失败后重试次数--sleep 3:队列空时休眠时间(秒)numprocs=2:建议设置为CPU核心数的1.5倍
3. 任务开发实战
3.1 创建基础任务类
在application/jobs目录下新建示例任务:
php复制namespace app\jobs;
class ExportJob
{
protected $data;
public function __construct($data)
{
$this->data = $data;
}
public function handle()
{
// 模拟耗时操作
$file = "export_".date('YmdHis').'.csv';
$fp = fopen($file, 'w');
foreach ($this->data as $row) {
fputcsv($fp, $row);
}
fclose($fp);
// 返回结果路径
return ['path' => $file];
}
}
3.2 任务投递的三种方式
- 即时投递:
php复制$job = new \app\jobs\ExportJob($data);
\think\Queue::push($job);
- 延迟投递(5分钟后执行):
php复制\think\Queue::later(300, $job);
- 批量投递:
php复制$batchId = uniqid();
foreach ($chunks as $chunk) {
\think\Queue::push(new ExportJob($chunk), '', $batchId);
}
3.3 任务状态追踪方案
在Fastadmin中扩展任务监控表:
php复制// 新建数据表模型
class QueueJob extends Model
{
protected $name = 'queue_job';
// 状态常量
const STATUS_PENDING = 0;
const STATUS_PROCESSING = 1;
const STATUS_COMPLETED = 2;
const STATUS_FAILED = 3;
// 记录任务
public static function createJob($jobClass, $data)
{
return self::create([
'job_class' => $jobClass,
'payload' => json_encode($data),
'status' => self::STATUS_PENDING,
'create_time' => time()
]);
}
}
在任务类中更新状态:
php复制public function handle()
{
try {
QueueJob::updateStatus($this->jobId, QueueJob::STATUS_PROCESSING);
// 业务逻辑...
QueueJob::updateStatus($this->jobId, QueueJob::STATUS_COMPLETED, [
'result' => $result
]);
} catch (\Exception $e) {
QueueJob::updateStatus($this->jobId, QueueJob::STATUS_FAILED, [
'error' => $e->getMessage()
]);
throw $e; // 触发重试机制
}
}
4. 高级应用场景
4.1 优先级队列实现
修改Redis配置支持多队列:
php复制// config/queue.php
'connections' => [
'redis' => [
// ...
'queue' => 'high,default,low', // 多队列声明
],
];
投递时指定优先级:
php复制// 高优先级任务
\think\Queue::push($job, 'high');
// 默认优先级
\think\Queue::push($job, 'default');
// 低优先级任务
\think\Queue::push($job, 'low');
启动多个消费者分别处理不同队列:
bash复制php think queue:listen --queue high --tries 3
php think queue:listen --queue default --tries 3
php think queue:listen --queue low --tries 3
4.2 失败任务重试策略
自定义重试逻辑(在任务类中添加):
php复制public function failed($data)
{
$retryTimes = $data['attempts'] ?? 0;
if ($retryTimes < 3) {
// 指数退避重试
$delay = pow(2, $retryTimes) * 60;
\think\Queue::later($delay, new static($this->data));
} else {
// 最终失败处理
Log::error("Job failed after retries", [
'job' => static::class,
'data' => $this->data
]);
}
}
4.3 与Fastadmin权限系统集成
在任务类中临时切换执行身份:
php复制public function handle()
{
// 保存当前用户会话
$originalAdmin = session('admin');
try {
// 切换为系统用户执行
session('admin', [
'id' => 0,
'username' => 'system_job'
]);
// 执行业务逻辑...
} finally {
// 恢复原始会话
session('admin', $originalAdmin);
}
}
5. 性能优化与问题排查
5.1 Redis内存优化技巧
- 压缩任务数据:
php复制\think\Queue::push(new Job(gzcompress(serialize($data))));
- 设置合理过期时间:
php复制'connections' => [
'redis' => [
'expire' => 86400, // 24小时后自动删除已完成任务
],
];
- 监控关键指标:
bash复制redis-cli info | grep -E 'used_memory|queue'
5.2 常见问题解决方案
问题1:任务重复执行
- 原因:消费者进程异常退出导致任务未确认
- 解决方案:
php复制// 在任务类中添加唯一锁
public function handle()
{
$lockKey = 'job_lock:'.md5(serialize($this->data));
if (!Redis::setnx($lockKey, 1)) {
return;
}
try {
// 业务逻辑...
} finally {
Redis::del($lockKey);
}
}
问题2:长任务超时
- 修改Supervisor配置:
ini复制stopwaitsecs=60 ; 默认10秒改为60秒
问题3:内存泄漏
- 定期重启消费者:
ini复制startretries=3
stopsignal=QUIT
stopwaitsecs=30
5.3 监控方案实现
使用Prometheus+Grafana搭建监控看板:
- 添加指标收集中间件:
php复制namespace app\middleware;
class QueueMetrics
{
public function handle($job, $next)
{
$startTime = microtime(true);
$status = 'success';
try {
return $next($job);
} catch (\Exception $e) {
$status = 'failed';
throw $e;
} finally {
$duration = microtime(true) - $startTime;
// 记录到Prometheus
$this->recordMetrics(
get_class($job),
$status,
$duration
);
}
}
}
- 配置指标暴露端点:
php复制// 在route.php中添加
Route::get('/metrics', function() {
$registry = Prometheus\CollectorRegistry::getDefault();
$renderer = new Prometheus\RenderTextFormat();
return response($renderer->render($registry->getMetricFamilySamples()))
->header('Content-Type', 'text/plain');
});
6. 实际项目经验分享
在电商订单处理系统中,我们通过队列实现了以下优化:
- 分级处理策略:
- 即时队列:库存扣减、支付回调(<1秒延迟)
- 普通队列:订单日志、数据分析(<5分钟延迟)
- 延迟队列:未支付订单取消(30分钟延迟)
- 动态并发控制:
php复制// 根据服务器负载自动调整消费者数量
$load = sys_getloadavg()[0];
$workers = max(1, floor(4 - $load)); // 4核服务器
exec("supervisorctl update fastadmin_queue numprocs={$workers}");
- 关键教训:
- 永远假设任务会失败:重要任务必须实现幂等性
- 任务数据不要包含资源对象:只传递ID而非完整模型
- 监控比优化更重要:先确保可观测性,再考虑性能提升
一个典型的订单处理任务类改进版:
php复制class OrderProcessJob
{
private $orderId;
public function __construct($orderId)
{
if (!is_numeric($orderId)) {
throw new \InvalidArgumentException('只允许传递订单ID');
}
$this->orderId = $orderId;
}
public function handle()
{
$order = Order::with(['items', 'user'])
->find($this->orderId);
if (!$order) {
Log::warning("订单不存在: {$this->orderId}");
return;
}
// 幂等检查
if ($order->status > Order::STATUS_PENDING) {
return;
}
DB::transaction(function() use ($order) {
// 扣减库存
foreach ($order->items as $item) {
Product::where('id', $item->product_id)
->where('stock', '>=', $item->quantity)
->dec('stock', $item->quantity)
->update();
}
// 更新订单状态
$order->status = Order::STATUS_PROCESSED;
$order->save();
});
}
public function failed($exception)
{
// 触发人工审核流程
AlertService::notifyAdmin(
"订单处理失败: {$this->orderId}",
$exception->getMessage()
);
}
}
