简介:一套基于PHP 7.0+构建的在线留言板系统源码,面向PHP学习者与Web开发入门者,可用于快速掌握留言提交、数据库存储与后台管理的基本流程。界面采用Bootstrap 5实现响应式布局,适配PC、平板与手机等不同屏幕;程序使用POST方法提交留言,内置后台可修改网站标题、删除留言,并附带默认登录密码说明,方便本地测试。资源包共133个文件,以113个PHP业务脚本为主体,涵盖前端页面、后台处理与公共配置,另有JSON配置、JavaScript脚本、文本说明及少量图片素材,压缩包大小约9.77MB,结构简洁易读。目前已有40人学习下载,适合用于课程设计、个人项目练手,或作为快速搭建留言反馈功能的参考模板,便于在此基础上扩展用户系统、内容审核等高级特性。
1. 基于PHP 7.0+重写在线留言板系统:不是老需求,是低成本方案
很多开发者觉得PHP留言板早就过时了,但企业内部意见收集、外贸站询盘、小程序后台留言,仍然需要一个不依赖大型框架的轻量模块。基于PHP 7.0+开发意味着能用上??运算符、标量类型声明和匿名类,而不是继续在mysql_*函数的泥潭里修补。下面从数据表、PDO通信、安全加固到nginx部署,完整走一遍可二次开发的在线留言板系统源码。如果你需要快速让一个新站点跑起来,或者想把这套逻辑并进已有后台,按步骤做就能落地。
2. 在线留言板系统的数据模型与PHP 7.0+选型理由
2.1 消息、回复与用户:三张表的划分逻辑
对留言板来说,最先要想清楚的是留什么数据。常见做法是三张表:messages存留言主体,replies存后台回复,users只存管理员账号。如果你硬要把所有信息塞进一张表,后面做审核、分页、统计都会很别扭。下面是我通常会用的建表脚本:
CREATE TABLE messages ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, nickname VARCHAR(50) NOT NULL DEFAULT '访客', content TEXT NOT NULL, email VARCHAR(100) DEFAULT NULL, ip VARCHAR(45) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2驳回', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE replies ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, message_id INT UNSIGNED NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_message_id (message_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, role TINYINT NOT NULL DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;字段说明:messages.status用整数而不是字符串,是为了查询时用索引更快,也方便扩展“审核中/已拉黑”这类状态;ip字段允许NULL,因为有些部署场景里 nginx 没有传出来;created_at直接交给 MySQL 生成,避免 PHP 时区设置不一致导致写入错误。replies里的message_id必须建索引,否则后台按留言查回复时会走全表扫描。
2.1.1 为什么不用JSON字段存回复
有人会问,既然 MySQL 5.7 之后支持 JSON 字段,为什么不直接把回复嵌在messages里?原因有两个:一是当回复数量和正文都变大时,SELECT会把整个 JSON 加载进来,后台列表页的响应会越来越慢;二是审核状态、置顶、软删除这些操作需要单独过滤,放在子表里写WHERE条件更干净。留言板的回复读取路径很简单,就是按message_id查子表,这种关系用传统外键其实最直接。
2.2 在PHP 7.0+上用PDO替代mysqli的三个原因
我见过不少老项目把mysql扩展换成mysqli就以为万事大吉,其实mysqli的参数绑定依然容易在传入字段和参数类型上出错。用 PDO 是更被推荐的方案:一是它支持命名占位符,二是它允许在连接层面统一设置异常模式,三是 PHP 7.0+ 之后 PDO 的异常类已经继承Throwable,可以更干净地与错误处理合并。
有人在 PHP 代码审计时发现,mysqli也可以做预处理,为什么还要用 PDO?因为 PDO 对于不同数据库的兼容性更好。虽然这个留言板主要跑在 MySQL 上,但当你需要把数据迁移到 PostgreSQL,或者做测试时用 SQLite,PDO 只需要改 DSN 和少量 SQL。下面的表格列出了我在项目里常用的 PDO 选项:
| 选项 | 推荐值 | 说明 |
|---|---|---|
PDO::ATTR_ERRMODE | PDO::ERRMODE_EXCEPTION | 让每次 SQL 错误都抛异常,配合全局 handler 记录日志 |
PDO::ATTR_DEFAULT_FETCH_MODE | PDO::FETCH_ASSOC | 默认返回关联数组,代码可读性更好 |
PDO::ATTR_EMULATE_PREPARES | false | 关闭预处理模拟,使用 MySQL 原生预处理,减少类型混淆风险 |
PDO::ATTR_STRINGIFY_FETCHES | false | 不把数字类型转成字符串,避免比较时出现===问题 |
这里要特别强调PDO::ATTR_EMULATE_PREPARES。默认情况下 PDO 可能会模拟预处理语句,也就是把参数拼接进 SQL 再发给 MySQL。开启原生预处理后,参数和 SQL 是分开传送的,SQL 注入的入口基本被堵死。在 PHP 7.0+ 上你还可以用intval()或显式类型声明把整型参数固定,比如function getMessage(int $id): array。
2.3 目录结构与自动加载设计
我一般不会把入口文件都堆在根目录。对于这个在线留言板系统,最小的“可二次开发”结构是这样的:
guestbook/ ├── composer.json ├── public/ │ ├── index.php │ └── assets/ ├── src/ │ ├── Database.php │ ├── Message.php │ ├── Reply.php │ └── Security.php ├── views/ │ ├── index.php │ └── admin.php └── storage/ └── logs/public目录作为服务器唯一暴露的入口,src目录放业务类,views只做模板。这样设计后,即使你用的是 PHP 7.0+ 自带的开发服务器,也可以避免别人直接读取配置文件。Composer 的 autoload 会按 PSR-4 规则加载src/下的类,composer.json里这样写就能满足需求:
{ "require": { "php": ">=7.0" }, "autoload": { "psr-4": { "GuestBook\\": "src/" } } }这里没有要求第三方包,所以连composer install都很快。GuestBook\\的命名空间对应src/目录,后面新增类只要放在src/下并声明命名空间,就能被自动加载。不推荐在这个阶段引入 Laravel/Symfony 这类全家桶,留言板业务足够简单,框架的学习成本和部署体积反而会拖慢你排查问题。
3. 用PHP 7.0+实现留言发布与分页展示的核心代码
3.1 数据库连接与全局异常处理
所有接口的第一步是连接数据库。我习惯写一个单例的 Database 类,然后设置 PDO 的错误模式。注意 PHP 7 之后,构造函数里的异常处理不再依赖函数返回值。当 SQL 出错时,异常会直接抛到全局 handler,避免泄露路径和 SQL:
<?php namespace GuestBook; use PDO; class Database { private static $instance; private $pdo; private function __construct() { $dsn = 'mysql:host=' . getenv('DB_HOST') . ';dbname=' . getenv('DB_NAME') . ';charset=utf8mb4'; $this->pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false ]); } public static function pdo(): PDO { if (!self::$instance) { self::$instance = new self(); } return self::$instance->pdo; } }这里用getenv()而不是在代码里写死账号,是为了让同一份源码能直接部署到 docker 或者云主机。如果你没有环境变量,也可以用parse_ini_file读取 config.ini,但要注意把配置文件放在 web 根目录之外。参数说明:DB_HOST默认可以是127.0.0.1,DB_NAME是留言板库名。使用单例可以减少重复连接,但要注意在长生命周期任务里连接可能会断,这时需要监听PDO::ATTR_TIMEOUT或者用进程内重建。
3.2 留言提交接口:CSRF令牌与输入过滤
提交留言是核心入口,最容易出问题的不是 SQL 注入,而是 XSS 和 CSRF。先看提交部分代码。为了演示 PHP 7.0+ 的??运算符,我直接从$_POST里取值:
public function create(array $input): int { $token = $input['csrf_token'] ?? ''; if (!hash_equals($_SESSION['csrf_token'] ?? '', $token)) { throw new \Exception('CSRF token mismatch', 403); } $nickname = mb_substr(trim($input['nickname'] ?? ''), 0, 20); $content = mb_substr(trim($input['content'] ?? ''), 0, 2000); if ($content === '' || mb_strlen($content) < 5) { throw new \InvalidArgumentException('content too short'); } $stmt = Database::pdo()->prepare( 'INSERT INTO messages (nickname, content, ip, status) VALUES (:nickname, :content, :ip, 0)' ); $stmt->execute([ ':nickname' => $nickname, ':content' => $content, ':ip' => $_SERVER['REMOTE_ADDR'] ?? null ]); return (int) Database::pdo()->lastInsertId(); }这个函数先校验 CSRF,再截断长度,然后直接写入数据库。这里故意没有做htmlspecialchars转义,因为转义应该统一放在输出层,否则后台编辑留言时会出现双重编码。hash_equals是 PHP 5.6 引入的,在 PHP 7.0+ 上仍然推荐使用,它可以避免时间侧信道攻击。
3.2.1 为什么入库时不做HTML实体转义
如果写入时就把<转成<,数据库里存的就是被编码后的文本。当你要在后端编辑这条留言时,编辑器读出来的内容会带上一堆实体符号,保存时还要再判断是否已经转义。更安全的做法是数据库存原始文本,所有视图和接口在输出前调用统一的转义函数。这条原则适用于纯文本留言板,如果未来要支持富文本,需要引入特定的过滤白名单,而不是简单转义。
3.3 分页查询与模板输出
前台列表需要按时间倒序,并做分页。分页必须绑定整型参数,这一点很容易在代码审计中被忽略。下面的代码使用命名占位符,同时设定PDO::PARAM_INT:
public function paginate(int $page = 1, int $perPage = 10): array { $pdo = Database::pdo(); $offset = ($page - 1) * $perPage; $total = $pdo->query('SELECT COUNT(*) FROM messages WHERE status = 1')->fetchColumn(); $stmt = $pdo->prepare( 'SELECT id, nickname, content, created_at FROM messages WHERE status = 1 ORDER BY id DESC LIMIT :limit OFFSET :offset' ); $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); return [ 'items' => $stmt->fetchAll(), 'total' => (int) $total, 'page' => $page, 'pages' => max(1, (int) ceil($total / $perPage)) ]; }这里的bindValue必须用PDO::PARAM_INT绑定两个变量,因为LIMIT子句里的参数不能用字符串占位。如果你在execute里直接传数组,PDO 会默认把它们当字符串,MySQL 虽然能自动转换,但有时会失去索引优化。使用?占位符也可以,但命名占位符在参数超过三个时更易读。
输出时,模板里可以使用统一的转义函数,在 PHP 7.0+ 上可以配合 nullable 类型:
function e(?string $value): string { return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8'); }这样在视图里写<?php echo e($item['content']); ?>就行。这个函数还处理了 null,避免空数组字段直接报错。建议把e()放在公共函数文件里,并在系统初始化时加载。
3.4 用PHP 7.0+的匿名类处理回复通知
留言板后台审核后可能需要通知前端,这个场景不需要引入任务队列,直接用 PHP 7.0+ 的匿名类就能做一个很简单的观察者。匿名类可以用在需要临时实现接口的地方,比如给回复创建一个钩子:
$notifier = new class { public function onReply(Reply $reply) { error_log('Message ID ' . $reply->getMessageId() . ' has been replied.'); } }; $notifier->onReply($reply);这个例子有点刻意,但想说明的是,PHP 7.0+ 项目不必为了“面向对象”写一堆没有复用的接口和实现类。匿名类适合一次性的渠道切换,比如把日志从文件改成syslog,直接在构造函数里传入一个匿名类对象。在留言板这种业务上,它的意义是让你在维持源码结构整洁的同时,不损失“可替换实现”的灵活性。
4. 在nginx与php-fpm上部署PHP 7.0+留言板源码的完整流程
4.1 环境检查与php.ini关键参数
拿到一套源码,先不要急着扔进网站目录。先确认 PHP 版本和扩展。这个项目要求 PHP 7.0+,但实际我推荐用 PHP 7.4 或 8.0 以上,因为 PHP 7.0 已经停止维护。用php -v看版本,php -m看模块。下面这个表格是必须检查的扩展:
| 扩展 | 用途 | 如果缺失 |
|---|---|---|
pdo_mysql | 数据库访问 | 无法连接MySQL |
mbstring | 中文字符截断与编码 | 报Call to undefined function |
json | 接口数据交换 | 无法解析JSON |
openssl | 密码哈希和HTTPS | 后台登录失败 |
filter | 输入过滤 | FILTER_VALIDATE_INT不可用 |
4.1.1 检查扩展的快捷命令
在 Linux 上可以一次性列出需要的扩展并校验:php -m | grep -E 'pdo_mysql|mbstring|json|openssl|filter'。如果有未安装的项,Debian/Ubuntu 上用apt install php7.4-mbstring装完后重启 php-fpm。如果是共享主机,不能装扩展,就要在代码里提前判断extension_loaded()并给出友好错误页,而不是等到调用mb_substr时直接白屏。
php.ini里主要调整这几项:memory_limit至少 128M,upload_max_filesize如果不需要上传保持默认;date.timezone设为你的业务时区,不然created_at依赖数据库时间可能相差 8 小时;error_reporting建议在开发时开E_ALL,生产环境设置display_errors=Off,并打开log_errors=On。
4.2 nginx站点配置与伪静态规则
为了让留言板地址更美观,比如/index.php?page=2变成/page/2.html,需要在 nginx 里做一次 rewrite。由于留言板只有一个入口,配置并不复杂:
server { listen 80; server_name gb.example.com; root /var/www/guestbook/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(js|css|png|jpg|gif)$ { expires 30d; access_log off; } }注意root指向public,而不是项目根目录。如果不这样设置,别人访问http://域名/composer.json就能直接看到源码依赖声明。try_files指令是让前端控制器生效的关键:当请求路径不存在时,全部交给index.php,再由Message::paginate根据page参数返回数据。
关于 fastcgi 的版本,如果你的 PHP 是 7.0,那么 socket 路径可能是php7.0-fpm.sock,需要在/etc/php/下确认。SCRIPT_FILENAME中$document_root变量在较新 nginx 版本中可以直接用,但有些发行版会提示你改用$realpath_root,出现 file not found 错误时优先检查这里。
4.3 使用Composer安装依赖和生成autoload
虽然这个留言板核心没有第三方依赖,但使用 Composer 的目的是让 PSR-4 自动加载生效。部署时执行:
composer install --optimize-autoloader --no-dev如果没有全局安装 Composer,可以先下载composer.phar放到项目根目录。--optimize-autoloader会生成 classmap,减少每次请求时的目录扫描开销;--no-dev确保不会把测试工具部署到生产环境。对于 PHP 7.0+ 项目,Composer 1.x 可以支持,建议用 2.x,但 Composer 2 要求 PHP 7.2.5+。如果你的服务器是 PHP 7.0,可以先执行composer selfupdate看看是否兼容,不行就用 1.x 的 phar。
如果你从网站下载的“php免费网站”类源码不提供 Composer,就需要把vendor/autoload.php删掉,改成手动spl_autoload_register。但我不推荐,因为一旦项目变大,手动 autoload 很容易漏类。推荐直接维护好composer.json。
4.4 错误日志与502排错
部署后最常遇到的是 502 Bad Gateway,通常有三个原因:php-fpm 进程没启动,socket 路径不对,或者 PHP 执行超时。先看php-fpm状态:
systemctl status php7.4-fpm tail -f /var/log/nginx/error.log如果 error.log 里出现connect() to unix:/run/php/php7.4-fpm.sock failed,说明 socket 路径对不上;如果出现Primary script unknown,多半是 root 配置错误。这个排查步骤也是对源码做一次 PHP 错误处理验证:确认display_errors关闭、log_errors开启后,所有异常都应该能落到/var/log/php/error.log,不会直接打印到页面上。
5. 在线留言板系统的安全加固与PHP代码审计常见坑
5.1 XSS防御:输出编码与数据库存原文的取舍
留言板最大的威胁是有人在内容里插<script>。我在第3章已经说过,数据库里建议存原始文本,输出时统一编码。这是对纯文本场景最不容易出错的做法。下面是一个统一输出函数:
function h(?string $value): string { return htmlspecialchars((string) ($value ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }ENT_SUBSTITUTE会在遇到无效 UTF-8 序列时用替代字符替换,而不是直接返回空字符串,能防止某些基于编码绕过的 XSS。在模板中所有动态字段,包括nickname、content、email都必须套上这个函数。另外,如果你用json_encode输出到前端,要使用JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT这四个标志,否则 JSON 里的</script>会提前结束标签。
5.1.1 关于ENT_SUBSTITUTE的补充
有些老代码在转义时只写htmlentities($str),默认字符集是 ISO-8859-1,遇到中文会被转成奇怪的实体。统一指定UTF-8并且带上ENT_SUBSTITUTE,可以避免两种问题:一是非法编码产生空字节,二是浏览器无法识别实体导致的乱码。这个细节在代码审计时很容易漏掉。
5.2 SQL注入防御:预处理不是万能保险
PDO 预处理能防大部分注入,但有两个例外:标识符(表名、列名)不能通过占位符绑定;ORDER BY子句中的字段名和方向也不能。留言板如果有排序功能,必须用白名单过滤:
$allowedSort = ['id', 'created_at']; $sort = $allowedSort[$input['sort']] ?? 'id';如果你把$input['sort']直接拼进 SQL,即使是用prepare()也无济于事,因为预处理只处理值,不处理结构。在 PHP 代码审计时,这属于高危问题。所以凡是出现在 SQL 里的非值部分,一律先映射到固定集合。
另外,PDO 的ATTR_EMULATE_PREPARES一旦被开启,预处理实际是本地模拟,参数会拼进 SQL 发给 MySQL。这种情况下如果你使用了LIMIT这样的子句,PDO 可能不会正确转义,所以我们需要继续保持false。在共享主机上如果无法修改 PDO 选项,可以考虑使用intval()强制把分页参数变成整数。
5.3 CSRF防护与频率限制的完整实现
上面的代码只验证了 token,没有详细说 token 生成。正确做法是每个会话生成一次,而不是每次请求都变:
if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); }random_bytes是 PHP 7.0+ 推荐的安全随机数函数,不要再用rand()或mt_rand()。提交表单时把 token 放在一个 hidden 字段里,并用hash_equals比较。如果前后端完全分离,可以在 API 层用一个自定义响应头X-CSRF-Token,JavaScript 读取后再放入请求头。
频率限制可以直接用数据库计数或者简单的文件锁。我会在messages表增加一个字段ip_hash,然后每次提交前查一下最近 60 秒的计数:
SELECT COUNT(*) FROM messages WHERE ip_hash = :ip_hash AND created_at > NOW() - INTERVAL 60 SECOND;注意ip_hash不能存原始 IP,因为弱口令会导致用户隐私问题。使用hash('sha256', $ip . $salt)生成一个不透明的值。这个查询如果发现超过 3 次,就返回一个 429 状态码。这种方案比验证码更流畅,但对刷接口的脚本来说也足够挡掉一部分。
5.4 代码审计视角:留言板常见的4个漏洞
结合以前做 PHP 代码审计的经验,最常出问题的地方不是 SQL 注入,而是文件上传和变量覆盖。下面的表格是一份自查清单:
| 风险点 | 危害 | 修复建议 |
|---|---|---|
未对$_GET做类型校验 | 反射型XSS | 用filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) |
| 管理员登录密码明文 | 拖库后直接泄露 | 使用password_hash和password_verify |
| 备份文件留在 web 根目录 | 数据库密码泄露 | 把.sql移到根目录之外或加 deny 规则 |
日志记录$_REQUEST | 日志存储型XSS | 记录$_SERVER['HTTP_USER_AGENT']时用strip_tags |
特别是密码哈希,PHP 7.0+ 内置的password_hash默认使用 bcrypt,不要再用 md5。登录验证码也应基于 session,而不是直接比较验证码文本,因为比较不及时会留下暴力破解窗口。
6. 让留言板支持实时新留言提醒的Redis订阅实现
6.1 轮询与SSE的取舍
留言板通常不需要像聊天室那样实时,但“用户提交后管理员能立刻看到”是很常见需求。传统做法是管理后台每隔几秒轮询一次unreadCount接口。轮询实现简单,但当管理页面开得很多时,会产生无谓的数据库压力。Server-Sent Events(SSE)是更轻量的选择:浏览器通过EventSource订阅一个 PHP 流,服务器有消息时再推给客户端。
SSE 与 WebSocket 最大的区别是不需要额外协议,可以直接跑在 HTTP/HTTPS 上,透过 Nginx 也能工作。缺点是不支持旧版 IE,只支持单向服务端推送,但留言板场景已经够用。下面给出两者对比:
| 方案 | 传输方向 | 连接数 | 实现成本 | 适用场景 |
|---|---|---|---|---|
| 轮询 | 客户端主动拉 | 高 | 低 | 低频更新 |
| SSE | 服务端推 | 低 | 中 | 留言提醒、通知 |
| WebSocket | 双向 | 低 | 高 | 聊天、协作 |
6.2 在PHP 7.0+中用Redis发布订阅实现简易推送
如果项目里已经引入了 Redis,可以使用它的 Pub/Sub 功能。在留言板提交成功后,除了插入数据库,再publish一个事件。后台管理页面通过一个长连接的 PHP 脚本订阅该频道,有消息时把留言数据写回 SSE 流。
先看发布端,在create()方法末尾增加:
$redis = new \Redis(); $redis->connect('127.0.0.1', 6379); $redis->publish('guestbook_new_message', json_encode([ 'id' => $newId, 'nickname' => $nickname, 'created_at' => date('Y-m-d H:i:s') ]));这里假设你已经安装 phpredis 扩展。如果没有,用 Predis 库也可以,但那是纯 PHP 实现,在高并发下 CPU 占用会高一些。连接 Redis 时不要每次请求都新建连接,应该在 Database 单例旁边再做一个 Redis 单例,但注意在 CLI 常驻模式下要处理断线重连。
6.3 验证推送效果与压测的小技巧
写完上面的功能后,如何验证没有遗漏消息?我一般会开启 Redis 的 MONITOR 命令,观察publish和subscribe是否配对。具体做法是打开一个终端运行redis-cli MONITOR,然后在另一个终端提交一条留言,如果看到PUBLISH guestbook_new_message说明发布正常。SSE 客户端需要在浏览器里打开管理页,看到控制台输出[data]即完成。
如果要压测推送能力,不要用 ab 压 SSE 接口,因为 SSE 是长连接,ab 会把线程全部占满。用wrk或者直接用curl -N手动接收。对于留言板,更实际的是压POST /message接口,使用以下命令模拟 100 个并发持续 30 秒:
wrk -t4 -c100 -d30s -s post.lua http://gb.example.com/messagepost.lua需要包含 CSRF token 和随机内容,但压测前最好关闭 CSRF 校验,否则所有请求都会返回 403,测出来的数据没有意义。如果发现 Redis 订阅丢失,先检查默认超时时间,phpredis 的subscribe方法在没有消息时会阻塞,不要被 PHP 的max_execution_time限制住,需要在 CLI 或 fastcgi 配置中放开。最后建议把后台自动刷新的时间设置为 30 秒而不是 5 秒,配合 Redis 订阅的长连接,可以显著降低数据库压力;如果 Redis 不可用,这个 30 秒轮询也能保证最坏情况不超过 30 秒延迟。
本文还有配套的精品资源,点击获取