☰
dede数据库分离部署:远程连接排查与跨库调用实战
2026/11/3 13:03:50 网站建设 项目流程

网站和数据库拆到两台机器上跑,这事儿在早期建站圈子里其实挺常见——一台机器扛不住的时候,先把数据库挪出去;或者反过来,数据库是老的、数据不能丢,网站换新机器重装。麻烦就麻烦在,dede 这套老牌 PHP 内容管理系统天生是"本机数据库"思维,默认配置里数据库地址写的就是 localhost,一旦拆开,首页白屏、后台登录转圈、栏目页 500 就全来了。这篇文章讲的就是这件事:网站与数据库不在同一服务器时怎么把连接打通,以及两套跑在不同机器上的 dede 数据库之间怎么互相调用数据。适合手上有存量 dede 站点、正在做机房迁移或者多站整合的运维和开发者看,纯新手也能照着一步步走下来。

1. 拆开部署之后,dede 的连接配置到底改哪几处

很多人以为分离部署就是把配置文件里的 localhost 改成 IP,改完发现不行,又改回来,最后得出的结论是"dede 不支持远程数据库"。这个结论是错的,但错的也不冤——因为 dede 的连接建立过程里,配置文件只是第一道门,后面还有账号授权、监听地址、网络放行三道关,任何一道没通,表现都一样:连不上。所以这一节我不按"步骤一二三"讲,按"哪几处必须同时满足"来讲,你对着清单排查会快很多。

1.1 配置文件里那个 $cfg_dbhost 只是第一步

dede 的数据库参数集中在data/common.inc.php这一个文件里,内容大概长这样:

<?php $cfg_dbhost = 'localhost'; $cfg_dbname = 'site_main'; $cfg_dbuser = 'site_main_rw'; $cfg_dbpwd = '******'; $cfg_dbprefix = 'dede_'; $cfg_db_language = 'utf8'; ?>

分离部署要动的只有$cfg_dbhost,把它换成数据库服务器的地址。如果数据库端口不是默认的 3306,dede 这个变量是支持写成10.0.3.10:3307这种形式的,底层的 mysqli 连接会自己解析。这一点知道的人不多,不少人为了改端口去动include/dedesqli.class.php,其实完全没必要。

但这里有个特别容易翻车的细节:改这个文件的时候一定要用编辑器直接改,不要用某些面板的"在线文件编辑"功能。dede 的common.inc.php是纯 PHP 文件,开头不能有任何空格、BOM 头或者换行,一旦多了个 UTF-8 BOM,PHP 会先把 BOM 当输出打出去,然后才执行header(),结果就是后台登录一直提示验证码错误、或者页面顶部多出一行乱码。我见过至少三次"改完数据库地址后后台登不上",最后查出来都是 BOM 头。

另外,如果你是多站点共用一套 dede 程序(比如用不同的data/common.inc.php做多域名),那这个文件的位置可能被改到了别处,得先确认当前站点实际加载的是哪个配置。最直接的办法是在站点根目录临时放一个 phpinfo 探针,或者干脆在index.php最前面加一行echo __FILE__;看它从哪个目录起来。这一步花两分钟,能省掉后面半小时的自我怀疑。

1.2 数据库侧的账号授权才是真正的拦路虎

配置文件改完,绝大多数人卡在第二步:Access denied for user 'site_main_rw'@'10.0.3.15'。这个报错的意思是,数据库认识这个用户,但不允许从这个来源 IP 登录。

MySQL 的账号是"用户名 + 来源主机"两段式组成的,'site_main_rw'@'localhost'和'site_main_rw'@'10.0.3.15'在 MySQL 眼里是两个完全不同的账号。本地跑得好好的,是因为本地登录匹配的是localhost那条记录;换成远程 IP,匹配不上,就拒绝。

正确做法是给 Web 服务器的私网 IP 单独授权。MySQL 5.7 及以前的版本可以一条语句搞定:

GRANT SELECT, INSERT, UPDATE, DELETE ON `site_main`.* TO 'site_main_rw'@'10.0.3.15' IDENTIFIED BY '一个足够长的密码'; FLUSH PRIVILEGES;

MySQL 8.0 开始不支持在 GRANT 里建用户,得拆成两步:

CREATE USER 'site_main_rw'@'10.0.3.15' IDENTIFIED BY '一个足够长的密码'; GRANT SELECT, INSERT, UPDATE, DELETE ON `site_main`.* TO 'site_main_rw'@'10.0.3.15'; FLUSH PRIVILEGES;

