问题背景与解决方案
问题场景
在实现Excel数据导入功能时,遇到一个典型的生产者-消费者场景:
- 主流程:Excel文件解析 → 数据校验 → 数据库事务写入
- 附加流程:将成功数据推送给第三方系统
当第三方接口响应缓慢时(实测平均耗时8-12秒),导致整体接口响应时间超出前端等待阈值,造成以下问题:
- 前端显示系统错误(HTTP 500)
- 实际业务数据已完整入库
- 用户体验与数据一致性存在割裂
解决方案演进
- 同步方案:直接顺序执行(已存在问题)
- 队列方案:Redis队列 + 独立消费者进程(最优解但需额外部署)
- 折中方案:PHP进程控制 + Redis临时队列(本文实现方案)
最终采用方案3,在保证系统轻量化的前提下实现异步处理,技术组合:
- Redis List结构作为临时存储
- PHP pcntl扩展进行进程控制
- ThinkPHP6命令行组件实现消费逻辑
技术实现详解
1. Redis队列封装(生产者端)
class RedisQueue extends RedisBase
{
/**
* 安全写入队列(支持复杂数据结构)
* @param string $name 队列名称
* @param mixed $value 支持字符串/数组/对象
* @return int 队列长度
*/
public function rPush(string $name, $value)
{
$value = is_scalar($value) ? $value : json_encode($value);
return $this->handle->rPush($this->getCacheKey($name), $value);
}
/**
* 安全读取队列(自动反序列化)
* @param string $name 队列名称
* @return mixed 原始数据类型
*/
public function lPop(string $name)
{
$value = $this->handle->lPop($this->getCacheKey($name));
return json_decode($value, true) ?? $value;
}
}
设计要点:
- 自动序列化/反序列化处理
- 兼容标量值与复杂数据结构
- 继承RedisBase实现连接池管理
2. 异步触发机制
private function send_import_sku_to_yjt($data)
{
$redis = new RedisQueue();
$queueName = 'yjt_sku_import';
// 数据分批入队(避免大消息体)
foreach (array_chunk($data, 100) as $batch) {
$redis->rPush($queueName, $batch);
}
// 构建异步命令
$rootPath = root_path();
$command = sprintf(
'php74 %sthink jiayi sendSkuToYjt -r %s -p %s &> /dev/null &',
$rootPath,
escapeshellarg($queueName),
escapeshellarg(json_encode(['operator' => $this->getCurrentUser()]))
);
// 非阻塞执行
pclose(popen($command, 'r'));
}
关键技术点:
popen()
:创建并行进程,非阻塞执行escapeshellarg()
:防止命令注入攻击- 后台运行符
&
:脱离当前进程控制 - 输出重定向:
&> /dev/null
丢弃日志
3. 命令行消费者(守护进程)
class Jiayi extends Command
{
public function sendSkuToYjt(Input $input)
{
$queueName = $input->getOption('redis_name');
$context = json_decode($input->getOption('params'), true);
$redis = new RedisQueue();
$retryCount = 0;
while ($batch = $redis->lPop($queueName)) {
try {
$this->processBatch($batch, $context);
$retryCount = 0; // 重置重试计数器
} catch (Exception $e) {
if ($retryCount++ < 3) {
$redis->rPush($queueName, $batch); // 重新入队
sleep(pow(2, $retryCount)); // 指数退避
} else {
$this->logError($e, $batch);
}
}
}
}
private function processBatch($batch, $context)
{
$yjtData = YJTSku::convertYJTSkuData($batch);
$yjtData['yjt_token'] = YJTUtil::getToken($batch[0]['company_id']);
AbsYJT::sendYJT(
YJTUrlKey::ADD_SKU,
$yjtData,
$context['operator']
);
}
}
消费者特性:
- 失败重试机制(3次指数退避)
- 上下文传递(操作人信息)
- 异常处理与日志记录
- 批量处理支持
系统架构原理
方案优势分析
响应速度优化
- 主流程耗时从12s+降至200ms内
- 前端立即获得成功反馈
系统可靠性
- Redis持久化保证数据不丢失
- 重试机制应对第三方系统不稳定
- 进程隔离避免主流程崩溃
资源利用率
- 按需创建消费者进程
- 无常驻进程占用资源
- 可平滑过渡到专业队列系统
可观测性
- Redis队列长度监控
- 失败记录与告警机制
- 操作日志审计追踪
生产环境建议
安全增强
// 增加队列名前缀隔离 private function getCacheKey($name) { return config('app.env').':queue:'.$name; } // 命令执行增加权限校验 if (!in_array(get_current_user(), ['www-data', 'nginx'])) { exit('Permission denied'); }
性能调优
- 调整PHP-FPM的max_children配置
- 设置Redis内存淘汰策略为volatile-lru
- 监控队列堆积告警(通过Redis的LLEN命令)
高可用方案
- 部署多个消费者实例
- 使用Supervisor进程管理
- 设置Redis哨兵模式
本方案在保证系统轻量化的前提下,有效解决了同步接口的超时问题。后续若业务量增长,可通过以下步骤平滑升级:
- 引入RabbitMQ/Kafka专业消息队列
- 部署独立的消费者集群
- 增加流量控制与熔断机制
该模式适用于中小型系统的异步任务处理,特别是临时性、低频次的业务场景,能有效平衡开发成本与系统性能。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。