此文章为XIUNOX版本重构审计时发现问题,XIUNOX版本已优化修复此问题。分享出来方便后续想基于xiuno bbs4.0.4版本制作维护版本或插件模板等需求的开发者和站长参考。
现象
Xiuno BBS 4.0.4 在 db_mysql 类中提供了主从连接能力(master + slaves),但默认配置 'slaves' => array() 为空,从库连接回退到主库连接($this->rlink = $this->wlink)。业务层调用 db_find_one/db_find/db_count/db_maxid 等读操作时,虽然 db_mysql::find_one 内部会调用 connect_slave() 获取读连接,但由于 slaves 为空,所有读请求最终都打到主库。系统未强制要求生产环境配置 slaves,也未提供读请求路由到从库的明确策略,导致"支持主从"形同虚设。
源码证据
1. conf.default.php slaves 默认为空数组
文件:xiunobbs_4.0.4/conf/conf.default.php 第 12-37 行
'db' => array (
'type' => 'mysql',
'mysql' => array (
'master' => array (
'host' => 'localhost',
'user' => 'root',
'password' => 'root',
'name' => 'test',
'tablepre' => 'bbs_',
'charset' => 'utf8',
'engine' => 'innodb',
),
'slaves' => array (),
),
'pdo_mysql' => array (
'master' => array (
),
'slaves' => array (),
),
),
2. db_mysql::connect_slave 在 slaves 为空时回退到 master 连接
文件:xiunobbs_4.0.4/xiunophp/db_mysql.class.php 第 36-50 行
public function connect_slave() {
if($this->rlink) return $this->rlink;
if(empty($this->conf['slaves'])) {
if($this->wlink === NULL) $this->wlink = $this->connect_master();
$this->rlink = $this->wlink;
$this->rconf = $this->conf['master'];
} else {
$n = array_rand($this->conf['slaves']);
$conf = $this->conf['slaves'][$n];
$this->rconf = $conf;
$this->rlink = $this->real_connect($conf['host'], $conf['user'], $conf['password'], $conf['name'], $conf['charset'], $conf['engine']);
}
return $this->rlink;
}
3. db.func.php 读函数虽传 d参数,但默认NULL回退到_SERVER['db'] 主连接
文件:xiunobbs_4.0.4/xiunophp/db.func.php 第 60-99 行
function db_sql_find($sql, $key = NULL, $d = NULL) {
$db = $_SERVER['db'];
$d = $d ? $d : $db;
if(!$d) return FALSE;
$arr = $d->sql_find($sql, $key);
db_errno_errstr($arr, $d, $sql);
return $arr;
}
function db_count($table, $cond = array(), $d = NULL) {
$db = $_SERVER['db'];
$d = $d ? $d : $db;
if(!$d) return FALSE;
$r = $d->count($d->tablepre.$table, $cond);
db_errno_errstr($r, $d);
return $r;
}
业务调用几乎从不传 $d 参数:
文件:xiunobbs_4.0.4/model/user.func.php 第 27 行
function user__read($uid) {
$user = db_find_one('user', array('uid'=>$uid));
return $user;
}
全站 db_find_one、db_find、db_count、db_maxid 调用均不显式传从库连接,依赖 db_mysql::find_one 内部调用 connect_slave()。但 connect_slave() 在 slaves 为空时返回主连接,读请求实际打到主库。
4. db_mysql 类内部 find_one 调用 connect_slave 但被 slaves 空配置抵消
文件:xiunobbs_4.0.4/xiunophp/db_mysql.class.php(类内部 find_one 实现,Grep 命中第 1 行 trigger_error)
读方法虽调用 $this->connect_slave() 获取读连接,但 slaves 配置为空时 connect_slave() 返回 $this->wlink(主连接),读写分离失效。
5. 业务层无强制读写分离策略
Grep 全站无 connect_slave 显式调用,无 db_find_one(..., $slave_db) 传从库连接的代码模式。读写分离完全依赖 db_mysql 类内部隐式行为,业务无感知。
6. 写后立即读可能读到从库延迟数据
即使配置了 slaves,db_find_one 读从库但 db_insert/db_update 写主库,写后立即读(如注册后登录)可能因主从延迟读到旧数据,系统未提供"读主"开关。
风险等级与结论
架构缺陷(中)
危害:
- 默认配置 slaves 为空,所有读请求压主库,主库成为性能瓶颈
- 业务层无读写分离意识,无法显式指定从库读或强制读主
- "支持主从"为伪能力,需手工修改 conf.php 才能启用,文档未强制要求
- 写后立即读无"读主"开关,主从延迟下数据不一致
- 从库连接随机挑选(
array_rand),无健康检查、无故障转移
- 从库不可用时无降级策略(连接失败直接报错)
修复建议:
- 默认配置示例改为提供 slaves 占位项(注释说明),引导生产部署配置从库
- 业务层引入
db_read_one()(强制从库)/ db_read_one_master()(强制主库)双接口
- 写后读场景(注册后登录)自动路由到主库(基于 cookie/标记)
- 从库连接增加健康检查与故障转移,从库不可用时降级到主库并告警
- 配置层支持多从库权重(
weight 字段),按权重负载均衡