这里我强烈建议不要图省事写成'site_main_rw'@'%'。%的意思是任意来源都能连,等于把数据库的门锁拆了,只要端口对上,谁都能来试密码。正确姿势是精确到 Web 服务器那一台机器的私网 IP,如果未来可能加机器,再补第二条授权记录,也别用通配。

还有一点:dede 后台有些操作是需要额外权限的,比如"系统 - 数据库备份/还原"、"系统 - SQL命令行工具"、以及部分版本的"更新栏目缓存"。如果你给的是严格的SELECT/INSERT/UPDATE/DELETE,后台某些功能会报权限不足。我的做法是给 Web 站点用的账号保持四件套权限,另外单独留一个高权限账号只在需要做备份还原时临时使用,用完就撤。这样日常运行面最小,出事的时候也不会因为权限太大而被一锅端。

1.3 端口、监听地址与安全组三件套

授权做完了还是连不上,那就往下沉一层,看网络。这里有三个独立的开关,任何一个关着都不通:

检查项位置典型错误配置正确做法
监听地址数据库服务器my.cnfbind-address = 127.0.0.1改成私网 IP 或0.0.0.0
端口放行系统防火墙 / 安全组只开了 80、443、22放行 3306,且来源限定为 Web 服务器 IP
用户来源MySQL 授权表只有@localhost补@Web服务器私网IP

bind-address是最隐蔽的一个。这个参数默认是127.0.0.1的时候,MySQL 只在回环地址上监听,外面的机器连过来,TCP 层就直接被拒了,连"密码错误"的机会都没有,报的是Can't connect to MySQL server on '10.0.3.10' (111)。改完配置记得重启服务,并且用ss -lntp | grep 3306确认监听地址已经变成10.0.3.10:3306或0.0.0.0:3306。

防火墙这块,用iptables的老机器上加规则;用云上的安全组,就加一条入方向规则,协议 TCP、端口 3306、源地址填 Web 服务器的私网 IP(不要填0.0.0.0/0)。命令行验证连通性不要用ping,ping通不代表 3306 通,用nc -zv 10.0.3.10 3306或者telnet 10.0.3.10 3306,看到succeeded或者Connected才算过。

提示:从 Web 服务器上用mysql -h 10.0.3.10 -u site_main_rw -p手动连一次,是区分"网络问题"和"应用问题"最快的手段。命令行能连上而网站连不上,问题一定在 PHP 配置或者 dede 的配置文件路径上,跟网络无关,别再去折腾防火墙了。

2. 远程连库的隐性成本:延迟、超时与连接数

连接打通只是开始,真正让人难受的是性能。本地数据库的时候,一次查询的往返时间大概是零点几毫秒,你可以随便写循环、随便在模板里塞十几个{dede:arclist};换成同机房内网的远程数据库,往返时间变成零点几到一毫秒多一点,感觉还行;但如果跨了机房、或者不幸走了公网,往返时间直接飙到十几毫秒甚至几十毫秒,页面的加载时间就会以肉眼可见的速度涨。

2.1 一次页面请求背后到底建了几次连接

dede 的执行流程是:index.php引入include/common.inc.php,里面new DedeSql(false)建立全局$dsql,之后所有的查询都复用这一个连接。所以正常情况下,一个页面请求只建一次连接,这个设计本身没问题。

问题出在"一次连接"背后的握手成本上。MySQL 建立连接要做 TCP 三次握手、可选的反向 DNS 解析、身份认证、初始化会话变量,整套下来本地可能是 1 毫秒,远程可能是 20 毫秒。如果页面只查一次库,这 20 毫秒无所谓;但 dede 的列表页往往要跑十几条查询,如果这些查询分摊在多个连接上,代价就翻倍了。

更麻烦的是后台的批量操作。比如后台"更新整站 HTML"、批量审核文章、批量移动栏目,这些操作在循环里不停查库,如果每轮都建连接,几百篇文章跑下来能把执行时间顶到超时。我遇到过最夸张的一次,是一次性发布三百多篇文章,页面跑了四分钟还没结束,最后是max_execution_time把它掐了,前两百篇发布成功、后面一百多篇状态不明,只能手工核对。

