铁匠日记2内置MOD菜单
81.46MB · 2025-10-10
在现代 PHP 应用开发中,将耗时任务(如邮件发送、报表生成、数据同步)从主请求流程中剥离出来进行异步处理,是提升用户体验和系统吞吐量的关键。虽然 RabbitMQ、Kafka 等专业消息队列是标准解决方案,但对于中小型项目或追求轻量级架构的场景,我们往往希望找到一个更“接地气”的方案。
本文记录了一次完整的技术探索之旅:从一个简单的想法开始,如何利用 PHP 生态中已有的工具,构建一个能够应对“大量写入、少量消费”场景的高性能队列。
我们的核心需求很简单:
1. 轻量级:不希望引入新的、重型的服务依赖(如 Java/Erlang 系的消息中间件)。
2. 可靠:任务不能轻易丢失。
3. 高性能:能够承受 Web 应用在高并发下的大量任务写入。
很自然地,我们的第一个想法落在了 SQLite 身上。它无需额外服务、零配置、就是一个文件,完美符合“轻量级”的要求。
还有一个需求,这个针对的是单机内部的队列,比如日志记录,日志先记录到队列中,再通过消费者统一记录到集中机器上。
使用 SQLite 做队列的思路非常直接:生产者 (PHP-FPM) 在 Web 请求中向 tasks
表 INSERT
一条记录;消费者 (CLI) 则是一个后台脚本,循环 SELECT
任务来处理。
然而,当面临“大量写入、少量消费”的场景时,瓶颈很快出现:写入并发。SQLite 在默认模式下,任何时刻只允许一个写入者。当成百上千个 PHP-FPM 进程同时尝试插入任务时,会激烈地争夺数据库的写锁,导致大量请求失败或超时。
为了解决并发问题,一些基础优化是必须的:
1. 开启 WAL 模式:PRAGMA journal_mode=WAL;
,允许一个写入者和多个读取者并发,极大缓解了锁争用。
2. 设置超时:PRAGMA busy_timeout = 5000;
,让写入失败的进程等待一段时间而不是立即报错。
3. 使用高速磁盘 (SSD):从物理层面提升 I/O 性能。
这些优化能解决大部分问题,但如果写入压力达到极限,我们还能做什么?
为了彻底消除磁盘 I/O 瓶颈,我们自然想到了内存。
APCu 是 PHP 的一个共享内存缓存。我们可以用一个共享数组来当队列。但这很快暴露了新问题:
**CPU 争用:**为了保证并发安全,我们必须使用 apcu_cas (Compare-And-Swap) 循环来写入。在高并发下,大量进程会在此处“忙等待” (Busy-Waiting),疯狂空转 CPU,导致系统负载飙升。
**数据易失:**服务重启,内存中的所有任务全部丢失。
结论:APCu 方案在高并发写入下性能不佳且有数据丢失风险,不推荐。
Redis 是内存方案的工业标准。它使用高效的事件循环模型,提供原子的列表操作 (LPUSH
/BRPOP
),性能卓越且功能丰富。它也支持数据持久化。唯一的“缺点”是,它违背了我们最初“不引入新服务”的原则。
有没有一种方案,既能拥有内存的极致速度,又能利用 SQLite 成熟的并发模型,还无需修改代码?答案是:将 SQLite 数据库文件放在内存文件系统 (tmpfs) 中。
在 Linux 中,tmpfs
是一个基于内存的文件系统(/dev/shm
就是一个现成的例子)。我们可以将 SQLite 数据库文件创建在这里。
在现代化的 Docker 开发环境中,实现这个方案变得异常简单和优雅。我们无需手动在服务器上执行 mount
命令,只需在 docker-compose.yml
中声明即可。
# File: docker-compose.yml
version: '3.8'
services:
php-fpm:
build: ./php
volumes:
- ./src:/app
# 关键配置:声明一个 tmpfs 挂载
tmpfs:
# 推荐方案:精确设置所有者
- /app/ramdisk:size=256M,uid=33,gid=33
# ... 其他服务
由于 PHP-FPM 进程通常以低权限用户(如 www-data)运行,而 tmpfs 默认由 root 创建,直接写入会因权限不足而失败。
**专业做法(推荐):**通过 uid=33,gid=33(www-data 的典型 ID),我们精确地将内存目录的所有权交给了 PHP 进程,这遵循了安全的最小权限原则。
粗暴捷径(不推荐):使用 mode=0777
。这会将目录权限设为 rwxrwxrwx
,允许容器内的任何用户写入。虽然能解决问题,但它过度授权,留下了安全隐患。
由此来看,如果我们不通过Redis或其他专业的消息队列服务,同时要处理的任务是在单机内的,不需要远程互联,那么我们完全可以通过sqlite实现,再配合linux开启内存文件系统,避免触发磁盘IO。虽然无论从写入、更新、删除、读取等任何方面,Redis都更专业,性能更好,但我们的方案是更简单的,同时可以利用sql表结构,很省事的实现队列的需求。