升级与迁移
BasicApp 已接入 XiHan.Framework.Upgrade 的版本状态、迁移台账、多租户分发、租约锁与维护模式扩展,并在 WebHost/UpdateScripts 保存前向 SQL 脚本。XiHan:Upgrade:EnableAutoCheckOnStartup 为 true 时,应用启动会自动执行待执行的脚本,失败即中断启动。
当前结论
| 能力 | 当前状态 |
|---|---|
| Framework Upgrade 模块与引擎 | 已注册 |
SysVersion 版本状态 | 已落库,每个数据库各自维护 |
SysMigrationHistory 台账存储与查询页 | 已接线;每执行一个脚本记一条 |
| 文件系统脚本发现 | 已接入,目录为 UpdateScripts/<版本>/ |
| 平台库 + 独立租户库分发 | 已实现 |
| 数据库租约锁 | 已实现 |
| 维护模式中间件 | 已实现 |
| 启动时自动执行 SQL | EnableAutoCheckOnStartup=true 时执行;框架缺省为 true,开发配置也写的是 true |
| 新建库登记基线 | 本次启动从零建出的平台库、InitializeDatabase 新建的独立库,建好即登记为最新脚本版本,不补跑历史脚本 |
启动即迁移,失败即中断
启动时有两个执行点,都调用 IUpgradeEngine.ExecuteAsync()、都以 EnableAutoCheckOnStartup 为开关:
- 数据库初始化的升级段:
SaasSchemaUpgrader实现框架的IDbSchemaUpgrader,DbInitializer按「建库建表 → 升级脚本 → 播种」三段执行,让存量表的新列在种子读写之前补齐。这一段只在EnableDbInitialization与EnableTableInitialization都开启时运行。 - 应用初始化之后:
XiHanUpgradeModule.OnPostApplicationInitializationAsync先确保版本行存在,再执行一次引擎。数据库初始化关闭时由这里执行;第 1 段已经升级过时这里版本已是最新,直接空转。
两处在引擎返回失败时都抛出「数据库升级失败,已中断启动」,应用不会带着半套表结构对外服务。不希望应用启动时改库,就把 EnableAutoCheckOnStartup 设为 false,由发布流程先执行脚本再启动应用。
代码落点
| 职责 | 位置 / 类型 |
|---|---|
| 框架模块 | XiHanBasicAppCoreModule 依赖 XiHanUpgradeModule |
| 版本与台账存储 | SaasUpgradeVersionStore |
| 数据库租约锁 | SaasUpgradeLockProvider |
| 多租户列表 | SaasUpgradeTenantProvider |
| SQL 执行 | SaasUpgradeMigrationExecutor |
| 初始化升级段 | SaasSchemaUpgrader(IDbSchemaUpgrader:登记新库基线,按开关执行引擎) |
| 维护模式 | BasicAppUpgradeMaintenanceModeManager + MaintenanceModeMiddleware(经 MaintenanceModeStartupFilter 挂在请求管线最前,认证与租户解析之前就拦下) |
| 脚本目录 | backend/src/main/XiHan.BasicApp.WebHost/UpdateScripts |
| 版本页面 | /setting/version |
| 升级记录页面 | /log/migration |
BasicApp 在 AddSaasDomainServices() 中注册四个数据库适配器,使单服务解析优先使用业务实现:版本与台账写入业务库,SQL 由当前租户上下文解析出的 SqlSugar 客户端执行。
全新数据库与存量数据库
两条路径职责不同:
| 场景 | 机制 | 负责内容 |
|---|---|---|
| 全新数据库 | SqlSugar CodeFirst + Seeder | 建库、建表、基础数据(演示数据按开关) |
| 存量数据库 | Upgrade + UpdateScripts | 增删列、索引变化、数据修复与版本推进 |
DbInitializer 对已存在的表不会自动补列。实体结构变更如果只改 C#、不写前向 SQL,存量库会在查询时出现 column does not exist 一类错误。
脚本约定
目录结构是一个版本一个子目录(框架的 FileSystemUpgradeScriptProvider 只扫子目录),当前形如:
UpdateScripts/
├── 3.10.0/3.10.0.sql
├── 3.10.1/3.10.1.sql
├── …
├── 5.4.0/
│ ├── 5.4.0-page-codes.sql
│ └── 5.4.0.sql
├── 5.6.0/5.6.0.sql
└── README.md规则:
- 目录名即版本号,如
UpdateScripts/5.6.1/5.6.1.sql。只有高于库中DbVersion的版本会执行,与程序版本backend/props/version.props无关。 - 当前脚本使用 PostgreSQL 方言;文档中的 MySQL / MariaDB 支持主要指 ORM 与首次 CodeFirst,不代表现有升级脚本可直接跨库运行。引擎不区分方言,会把脚本原样交给当前库执行:非 PostgreSQL 的存量库开着
EnableAutoCheckOnStartup升级时,DO $$ … $$这类 PostgreSQL 专有语法会执行失败并中断启动,需要自备对应方言的脚本,或关掉自动升级另行处理。 - PostgreSQL 标识符使用小写且不加引号,匹配 SqlSugar 实际创建的表列名。
- 尽量用
IF EXISTS/IF NOT EXISTS写成可重试脚本。 - 一个版本内若存在多个脚本,引擎按脚本名排序;版本之间按语义版本升序执行。
- 先在生产数据副本验证脚本、备份与回滚方案,再发布应用。
示例:
-- 5.6.1/5.6.1.sql
ALTER TABLE sys_example
ADD COLUMN IF NOT EXISTS remark varchar(500);
CREATE INDEX IF NOT EXISTS ix_sys_example_tenant_id
ON sys_example (tenant_id);引擎执行流程
启动时的两个执行点调用的是同一台 UpgradeEngine,它对每个目标数据库执行:
解析应用版本与脚本
→ 读取/创建 SysVersion
→ 比较 DbVersion 与最新脚本版本
→ 获取数据库租约锁
→ 标记 IsUpgrading
→ 进入维护模式
→ 按版本和脚本名执行未成功记录过的脚本
→ 写 SysMigrationHistory 并推进 DbVersion
→ 更新 AppVersion
→ 退出维护模式并释放锁HasMigrationHistoryAsync(version, scriptName) 只跳过已有成功记录的脚本;失败记录不会阻止下一次重试。
多租户
EnableMultiTenantIsolation=true 时,目标顺序是:
- 平台库;
IsolationMode=Database且ConfigStatus=Configured的租户独立库。
字段隔离租户与平台共库,不重复执行。每个独立库都有自己的 SysVersion 与 SysMigrationHistory,数据库版本可以独立追踪。
新建的库不补跑历史脚本:本次启动从零建出全部实体表的平台库(SaasSchemaUpgrader 按 DbSchemaUpgradeContext.IsFresh 判定)与 InitializeDatabase 新建的独立库,建好即通过 IUpgradeEngine.BaselineAsync 登记为最新脚本版本。独立库里只有租户库实体的表,脚本改平台库表([PlatformDataSource] 实体)的语句要先判表存在,见 UpdateScripts/README.md。
租约锁
SaasUpgradeLockProvider 通过条件更新 SysVersion.IsUpgrading 抢占执行权,并用 UpgradeStartTime + LockExpirySeconds 回收崩溃节点遗留的锁。它不是 Redis 锁,也不是 PostgreSQL 会话级建议锁。
PrimaryNodeName 非空时,只有节点名完全匹配的实例会执行;留空时所有节点均可竞争数据库租约。NodeName 留空则由机器名与应用实例 ID 组合生成。
非主节点、以及没抢到租约的节点不会等待:引擎直接返回「等待升级 / 锁已被占用」,这不算失败,该节点照常完成启动。多副本同时发布时,要靠发布编排保证升级节点完成之前其它副本不接流量。
维护模式
引擎进入维护模式后,本节点的大部分请求返回:
{
"code": 503,
"message": "系统正在升级维护,请稍后重试。"
}响应带 Retry-After: 30。/health 与 /.well-known/ 被放行,保证编排探针和 OIDC 发现/JWKS 可继续访问。
维护状态只在当前进程内生效
MaintenanceModeState 是进程内单例。多副本部署时,执行迁移的节点进入维护模式,不会自动让其它节点一起停流。若迁移与旧版本不兼容,应在网关或发布编排层统一摘流,不能只依赖该中间件。
执行方式怎么选
当前默认是第一种;要换成别的方式,先关掉 EnableAutoCheckOnStartup:
| 方式 | 适用场景 | 注意 |
|---|---|---|
启动时自动执行(当前默认,EnableAutoCheckOnStartup=true) | 单体或单副本先行的发布 | 失败即中断启动;多副本时非主节点和抢不到租约的节点不等待,需编排层控制放流 |
发布流水线独立迁移步骤(EnableAutoCheckOnStartup=false) | 多副本生产 | 迁移成功后再放应用流量,边界最清晰;仓库没有现成的独立迁移命令,需要自建 |
管理端调用 IUpgradeCoordinator.StartAsync() | 人工触发 | BasicApp 没有接这个入口;协调器后台运行且只记日志,不能形成启动失败门禁 |
不要在多个位置同时触发。无论选择哪种方式,都要保证只有一个可审计入口,并测试多节点竞争、脚本失败、进程中断和重复执行。
页面与权限
/setting/version:查看SysVersion,只读;写端点已移除。/log/migration:查看脚本版本、脚本名、成功状态、执行时间、节点与错误信息。- 两页读取都要求
saas:version:read;版本列表导出使用saas:version:export。
页面展示的是当前租户上下文解析到的数据库状态。排查独立租户库时要先确认当前租户,不能只看平台库记录。
发布检查清单
- 抬高应用版本,并新增同版本或更低的必要 SQL 脚本。
- 确认脚本为 PostgreSQL 小写无引号标识符,且可安全重试。
- 在生产副本依次验证升级、重复执行和失败恢复。
- 备份平台库与全部数据库隔离租户库。
- 确认生产环境的
EnableAutoCheckOnStartup:缺省为true,新版本启动即执行脚本;关掉时要在启动前由发布流程完成迁移。 - 多节点发布时确认
NodeName/PrimaryNodeName、租约时长与统一摘流方案。 - 升级后检查
/setting/version、/log/migration、应用日志与关键业务查询。
相关页面
- 数据库配置:CodeFirst、连接与存量结构变更
- 健康检查与可观测性:维护窗口仍放行的
/health - 配置参考:
XiHan:Upgrade字段 - 部署:发布、进程守护与生产配置
- Framework 升级与迁移:引擎抽象与通用流程