解决办法很朴素:确认 dede 用的是 mysqli 而不是已经废弃的老 mysql 扩展(老扩展在 PHP 7 之后已经移除,如果你的程序还能跑,说明已经是 mysqli 版本);确认没有哪个插件在循环里new mysqli()。我在改造一个老站的时候,就发现某个统计插件每次调用都在函数内部重新连接一次数据库,一个首页调用了六次,等于凭空多做了六次远程握手。改成全局复用$dsql之后,首页耗时从 1.2 秒降到 0.4 秒。

2.2 wait_timeout 与 "MySQL server has gone away"

远程连接还有一个本地连接不会遇到的坑:空闲连接被服务端主动掐掉。MySQL 有个wait_timeout参数,默认 28800 秒(8 小时),意思是连接空闲超过这个时间,服务端就把它关了。本地连接几乎不会碰到,因为进程生命周期短;但远程场景下,如果你用的是长连接(pconnect),或者用了常驻进程(比如某些队列消费者、定时脚本),连接一挂就是几个小时,再拿起来用就会报MySQL server has gone away。

dede 本身默认用的是短连接,每个请求结束连接就释放,所以前台页面基本不会遇到。但如果你写了定时同步脚本、或者后台跑长时间的数据导出,中途停顿时间长一点就会中招。

判断和处理的思路是这样:先看报错出现的时间点,如果是脚本启动后立刻报,那是连接参数问题(比如max_allowed_packet太小,一次写入大字段正文超过限制,连接被服务端强制断开);如果是脚本跑了很久之后报,那就是wait_timeout。前者的解决办法是调大两边的max_allowed_packet,dede 的文章正文存在dede_addonarticle.body里,一篇带大量内联图片的长文能到几 MB,默认 4MB 有时候真不够。后者的解决办法是在长脚本里每处理一批就主动ping()一次,或者干脆把wait_timeout在会话里临时调大。

-- 在长脚本开头执行,把本次会话的超时拉到 8 小时 SET SESSION wait_timeout = 28800; SET SESSION interactive_timeout = 28800;

2.3 skip-name-resolve 带来的那几秒

这个细节比较冷,但踩过的人印象深刻。MySQL 在收到连接请求时,默认会尝试把来源 IP 反向解析成域名,解析成功才继续。如果 DNS 服务器响应慢、或者根本解析不了,每次连接都要干等几秒甚至十几秒才超时。表现出来就是"网站偶尔打开特别慢,慢的时候十几秒,快的时候秒开",而且没有规律。

关闭反向解析的办法是在数据库服务器的my.cnf里加一行:

[mysqld] skip-name-resolve

加完重启。副作用是授权表里就不能再用主机名了,必须全部用 IP 或者%。如果你的授权本来就是按 IP 写的,那没影响;如果写的是'user'@'web01.example'这种主机名形式,得先改成 IP 再开这个参数,否则会全部登录失败。

我一般的做法是:分离部署一上来就把skip-name-resolve打开,不要等出了问题再查。这个参数开了只有好处,没有坏处(前提是授权按 IP 写),能省掉一大堆"玄学卡顿"。

3. 跨库调数据的四条路线,以及各自的适用边界

把数据库拆开之后,很多人紧接着会冒出第二个需求:两个站的数据能不能互通?比如主站和子站各自有一套 dede,想把主站某个栏目的最新文章拉到子站首页显示;或者新旧两套系统并行期间,新站要从老库里读历史文章。这类需求在 dede 圈子里问得非常多,答案不止一种,选错了后期维护会很痛苦。

3.1 同实例跨库查询:最省事也最容易被忽略

如果两个 dede 的数据其实在同一个 MySQL 实例里(只是分成了两个库,比如site_main和site_sub),那恭喜你,这是最舒服的情况,一条 SQL 就能跨库查,语法就是在表名前加库名:

SELECT a.id, a.title, a.litpic, a.senddate, a.description FROM site_main.dede_archives a WHERE a.typeid = 12 AND a.arcrank >= 0 ORDER BY a.senddate DESC LIMIT 10;

这种做法几乎零额外成本,查询优化器能正常走索引,事务也是统一的。缺点是耦合度高——两个库之间形成了硬依赖,将来想把其中一个库迁走就麻烦了。所以它适合"两个站本来就是一家人、短期内不打算拆开"的场景。

用 dede 自带的{dede:sql}标签就能直接在模板里写这种跨库查询,不用改任何代码,模板里大概是这样:

