本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:暂存20260913/docs/改造执行计划 (2).md。
SoSite 1.0(SplitDB 引擎迁移)— 改造执行计划
文档状态:定稿(2026-08-13,经用户确认)
基线版本:SoSite 5.0(MySQL)→ SoSite 1.0(SplitDB 纯 SQLite 分片引擎)
依据:《SplitDB v2.3 白皮书》(根目录
Splitdb V2.3 白皮书.txt)兼容基线:PHP 8.0.2(phpstudy
D:\phpstudy_pro\Extensions\php\php8.0.2nts,内置 SQLite 3.33.0)编码规范:全部生成文件 UTF-8 without BOM + LF
一、角色与目标
- 角色:资深 PHP 架构师与重构专家
- 目标:将现有 SoSite 5.0(MySQL)核心代码重构为 SoSite 1.0(SplitDB 纯 SQLite 分片引擎)
- 原则:彻底剥离 MySQL,不使用任何 MySQL 兼容层 / 双引擎切换 / 回退功能,只存在 SplitDB 一种驱动
二、核心开发红线(严禁违反)
- 彻底移除 MySQL 依赖:所有数据读写必须通过 SQLite 文件进行。严禁保留
PDO_MySQL连接逻辑、mysql_*函数、MySQL 语法兼容判断 - 不兼容 MySQL:不提供双引擎切换或 MySQL 回退,只存在
SplitDB一种驱动 - 功能精简:保留除插件之外的核心功能(用户注册/登录、发帖、回帖、版块、个人中心等);插件系统代码与目录全部保留,不删除——P0~P6 阶段仅「暂停迁移插件」,保持插件未激活/忽略状态,先让 SoSite 核心骨架跑通 SplitDB
补充要求(用户 2026-08-13 追加,与红线同等强制)
- 【精确的"暂不删除"机制】(P3/P5):遇到
Plugin::hook()调用时,严禁直接删除该行代码。必须采用// [SplitDB] 临时注释:前缀将其注释掉,注释内保留原始调用原文,方便日后回顾与恢复(恢复时去掉注释前缀即可)。 - 【数据无损迁移】(P7 重点):编写
cli/migrate.php时,必须确保正文(MEDIUMTEXT)成功剥离为extern/目录下的.txt文件。若 extern 文件生成失败,迁移脚本必须报错并整体回滚——严禁出现「索引导入成功但正文丢失」的情况。 - 【ViewCounter 安全降级】(P4):实现浏览计数时,若系统检测到未安装 APCu 扩展,严禁让全站报错。必须静默降级为简单的
UPDATE实时计数(降低性能但不崩站),确保程序在任何 PHP 环境下都能运行。
三、执行规范与工作流(必须严格遵守)
- 计划先行,禁止直接修改:行动前先输出书面《改造执行计划表》,经用户确认后才动工
- 绝对安全的更新模式:改动的核心文件可在原位置直接覆盖,同时在
update/目录下同步改动过的文件;update/根目录维护CHANGELOG.md,详细记录修改了哪些文件、进行了什么逻辑变更 - 串行执行与单步反馈:不批量修改,严格"改好一个核心模块,立即测试,通过后再进行下一个"
- 编码安全:全部文件
UTF-8 without BOM、LF 行尾,禁止乱码
四、架构与实现要求(参照白皮书 v2.3)
| # | 要求 | 对应实现 |
|---|---|---|
| 1 | 分片结构 | 季度时序分区 + 32 桶哈希分片(ID % 桶数),读取以 main_index.bucket_path 为准,不二次哈希 |
| 2 | 外置文件存储 | 帖子/回复正文剥离 SQLite,存入 extern/ 目录 .txt 文件 |
| 3 | 连接池与并发 | 单进程 LRU 连接池,最大 8 个常驻句柄,禁止 ATTR_PERSISTENT 持久连接 |
| 4 | 全局 ID 生成器 | global_id.sqlite 表,高并发原子自增取号(含重试机制) |
| 5 | 后台任务队列 | SQLite task_queue 异步队列(异步重建索引/更新统计) |
| 6 | 索引缓存 | 列表页索引 main_index.sqlite 与核心 Bucket 存储分离,支持最终一致性 |
五、适配 SoSite 5.0 现有骨架
- Model 层:保留
app/Models/目录结构(User.php、Thread.php、Post.php等),查询逻辑重写为调用 SplitDB 连接池与路由,而非直接执行 MySQL SQL - Config:修改
config.php,去掉 MySQL 配置项,加入DATA_PATH(数据目录)、BUCKET_SIZE(桶数量)等 SplitDB 配置 - 新增核心 Helper:在
app/Helpers/(实际落点app/SplitDB/)为 SplitDB 功能(外置文件读写、队列任务分发等)增加辅助类
六、交付标准
- 结构完整性:
update/目录代码必须是"可以运行"的骨架 - 不可逆性:提供可执行的旧 MySQL → SplitDB 数据迁移脚本(可选)
- 测试流程:交付后提供本地手动测试说明(如何发帖并验证分片文件生成)
七、勘察结论(现状盘点)
| 项 | 现状 | 对迁移的影响 |
|---|---|---|
config.php | 不存在,由 install.php 生成(DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASS/DB_CHARSET + 上传配置) | 重写生成模板:去 MySQL 配置,加 DATA_PATH/BUCKET_SIZE 等 |
app/Core/Database.php | MySQL 单例 PDO,query/fetchAll/fetchOne/lastInsertId/begin/commit/rollback + 独立 Session 连接 | 重写为 SplitDB 门面,保留同名 API,Models 改动最小化 |
app/Core/Model.php | 通用 CRUD,反引号 + LIMIT + 参数绑定(MySQL 语法) | 去反引号、SQLite 兼容,API 不变 |
app/Models/*(10 个) | 直接拼 MySQL SQL,含 GREATEST()/NOW()/ON DUPLICATE KEY 等 | 全部重写为 SQLite 语法 + 分片路由 |
protected/database/schema.sql | MySQL DDL(ENGINE=InnoDB/AUTO_INCREMENT/MEDIUMTEXT) | 重写为 SQLite DDL(meta 库 + 分片表) |
app/init.php | Session 存 MySQL(NOW()/DATE_ADD/ON DUPLICATE/SHOW TABLES) | 改 SQLite(独立 sessions.sqlite) |
| 帖子/回复正文 | 存在 threads.content / posts.content(MEDIUMTEXT) | 剥离 → extern/ .txt 文件(白皮书红线) |
| 插件体系 | Plugin.php + 21 个 hook 散布在 index/init/控制器/视图 | 保留,暂停迁移(P0~P6 插件不激活;测试期 Plugin::hook 调用临时注释/绕过,不删除) |
| 浏览计数 | Thread::incrementView 实时 UPDATE | 改 APCu 内存计数 + 异步 flush(白皮书模块五) |
| 搜索 | 自研倒排索引 search_index 表 + bigram 分词 | 已确认:保留自研倒排索引(平移至 SQLite) |
八、目标架构总览(SplitDB v2.3 落地形态)
数据流:
写入 = global_id 取号 → 季度分区 + ID%32 哈希 → 写 Bucket + 正文 extern → 随机入队 → 返回
读取 = 列表/分页走 main_index → 详情走 bucket_path + extern → 搜索走 search_index(异步)
分层:
data/meta/ → main_index.sqlite(帖子主索引) · business.sqlite(用户/版块/设置/
博客/私信/附件/标签/投票等非分片业务数据)
search_index.sqlite(自研倒排) · global_id.sqlite · sessions.sqlite
data/meta/task_queue/ → queue_0~2.sqlite(三队列打散锁竞争)
data/bucket/active/{季度}/{0..31}.sqlite → topic / reply 真相源
data/bucket/archive/ → 18 个月以上冷数据
data/extern/{年}/{季度}/{桶}/{id}.txt → 正文外置
cli/ → worker.php(队列) · rebuild_search.php · archive.php · migrate.php已确认的设计决策(用户拍板):
- 搜索方案:保留自研倒排索引(
Search.php+ bigram 分词,表平移至 SQLite),不采用 FTS5 - 非分片数据:独立
business.sqlite(与 main_index 分离,避免索引写锁互相影响)
九、目录结构变动表
| 路径 | 处理方式 | 说明 |
|---|---|---|
config.php | ✏️ 重写 | 去全部 MySQL 项;新增 DATA_PATH/BUCKET_SIZE/META_PATH/EXTERN_PATH |
install.php | ✏️ 重写 | 不再连 MySQL;调用 SplitDB\Schema::bootstrap() 建目录/库/表 + 建管理员 |
app/SplitDB/ | 🆕 新增(核心引擎) | DBFactory(LRU池8) · IDGenerator · ShardRouter(季度+哈希) · ExternStorage(原子写) · Queue+QueueConsumer(CAS) · Schema(bootstrap) · ViewCounter(APCu+降级) |
app/Core/Database.php | ✏️ 重写 | SplitDB 门面(meta 库连接 + 事务),保留原 API;getSessionConnection → sessions.sqlite |
app/Core/Model.php | ✏️ 改 | 去反引号,SQLite 兼容,lastInsertId 改 lastInsertRowID |
app/Models/Thread.php | ✏️ 重写 | 分片写入/读取 + main_index + extern 正文 |
app/Models/Post.php | ✏️ 重写 | 同上(回复表) |
app/Models/User/Category/Setting/Blog/BlogCategory/BlogComment/Message/Tag | ✏️ 重写 | business.sqlite 查询,清理 MySQL 语法 |
app/init.php | ✏️ 改 | Session 改 SQLite;插件初始化与钩子临时绕过(注释,不删除) |
app/Helpers/Plugin.php | ✅ 保留 | 插件系统核心(P0~P6 暂停激活,不删除) |
app/Controllers/Admin/PluginController.php | ✅ 保留 | 插件后台管理(暂停迁移,不删除) |
plugins/(整目录) | ✅ 保留 | 插件目录结构保留(目录下插件已移除) |
app/Helpers/*(Auth/Settings/Vote/Points/Tag/Search/Segmenter 等) | ✏️ 清理 | 去 MySQL 语法;Search 平移至 SQLite(Plugin::hook 调用保留,测试期临时绕过) |
app/Controllers/*(前台+后台) | ✏️ 改 | 临时注释/绕过 Plugin::hook 调用(不删除)、数据流适配(约 12 处钩子点) |
app/Views/* | ✏️ 改 | 临时注释视图内钩子(nav_main_links/announcements_display/layout_head_end 等 7 处,不删除) |
protected/database/schema.sql | ✏️ 重写 | SQLite DDL:meta 库表 + bucket 内 topic/reply 表 |
cli/ | 🆕 新增 | worker.php · rebuild_search.php · archive.php · migrate.php · wal_reclaim 脚本 |
data/ | 🆕 运行时生成 | 白皮书四章完整目录树 |
update/ | 🆕 新增 | 同步所有改动文件 + CHANGELOG.md + 本计划文档(可运行骨架) |
app/Core/Router/Autoloader/TemplateCompiler/Controller、assets/、主题 | ✅ 保留 | 仅 Controller 1 处钩子临时注释(不删除) |
十、改造执行计划表(严格串行,每步验证通过才进下一步)
| 阶段 | 任务 | 交付物 | 验证方式 | 状态 |
|---|---|---|---|---|
| P0 骨架与配置 | ① 重写 install.php + 新增 Schema::bootstrap(建目录树/4 个 meta 库/队列库/首季度 32 桶)② 重写 config.php 生成模板 | 空库可初始化 | php -l + 跑 install 生成目录树 | ⏳ 待办 |
| P1 核心引擎 ✅ | ① DBFactory(LRU≤8、禁持久、统一 PRAGMA)② IDGenerator(global_id + BEGIN IMMEDIATE 事务 + 重试)③ ShardRouter(季度+ID%32)④ ExternStorage(tmp+rename 原子写 + APCu 降级) | 四个类 + 4 个冒烟脚本 | CLI 冒烟 50/50 通过 | ✅ 已完成 |
| P2 队列与消费 | ① Queue(随机三队列分发)② QueueConsumer(CAS 抢占/超时恢复/重试上限)③ cli/worker.php | 异步队列可用 | 压入 100 任务,worker 消费,验证 pending→processing→done/failed | ⏳ 待办 |
| P3 数据库门面 | ① 重写 Database.php(meta 库门面)② 改 Model.php ③ init.php Session 改 SQLite | 全站无 MySQL 连接 | grep 零残留 MySQL 关键字;页面可渲染 | ⏳ 待办 |
| P4 模型层 | ① Thread/Post 分片重写(写桶+extern+main_index,读 main_index+桶)② User/Category/Setting 等 business.sqlite 化 ③ ViewCounter(无 APCu 静默降级为 UPDATE 实时计数,不崩站) | Models 全 SQLite | CLI 发帖冒烟:分片文件/正文文件/索引行三处生成 | ⏳ 待办 |
| P5 控制器与视图 | ① 发帖/回帖/列表/详情流程接 SplitDB ② 全站 // [SplitDB] 临时注释: 前缀注释 Plugin::hook(保留原始调用原文,不删除,确保测试期不触发插件逻辑)③ 后台适配(数据库页改 VACUUM/备份 SQLite) | 前台+后台可用 | phpstudy 手动发帖→列表→详情→回复→搜索全链路 | ⏳ 待办 |
| P6 索引与统计 | ① 队列事件接 rebuild_search/stats ② cli/rebuild_search.php/archive.php ③ 凌晨统计 | 最终一致性闭环 | 删库重建索引,比对分片真相源 | ⏳ 待办 |
| P7 迁移与测试 | ① cli/migrate.php(可选:读旧 MySQL → 按路由规则导入;正文 MEDIUMTEXT 必须成功剥离为 extern/ .txt,失败则报错回滚,杜绝「索引导入成功但正文丢失」)② 测试流程文档 ③ CHANGELOG.md 收尾 | 迁移+验收 | 按测试流程走通 | ⏳ 待办 |
十一、update/ 同步与交付规范
- 同步规则:每改动一个文件,立即拷贝至
update/对应相对路径(含config.php模板、cli/、app/全量),保证update/是可运行骨架 - CHANGELOG.md:每次模块改动后追加一条,格式
[P1] DBFactory.php — 新增 LRU 连接池(MAX_HANDLES=8)…,含文件清单与逻辑变更说明 - 编码安全:全部文件
UTF-8 without BOM、LF 行尾(逐文件校验确认)
十二、测试流程说明(交付后本地验收)
- phpstudy 起 Nginx + PHP 8.0.2(无需 MySQL)→ 访问
/install.php初始化 → 确认data/目录树生成 - 注册账号 → 发帖「测试标题」→ 验证:
data/bucket/active/{当前季度}/{id%32}.sqlite出现 topic 行data/extern/{年}/{季}/{桶}/{id}.txt生成正文main_index.sqlite有索引行
- 首页/版块列表可见新帖(读 main_index);点开详情正文正常(读 extern);回复后 reply 落桶 + 回复计数异步更新
- 搜索关键词命中(search_index);
php cli/worker.php消费队列后索引一致
十三、进度状态
- ✅ P1 核心引擎已完成(2026-08-13):DBFactory / IDGenerator / ShardRouter / ExternStorage + 4 个冒烟脚本,50/50 通过
- 关键适配:PHP 8.0.2 的 SQLite 3.33.0 不支持
UPDATE…RETURNING(需 ≥3.35),IDGenerator 改用BEGIN IMMEDIATE写锁事务(等价原子性,兼容 7.4+) - PDO-SQLite 对
exec('BEGIN IMMEDIATE')不置内部事务标志,事务控制全程用原始 SQL(exec('COMMIT')/exec('ROLLBACK')),避免 "There is no active transaction" 与残留事务问题 - 全部文件 UTF-8 无 BOM + LF;
update/已同步 8 个文件 +CHANGELOG.md - ⏳ 下一阶段:P2 任务队列(
Queue+QueueConsumer+cli/worker.php),等待用户指令后开始