[开发]Ofstar 2.6.0 核心架构审阅报告(文本论坛时代巅峰之作)

👑Lv.11 元老 🌞 骨干会员
2026-10-24 00:15:46

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:暂存20260913/docs/Ofstar2.6架构审阅与SplitDB借鉴报告 (2).md。

Ofstar 2.6.0 核心架构审阅报告(文本论坛时代巅峰之作)

用途:审阅 Ofstar 2.6 的文件存储与索引机制,提炼可被 SplitDB(PHP + 纯 SQLite 分片引擎)吸收的设计思路。

阅读对象:ofstar2.6.0/ 全部核心代码(global.php / require/ / bbsdata/ / read.php / thread.php / index.php 等),GBK 编码转 UTF-8 逐行核对。

结论先行:Ofstar 的「秒载入」来自三件事——文件名即索引(O(1) 定位)、定长记录 + fseek 字节寻址(零扫描取最新)、首页只拼装若干 KB 级缓存快照(不碰大文件)。这三条对 SplitDB 分别对应「topic_index/bucket_path 定位」「extern .idx 偏移索引(已实现同源思路)」「列表页静态缓存 + 统计缓存(已实现并更强)」。


一、数据存储结构:一实体一文件 + 行内定长/分隔

1.1 整体布局

ofstar2.6.0/
├── global.php                 # 核心函数库(20 个函数):读/写/锁/路径安全
├── bbsdata/                   # ★ 文本"数据库"目录(版块/在线/统计等元数据)
│   ├── config.php             # 全局配置($db_linesize=70、$db_topnum=10、$dbpath 等)
│   ├── forumdata.php          # 版块定义表(多行,每行一条版块记录)
│   ├── bbsatc.php             # 站点统计缓存(帖子数/会员数/今日帖数等,| 分隔)
│   ├── bbsnew.php             # 最新发帖索引(追加式)
│   ├── bbsonline.php          # 在线统计缓存
│   ├── online.php / guest.php # 在线会员表 / 游客表(定长记录,$db_olsize)
│   ├── cache.php              # 用户组/管理员名单缓存
│   ├── postcache.php          # 表情/动作静态数组缓存
│   └── olcache.php            # 在线人数 PHP 变量缓存
├── ofstar_data/forumdir/      # 帖子真相源:{fid}/{tid}.php + {fid}/list.php + {fid}/status.php
├── userdata/                  # 用户:{username}.php(一用户一文件)+ groupdb/group_{gid}.php
├── require/                   # userglobal.php / forum.php / postnew.php / postreply.php 等
└── read.php / thread.php / index.php  # 读取入口

1.2 三种存储形态(按访问模式分工)