{dede:sql sql="SELECT id,title FROM site_main.dede_archives WHERE typeid=12 AND arcrank>=0 ORDER BY senddate DESC LIMIT 10"} <li><a href="/plus/view.php?aid=[field:id/]">[field:title/]</a></li> {/dede:sql}

注意{dede:sql}标签里不能直接写<、>这类符号,会被模板解析器吃掉,得用&lt;、&gt;转义,或者换个写法绕开。这个小坑我第一次用的时候卡了半个小时,页面直接空白,什么报错都没有。

3.2 FEDERATED 引擎:像本地表一样查远程表

如果两个库在不同实例上,又想用 SQL 直接查,那就要请出 FEDERATED 引擎。它的作用是建一张"影子表",本地只存表结构,真实数据放在远端,查询的时候自动转发过去。

CREATE TABLE dede_remote_archives ( id int(11) unsigned NOT NULL AUTO_INCREMENT, typeid smallint(5) unsigned NOT NULL DEFAULT '0', title varchar(60) NOT NULL DEFAULT '', senddate int(10) unsigned NOT NULL DEFAULT '0', arcrank smallint(6) NOT NULL DEFAULT '0', PRIMARY KEY (id) ) ENGINE=FEDERATED CONNECTION='mysql://reader:密码@10.0.3.10:3306/site_main/dede_archives';

用之前得确认两件事:一是本地 MySQL 编译时带了 federated 支持,二是配置里开了federated=1(默认是关的,要手动加在my.cnf里然后重启)。用SHOW ENGINES;看一眼,FEDERATED那行的 Support 列是YES或者DISABLED就说明支持。

FEDERATED 的优点是使用体验跟本地表一样,JOIN、WHERE、ORDER BY都能写;缺点是性能真的很一般,因为条件过滤基本是在远端做完再拉回来,索引优化很有限,数据量一上来就慢得没法看。而且它不支持事务,网络一抖就报错。我的建议是:只用来做小数据量的配置同步、栏目映射这类查询,别拿它跑列表页。

3.3 应用层双连接:在 dede 里自己写一层取数逻辑

这是我个人最推荐的方式,尤其是两套系统需要长期共存的时候。核心思路很简单:dede 自己的$dsql管本地数据,另外单独建一个连接对象管远程数据,两边互不干扰。

好处是可控性最强,可以加缓存、可以降级、可以做超时控制,出问题也不影响主站的正常运行。下面几种方案怎么选,我做了个对比:

方案实时性性能实现成本适用场景
同实例跨库 SQL实时高极低两库在同一实例,长期共存
FEDERATED实时低低小数据量、临时性查询
应用层双连接实时中(可加缓存)中两库不同实例,需要精细控制
定时同步 / 接口中转有延迟高中高数据量大、实时性要求低

3.4 定时同步与接口中转:数据量大、实时性要求低时的选择

如果两个库之间的数据是"看个大概"就行,比如子站首页展示主站热榜,滞后十分钟完全无所谓,那我更倾向于同步成一张本地表,而不是每次请求都去远程查。

具体做法是建一张结构精简的本地表(只存 id、title、url、封面图、时间这几个字段),然后写个 PHP 脚本挂在计划任务里,每五分钟从远程库拉一次最新数据,全量清空再写入。前台查询完全走本地,速度跟普通查询没区别,远程库挂了也只是数据不更新,页面照样能打开。

<?php // 精简版:拉取远程栏目最新文章写入本地缓存表 $remote = new mysqli('10.0.3.10', 'reader', '密码', 'site_main'); if ($remote->connect_errno) { exit('远程库暂不可用,本次跳过'); } $remote->set_charset('utf8'); $sql = "SELECT id, title, litpic, senddate FROM dede_archives WHERE typeid = 12 AND arcrank >= 0 ORDER BY senddate DESC LIMIT 20"; $res = $remote->query($sql); $local = new mysqli('127.0.0.1', 'site_sub_rw', '密码', 'site_sub'); $local->set_charset('utf8'); $local->query('TRUNCATE TABLE dede_hot_cache'); $stmt = $local->prepare( 'INSERT INTO dede_hot_cache (aid, title, litpic, senddate) VALUES (?,?,?,?)' ); while ($row = $res->fetch_assoc()) { $stmt->bind_param('issi', $row['id'], $row['title'], $row['litpic'], $row['senddate']); $stmt->execute(); } $stmt->close(); $remote->close(); $local->close();

