记一次奇葩的Discuz双站缓存乒乓效应排查
症状
两台独立服务器,跑着同一套Discuz代码,共享同一个MySQL数据库(主从架构,读写分离)。
诡异的是——两台服务器不能同时正常访问。
- 去A站后台清缓存 → A站正常了 → B站白屏
- 去B站后台清缓存 → B站正常了 → A站白屏
就像乒乓球一样,这边好那边就坏,永远只能活一个。
更迷惑的是,清完缓存后一切正常,直到下一次缓存自动更新时才出问题。
排查
错误日志里的关键线索
MySQL报错:duplicate key '1-1'
这是MySQL的复合主键冲突。1-1 的 1 来自 $_config['server']['id'] = 1(Discuz中所有站点的server_id都是1),另一个1是缓存行的ID。
PHP报错:501/503间歇性出现,PHP-FPM日志里大量32秒慢查询。
数据库缓存表中的并发写入
Discuz默认的缓存类型是 sql,也就是所有缓存数据都存放在同一个数据库表 dzx_common_syscache 中。
两站的缓存配置:
| 项目 | A站 | B站 |
|---|---|---|
| 缓存类型 | sql | sql |
| 写数据库 | 本地3306 | 远程3316 |
| 读数据库 | 本地3306 | 本地从库 |
两站后台清缓存时,会执行 DELETE FROM dzx_common_syscache,然后各自逐条INSERT重建缓存。
如果A站的PHP进程正在写某一行,B站的PHP进程恰好也在写同一行——duplicate key,必有一方写入失败。
写入失败的那一方,因为缺少了关键的系统设置缓存行,就会直接白屏。
清缓存为什么能临时修复
因为手动在后台"工具→更新缓存"是单线程执行的,不会产生并发冲突。
但线上的缓存更新请求是两个PHP进程同时在写同一个数据库表,必打架。
根本原因
两站共享一张缓存表 + 同时并发写入 = 必然冲突
数据库级别的缓存,本身就设计给单站点用的。两个独立服务器各自跑PHP进程,同时往同一张表里写,不出问题才怪。
解决方案
最简单的临时方案:两站使用不同的缓存方式。
我的选择是把B站从 sql 缓存改为 file 文件缓存。
// 改前
$_config['cache']['type'] = 'sql';
// 改后
$_config['cache']['type'] = 'file';为什么这样可行:
- B站不再读写数据库中的
dzx_common_syscache表 - B站的缓存存到本地
data/cache/目录下的文件里 - 两站不再竞争同一张数据库表
- 文件缓存比SQL缓存还要快(不需要走网络和数据库解析)
长期建议
如果条件允许,最规范的方案是引入Redis做独立缓存层:
A站 → Redis缓存
B站 → Redis缓存(不同db instance)这样两站完全隔离,互不干扰,而且Redis的读写性能远高于数据库查询和文件I/O。
经验教训
- Discuz的sql缓存不能跨多站共享——这是设计上单站点用的
- 乒乓球效应(一方正常→另一方必挂) 的根因=并发写入同一个数据库表导致的duplicate key
- 清缓存能临时恢复但会引爆对方 — 因为清完缓存后两边同时重建,冲突概率更高
- 改文件缓存是最快的隔离方案,不改架构不停机,一行配置搞定
记一次折腾了一整晚的排查经历,希望能帮到遇到同样问题的站长。