形态典型文件组织方式适用
一帖一文件forumdir/{fid}/{tid}.php首行 = 主帖字段(`\` 分隔),后续每行 = 一条回帖(同格式)
定长追加表forumdir/{fid}/list.php每条 str_pad(记录, $db_linesize=70) . "\n",只追加不修改版块列表:按字节偏移倒读最新 N 条
行式 KV 缓存bbsdata/*.php每行一条记录,<?die;?> 头防直接访问,`\` 分隔

关键细节:

  • 安全头:所有数据文件以 <?die;?> 开头——直接 HTTP 访问时 PHP 执行 die() 拒绝输出,防数据文件被明文拉取(openfile() 读取后 explode 时把该头当作第一条记录的首字段,字段值 die;?> 无实际意义)。
  • 分片思想(雏形):帖子按 fid(版块)分目录、按 tid(主题)分文件——这就是「按 ID 哈希分片」的原型;SplitDB 的 bucket ID%32 + 季度 是其加强版。
  • 外置正文(原型):帖子正文不在任何数据库里,就在文件里——与 SplitDB 的 extern/ 外置正文完全同源,Ofstar 是「正文即文件」,SplitDB 是「正文外置 + 索引定位」。

二、索引查找机制:文件名即索引 + 字节寻址

2.1 帖子定位 = 路径拼接(O(1))

read.php 第一件事:

if (!file_exists("$dbpath/$fid/$tid.php")) { include('require/url_error.php'); }
$articlearray = openfile("$dbpath/$fid/$tid.php");   // 整文件按行读入
$topic_detail = explode("|", $articlearray[0]);        // 主帖字段

没有索引表、没有二分查找——URL 里的 fid/tid 直接拼成文件路径。这就是 O(1)。

2.2 版块列表 = 定长字节寻址(零扫描取最新)

list.php 每条记录定长 70 字节 + 换行(71 字节/条),因此:

  • 帖子总数:floor(filesize(list.php) / 71) —— shownextname() 用这个公式分配新 tid(O(1) 自增,遇文件占用再 +1)。
  • 最新 N 条:thread.php 从文件末尾按定长步进倒读:
$offset = -($db_linesize + 1) * $linestart;
fseek($fp, $offset, SEEK_END);          // 从尾部往前跳

  • "本版最新帖"(getnewstatus()):只需 fseek($fp, -($db_linesize+1), SEEK_END) 读最后一行的 tid 字段——首页/版块页显示"最新帖"不需要任何扫描。
  • 置顶/精华(searchtop()):从文件头正读前 $db_topnum(=10)条定长记录。

2.3 浏览量自增 = 定长槽位原地改写(不重写整文件)

if ($fp = fopen("$dbpath/$fid/$tid.php", "rb+")) {
    flock($fp, LOCK_EX);
    fseek($fp, 0);
    fputs($fp, $hitwrite);    // 主帖首行定长槽位直接覆盖
    fclose($fp);
}

主帖首行的"浏览量"字段占据固定字节槽位,自增 = 打开文件 + 锁 + 跳 0 偏移 + 覆盖前 8 字节,读正文时顺带完成,无整文件重写。

2.4 对标 SplitDB

OfstarSplitDB 对应结论
{fid}/{tid}.php 路径定位topic_index.bucket_path + 桶内主键SplitDB 多一次索引行读,但支持改桶/迁季度;O(1) 等价
list.php 定长倒读idx_topic_index_list 复合索引 ORDER BY + LIMITSQLite 索引已等价实现,且免手工字节计算
getnewstatus() 尾行读取首页前置缓存 / 版块缓存已被 PageCache 永久缓存超越(整页静态 HTML)
extern .txt 正文extern .bin/.idx(4 字节大端长度 + fseek 偏移)同源思路,SplitDB 已升级为偏移索引

三、读写并发:单文件锁 + 短持锁 + 追加不重写

3.1 锁原语(global.php)

// 读:共享锁,整文件读入
function readover($filename) {
    $handle = fopen($filename, "rb");
    flock($handle, LOCK_SH);                 // 多读并发不互斥
    $filedata = fread($handle, filesize($filename));
    fclose($handle);                         // 读完立即释放
    return $filedata;
}
// 写:独占锁,整文件覆写
function writeover($filename, $data) {
    touch($filename);
    $handle = fopen($filename, "rb+");
    flock($handle, LOCK_EX);
    fputs($handle, $data);
    if (strlen($data) < filesize($filename)) ftruncate($handle, strlen($data));
    fclose($handle);
}

全站 45 处 flock:读用 LOCK_SH(25 处 rb)、写用 LOCK_EX(9 处 rb+)。

3.2 避免高并发阻塞的三板斧

  1. 锁粒度 = 单文件:一帖一文件 → 不同帖子的读写互不竞争;锁冲突面收敛到「同一帖子」这一最小单元(SplitDB 的 SQLite WAL 是页级锁 + 连接池,冲突面更大但被 WAL 吸收)。
  2. 短持锁:读锁读完即放;写锁配合定长槽位 fseek 定点改写(浏览量、在线表),把独占锁的持有时长压到微秒级。
  3. 追加写不重写:list.php 用 writeover(..., "ab") 追加模式,写新帖只往文件尾部加 71 字节,不动已有内容(SplitDB 对应 WAL 追加 + 桶内 INSERT)。

3.3 在线表的"频度可控整理"(ModifySelectFile)

在线表是定长记录($db_olsize),新在线用户写入时:

  • GetInsertOffset():fseek 块读扫描空槽位(str_pad(" ", $db_olsize) 即空位)复用;
  • ModifySelectFile():从文件尾倒扫最多 20000 步,把过期记录整行置空(fputs($addnbsp)),并不重写整个文件,同时把在线人数写进 olcache.php 缓存。

注释明确写着"频度可控制"——这是延迟整理 + 写放大量化上限的思路:即使 5 万用户,单次整理的扫描步数也被 20000 封顶。

四、缓存与优化:把"读"降级为"拼装几个小缓存文件"

这是 Ofstar「秒载入」的真正核心。首页 index.php 做的事情,全部是读 KB 级缓存文件然后拼 HTML:

list($forumcount, $forumarray) = getforumdb();        // forumdata.php(102B)
$alter = explode("|", readover("$dbpath/$forum[fid]/status.php")); // 每版块最新帖快照
list($bbsfb, $bbsnewer, $bbstotleuser) = explode("|", readover("bbsdata/bbsnew.php"));
list($bbsfb, $bbsol, $bbsoltime)        = explode("|", readover("bbsdata/bbsonline.php"));
list($bbsfb, ... 12 个统计字段)          = explode("|", readover("bbsdata/bbsatc.php"));
include_once('bbsdata/olcache.php');                  // PHP 变量缓存($userinbbs=0)

缓存手段机制SplitDB 对应
PHP 变量缓存(olcache.php)<?php $userinbbs=0; $guestinbbs=0; ?> 用 include 直接取数Settings runtime 缓存 / online_data.cache.json(更强:JSON + TTL)
统计快照文件(bbsatc.php)发帖/回帖时更新 12 项统计,首页读一次_runtime_* KV 缓存(已等价)
版块最新帖快照({fid}/status.php)每版块单独存最新帖 tid/标题/时间,首页/版块页零扫描未完全对应:SplitDB 版块列表仍查 topic_index(但有 idx 与 PageCache)
新帖索引(bbsnew.php)追加式记录,首页读尾PageCache 永久缓存(更强:整页静态 HTML)
表情/动作数组(postcache.php)纯 PHP 数组文件TemplateCompiler icon() 静态映射(等价)

「秒载入」的机制总结:请求 → 读 6 个 ≤2KB 的缓存文件 → explode 拆字段 → 拼 HTML。全程没有一次大文件读取、没有一次全表扫描——这是文本论坛时代最极致的"读缓存不读数据"。

五、可借鉴的灵感(对 SplitDB 的具体建议)

✅ 5.1 已同源、可确认无需改动

  1. extern 偏移索引(Ofstar 的定长槽位 → SplitDB 的 .bin/.idx 4 字节长度头 + fseek)——同源思路,SplitDB 已更强。
  2. 正文外置(Ofstar 正文即文件 → SplitDB extern 剥离 SQLite)——同源,已实现。
  3. 统计缓存(bbsatc.php → _runtime_* + category_stats 永久缓存)——已等价且更强(发帖/回帖主动失效)。
  4. 列表页静态缓存(Ofstar 拼缓存 → PageCache 整页 HTML + 前置缓存)——已超越。

💡 5.2 值得吸收的三个方向

① 版块级「最新帖快照」(最值得借鉴)

Ofstar 每个版块有 {fid}/status.php 独立快照,发帖/回帖时只更新该版块自己的快照文件,首页/版块页拼装时 O(版块数) 完成"每个版块最新帖"展示,不触任何大表。

→ SplitDB 现状:版块页/首页的"最新帖"仍实时查 topic_index(有索引但仍是查询)。可加一张 latest_by_category 小表(category_id 主键 + tid/标题/时间/作者),发帖/回帖时 UPSERT,展示时一次 SELECT * FROM latest_by_category(≤7 行)拼装。与 PageCache 配合后,冷渲染进一步下降。

② 「定长槽位 + 原地改写」的思想移植到计数列

Ofstar 浏览量自增 = 打开文件锁住 → fseek → 覆盖 8 字节。它不重建索引、不移动数据。

→ SplitDB 现状:incrementView() 是 UPDATE topic_index SET view_count = view_count + 1 WHERE id = ?,有 WAL 但每次 UPDATE 走索引。可借鉴的是 延迟合并:浏览量/回帖数走 Settings::runtimeIncr()(P24 已有"请求结束时批量写回"的计数器机制)——这正是 Ofstar"读正文顺带改首行"的 SQLite 版,方向已对,可确认扩展覆盖面。

③ 在线表"频度可控整理"的量化上限

Ofstar ModifySelectFile 明确用 $step < 20000 封顶单次整理量,防止一次请求卡死。

→ SplitDB 现状:updateOnlineStatus() 每请求 DELETE FROM online_users WHERE last_activity < 阈值,行数小时无感,但没有硬上限。若未来在线表膨胀,可给 DELETE 加 LIMIT(如每次最多清 5000 行,配合多请求分摊)——把"单次写放大"钉死在量化边界内。

⚠️ 5.3 明确不需要借鉴的(Ofstar 的妥协在 SplitDB 已解决)

  • <?die;?> 防直接访问 → SQLite 二进制 + 路径不可猜,已解决。
  • 整文件读入再分页(readover 读整个 {tid}.php,千楼帖一次读全文件)→ SplitDB 桶内 LIMIT 分页,已解决。
  • 无事务、崩溃一致性靠"追加+覆盖原子性" → SQLite WAL + 事务,已解决。
  • 锁粒度=文件导致"写放大=整文件覆写"(writeover 对非定长文件)→ SplitDB 行级更新 + WAL 追加,已解决。

六、结论

Ofstar 2.6 以极小的代码量(核心 ~100KB),用「一实体一文件 + 定长字节寻址 + 缓存快照拼装」三件套做到了秒载入,其本质是把"查数据"降级为"拼缓存"、把"定位"降级为"路径拼接/字节偏移"。

SplitDB 的分片 + 索引 + 缓存架构在数据规模与一致性上全面超越它,但 Ofstar 留下了两个仍有增量价值的朴素思想:

  1. 版块级最新帖快照(发帖只更新小快照,列表零查询大表);
  2. 写放大硬上限(单次整理/清理设量化封顶,防偶发卡顿)。

这两条都是"小改动、纯收益",可直接落地到现有 SplitDB 引擎。


审阅完成。代码基线:ofstar2.6.0/(GBK 源码转 UTF-8 逐行核对),核心证据:global.php 的 readover/writeover/openfile、require/postnew.php 的写入路径、require/userglobal.php 的 GetInsertOffset/ModifySelectFile、read.php 的定位与浏览量自增、thread.php/index.php 的定长倒读与缓存拼装。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 4 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录