这个脚本里我特意做了一件事:远程连不上就exit,不做任何兜底写入。原因是如果远程挂了而脚本继续跑,TRUNCATE已经把本地表清空,后面插入零条,前台就变成空白,反而更难看。让脚本直接退出,本地表保持上一次的旧数据,前台显示的还是旧内容,体验上更平滑。这是踩过一次坑之后改的——当时远程库重启了三十秒,正好撞上同步任务,子站首页空了一块,被用户截图反馈上来才发现。

4. dede 三表结构决定了跨库查询必须成套取

前面讲的是"怎么连",这一节讲"写 SQL 的时候要注意什么"。dede 的表结构跟一般 CMS 不太一样,它的文章数据是分散在三张表里的,很多人跨库查询写完发现列表出来了但正文是空的、或者封面图不显示,基本都是因为只查了其中一张。

4.1 archives / arctiny / addonarticle 的联动关系

先看这三张表各自存什么:

表名存什么关键字段行数级别
dede_archives文章主表,标题、栏目、属性、时间id, typeid, title, arcrank, senddate全量
dede_arctiny轻量表,只存索引需要的字段id, typeid, arcrank, sortrank全量
dede_addonarticle附加表,正文aid, body, userip全量(仅文章模型)

三张表通过主键关联:dede_archives.id = dede_arctiny.id = dede_addonarticle.aid。

dede 拆成三张表是有历史原因的:dede_arctiny字段少、行短,查列表的时候只扫它,速度快;dede_archives存标题、属性这些列表和详情都要用的字段;dede_addonarticle单独放正文这种大字段,避免扫列表的时候把几百 KB 的正文一起读进内存。这个设计在十年前是很聪明的,放到今天也不算落伍。

问题在于,跨库查询的时候你不一定知道这个拆分逻辑。我见过有人写:

SELECT id, title, body FROM dede_archives WHERE typeid = 12;

跑完报Unknown column 'body',然后就去远程库翻表结构,翻了半天发现正文不在主表里,才意识到有两张表。

4.2 只查主表会缺正文,只查副表会丢状态

正确的写法是带LEFT JOIN:

SELECT a.id, a.title, a.litpic, a.senddate, a.description, t.body FROM dede_archives a LEFT JOIN dede_addonarticle t ON t.aid = a.id WHERE a.typeid = 12 AND a.arcrank >= 0 ORDER BY a.senddate DESC LIMIT 10;

用LEFT JOIN而不是INNER JOIN,是因为有些文章模型不是"普通文章"(比如图集、软件、专题),它们的数据在各自的附加表里,dede_addonarticle里没有对应记录。用INNER JOIN这些文章会被直接过滤掉,列表就少了几条,而且你还不容易发现。

还有arcrank这个字段,是 dede 的审核状态,-1表示待审核(前台不可见),0及以上表示已审核。跨库调用的时候一定要带上arcrank >= 0这个条件,否则待审核的文章会被拉出来展示——这在本地查询里一般不会漏,因为大家都习惯从{dede:arclist}标签里抄条件;但自己手写 SQL 跨库查的时候特别容易忘。

另外senddate是发布时间戳,sortrank是排序权重,两者不一样。前台列表的默认排序用的是sortrank DESC(带置顶效果),不是senddate DESC。如果你希望跨库调出来的顺序跟站内一致,得用sortrank;如果只想要"最新的",才用senddate。

4.3 栏目表与缓存:跨库后栏目统计对不上

dede_arctype是栏目表,里面除了栏目名、目录名,还有几个统计字段:postnum(栏目文章数)、typedir(栏目路径)、isdefault(是否默认模板)、temparticle(模板文件名)。

跨库调数据的时候,如果你要生成正确的链接,就得从这张表里拿typedir拼路径,否则拼出来的 URL 是错的。dede 的栏目 URL 规则比较绕,有"动态"和"静态"两种模式,静态模式下是typedir/index.html,列表第二页是typedir/list_2.html这种。自己拼很容易拼错,稳妥的做法是把typedir和namerule一起取出来,或者干脆用{dede:arclist}标签在本地栏目上下文里生成链接。

还有一个坑:dede 会把栏目信息缓存成文件,位置在data/cache/下面。你在远程库改了一个栏目名,本地这边可能还是旧的,因为走的不是数据库而是缓存文件。跨库场景下这个更容易出问题,因为两边缓存各更新各的。遇到栏目信息对不上,先去后台点一次"更新栏目缓存",或者直接删掉data/cache/下的缓存文件让它重建,别在那怀疑数据库。

5. 一个可落地的跨库调用封装与模板接入

方案讲完了,落到代码上。我不建议在每个需要的地方都new mysqli(),那样重复代码太多、改起来也麻烦。封装成一个取数函数,带个简单的文件缓存,是最实用的做法。

5.1 封装一个轻量取数函数

新建一个文件,比如include/remote_data.php,内容大致如下:

<?php /** * 从远程 dede 库读取文章列表 * @param int $typeid 栏目 ID * @param int $limit 条数 * @param string $order 排序字段 * @return array */ function get_remote_archives($typeid, $limit = 10, $order = 'senddate') { // 白名单,防止排序字段拼接注入 $allowOrder = array('senddate', 'sortrank', 'click', 'id'); if (!in_array($order, $allowOrder, true)) { $order = 'senddate'; } $limit = (int)$limit; if ($limit <= 0 || $limit > 50) { $limit = 10; } // 文件缓存 300 秒 $cacheKey = 'remote_' . $typeid . '_' . $limit . '_' . $order; $cacheFile = DEDEINC . '/../data/cache/' . md5($cacheKey) . '.php'; if (is_file($cacheFile) && (time() - filemtime($cacheFile)) < 300) { $data = @include $cacheFile; if (is_array($data)) { return $data; } } $conn = @new mysqli('10.0.3.10', 'reader', '密码', 'site_main', 3306); if ($conn->connect_errno) { // 连不上就返回上次缓存,不抛错 return is_file($cacheFile) ? (array)@include $cacheFile : array(); } $conn->set_charset('utf8'); $sql = "SELECT a.id, a.title, a.litpic, a.senddate, a.description FROM dede_archives a WHERE a.typeid = ? AND a.arcrank >= 0 ORDER BY a.{$order} DESC LIMIT {$limit}"; $stmt = $conn->prepare($sql); $stmt->bind_param('i', $typeid); $stmt->execute(); $res = $stmt->get_result(); $list = array(); while ($row = $res->fetch_assoc()) { $list[] = $row; } $stmt->close(); $conn->close(); // 写缓存 $content = '<?php return ' . var_export($list, true) . ';'; @file_put_contents($cacheFile, $content); return $list; }

这段代码里有几个刻意的设计,值得说一下理由。

排序字段用白名单校验。ORDER BY后面的字段名没法用预处理参数绑定,只能字符串拼接。如果直接拼用户传进来的值,那就是一个明晃晃的注入点。白名单是最简单也最可靠的解法。

数据库连不上时返回旧缓存,而不是报错。这个前面提过,跨机器调用必然面临网络抖动,首页不能因为远程库抖一下就白屏。让缓存兜底,用户体验上就是"数据旧了点",比"页面打不开"好太多。

缓存写文件而不是写数据库。因为写数据库本身也要走远程连接,远程挂了缓存也写不了,等于没兜底。写本地文件才是真正解耦的。

用了reader这种只读账号。这个函数只需要读,给它读写权限毫无必要。远程库的账号权限越小,出问题的爆炸半径就越小。

5.2 模板里怎么调用,缓存怎么处理

dede 的模板是自带一套标签解析器的,要在模板里调自定义函数,需要用{dede:php}标签,或者提前在控制器里把数据准备好、赋给模板变量。

{dede:php}的写法大概是这样:

{dede:php} require_once DEDEINC.'/remote_data.php'; $hot = get_remote_archives(12, 8, 'senddate'); foreach ($hot as $v) { $url = '/plus/view.php?aid='.$v['id']; echo '<li><a href="'.$url.'">'.htmlspecialchars($v['title']).'</a></li>'; } {/dede:php}

{dede:php}这个标签在 dede 后台默认是关闭的,需要在"系统 - 系统基本参数 - 其它选项"里把"模板引擎禁用PHP"之类的开关放开,或者直接改dede_sysconfig表里的cfg_phpurl相关配置。放不放开是个安全权衡:放开了模板里能写 PHP,灵活但危险;不放开就只能在 PHP 文件里预先准备好数据。我倾向于在模板里不放 PHP,把逻辑写在 PHP 文件里,模板只负责渲染,这样模板改动不会引发安全问题。

缓存这块要多说一句。dede 自己有模板缓存机制,编译后的模板放在data/tplcache/,但那只缓存模板结构,不缓存查询结果。所以跨库查询的缓存得自己管,上面那个函数用的是"文件 + 过期时间",简单够用。如果站点访问量大,可以考虑把结果写进 APCu 或者 Redis,但不要为了用 Redis 而用 Redis——几百 IP 的小站,文件缓存的性能完全够,还省了一套依赖。

5.3 分页与 count 的处理

列表页涉及分页的时候,跨库查询会比本地麻烦一点,因为要算总数。

本地查询的时候,dede 的{dede:pagelist}标签会自动处理 count;但自己写的跨库 SQL 没有这层封装,得手动来:

// 先算总数 $countSql = "SELECT COUNT(*) AS c FROM dede_archives WHERE typeid = ? AND arcrank >= 0"; $stmt = $conn->prepare($countSql); $stmt->bind_param('i', $typeid); $stmt->execute(); $total = $stmt->get_result()->fetch_assoc()['c']; // 再取当页 $offset = ($page - 1) * $pagesize; $listSql = "SELECT id, title, senddate FROM dede_archives WHERE typeid = ? AND arcrank >= 0 ORDER BY senddate DESC LIMIT ?, ?";

注意LIMIT后面两个参数在预处理里都得绑成整数,而且不能加引号,加了会变成字符串导致语法错误。这是 mysqli 预处理里最经典的坑之一,报错信息是You have an error in your SQL syntax near ''0', '20'',看到这个基本就是引号问题。

分页还有个体验上的细节:COUNT(*)在数据量大的表上很慢,因为它要扫索引。如果文章数以万计,可以考虑缓存总数(比如缓存 10 分钟),或者用"只显示最近 N 页"的方式避开精确 count。dede 自己的列表页其实也有这个问题,只是大家习惯了。

6. 出问题时的排查顺序:从错误码倒推

分离部署之后出问题,最忌讳的是东改一下西改一下。我的习惯是按错误码走,因为 MySQL 和 PHP 的报错信息虽然难看,但错误码本身指向很明确。

6.1 2003 / 1045 / 1130 分别指向什么

这三个错误码是连接阶段最常见的,含义完全不同,处理方向也完全不同:

错误码报错原文关键词指向的问题处理方向
2003Can't connect to MySQL server网络层不通查监听地址、防火墙、端口
1045Access denied for user认证失败查密码、查用户是否存在
1130Host is not allowed to connect来源 IP 未授权补@IP的授权记录
1040Too many connections连接数打满查max_connections、查是否有连接泄漏
2006MySQL server has gone away连接被服务端断开查wait_timeout、max_allowed_packet

1130这个错误特别容易被误判成1045,因为两者都跟"登录不上"有关,但1130明确说的是"这台主机不允许连接",说明请求已经到了 MySQL,只是授权表里没这条记录,密码对不对都还没轮到验证。看到1130不用去改密码,直接去补授权。

1040的表现比较特殊,不一定是连接阶段报,可能是网站跑着跑着突然全部报错,刷新几次又好了。这通常是并发高峰把连接数打满了,去数据库上看一下SHOW PROCESSLIST,如果一屏都是Sleep状态的连接,说明有连接没被正确释放,重点查有没有长连接没关,或者某个脚本卡住了不退出。

6.2 页面白屏但数据库日志正常的情况

有一种情况最让人抓狂:数据库那边日志干干净净,连接数正常,慢查询也没有,但网站就是白屏,什么报错都不给。

第一反应应该是打开 PHP 的错误显示。dede 的入口文件一般会关掉display_errors,生产环境这么做没错,但排查的时候必须临时打开。在站点根目录放一个调试入口:

<?php ini_set('display_errors', '1'); error_reporting(E_ALL); require './index.php';

如果这样能打出报错,那就好办了。如果还是白屏,说明问题出在更早的阶段,比如data/common.inc.php有语法错误(少个分号、多个引号),PHP 解析阶段就挂了,根本走不到index.php。这时候直接用php -l data/common.inc.php做语法检查,比肉眼找快一百倍。

还有一种白屏是权限问题。data/目录下的缓存目录如果不可写,dede 在生成缓存的时候会静默失败,然后页面就空了。检查一下data/cache/、data/tplcache/、data/sessions/这几个目录的属主和权限,换成 Web 运行用户可写。这个坑在迁移服务器的时候特别高频,因为马的属主跟新机器上的 Web 用户不是同一个。

6.3 慢查询与索引缺失

连接都正常,但页面就是慢,那就要看慢查询了。在数据库服务器上开一下慢查询日志:

[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 2

跑一段时间后看日志,重点找扫描行数大的查询。

dede 的核心表索引其实建得还行,dede_archives上有typeid、senddate、arcrank的联合索引(不同版本略有差异),但跨库查询里的ORDER BY经常用不上索引。比如你要按click(点击数)排序,那基本就是全表扫描加文件排序,数据量上万就明显卡了。

遇到这种情况,要么在远程库上补一个合适的索引(typeid, click这样的组合索引),要么换个思路——把热门数据同步到本地表(前面 3.4 讲的方案),本地查怎么都快。

提示:跨库查询尽量别写SELECT *。远程传输的数据量直接影响响应时间,字段越少越快。dede 的dede_addonarticle.body动辄几十 KB,列表页千万别把它带出来。

7. 权限、备份与长期运维的几条经验

连接打通、查询跑顺了,剩下的就是长期维护。这部分没什么技术含量,但一旦疏忽,后面补起来很麻烦。

7.1 只读账号与最小权限

跨库读取场景,我坚持用独立的只读账号,理由有三个。

第一是安全边界。读取账号就算泄露,最坏结果是数据被拖走,而不是被篡改或者删库。第二是误操作防护,你在写 SQL 的时候手滑写了UPDATE,只读账号会直接报错,等于给自己加了一层保险。第三是便于审计,在数据库的performance_schema或者审计日志里,读请求和写请求分得清清楚楚,出问题好定位。

创建只读账号的语句:

CREATE USER 'reader'@'10.0.3.15' IDENTIFIED BY '独立的强密码'; GRANT SELECT ON `site_main`.* TO 'reader'@'10.0.3.15'; FLUSH PRIVILEGES;

注意这里的密码要跟站点自己用的账号完全独立,别图省事共用一个。两个账号的用途、权限、生命周期都不一样,共用会让后续的权限调整变得很别扭。

7.2 双机之后备份策略必须改

这个是最容易被忽略的。单机的时候,很多人依赖主机商提供的整机快照,或者后台"一键备份";拆开之后,网站文件和数据库分在两台机器上,快照就不是原子的了——你恢复的时候可能拿到的是昨天的网站文件配上前天的数据库,数据对不上。

我的做法是:

备份对象位置频率方式
网站程序文件Web 服务器每周打包压缩后传到独立存储
数据库全量数据库服务器每天mysqldump导出后压缩
数据库增量数据库服务器每小时binlog 保留 7 天

关键在于备份文件不要留在数据库服务器本机。机器一旦挂掉,备份跟着一起没。导出来之后传到另一台机器或者对象存储里去,这一步千万别省。

dede 后台自带的"数据库备份"功能在分离部署下要谨慎用,因为它是 PHP 逐条 SELECT 再拼接 SQL 语句导出的,走的是远程连接,几万条数据能跑很久,中途超时就会得到一个残缺的备份文件,而且它不会告诉你文件是残缺的。我更推荐直接用mysqldump:

mysqldump -h 127.0.0.1 -u backup_user -p \ --single-transaction --quick --default-character-set=utf8 \ site_main | gzip > /backup/site_main_$(date +%F).sql.gz

在数据库服务器本机上执行,走的是回环地址,速度最快。--single-transaction保证 InnoDB 表的一致性快照,不会锁表;--quick让它在导出大表的时候逐行取,不会把整个结果集塞进内存。

还有一点,备份完一定要验证。我见过太多"备份文件大小正常但还原报错"的情况。最省事的验证方式是每周找一台测试机还原一次,跑一下CHECK TABLE,或者至少zcat 备份文件 | head -100看一眼开头是不是正常的 SQL 语句。备份这件事,只有验证过的才叫备份,没验证过的只能叫"文件"。

最后分享一个我在实际操作中的体会:分离部署最大的坑其实不在技术本身,而在"两边状态不一致"。本地数据库的时候,改个字段、加个索引、清个缓存,都是瞬时完成的;拆开之后,每一件事都要在两边确认一遍,改完配置忘了重启、授权加了忘了 flush、缓存清了忘了另一台也有缓存,这些"忘了一步"才是日常故障的主要来源。我的习惯是把分离部署涉及的所有操作列成一张检查表,每次动完一项就打勾,虽然土,但真的很管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询