本文主要记录在 SQLite / PostgreSQL 双 Provider 与数据迁移场景下,因 Guid 文本大小写 引发的乐观锁失败、登录外键失败与进度上报 500 如何识别和收口。根因在 SQLite TEXT 等值比较大小写敏感,而不是业务权限本身。
同一根因会以不同表面症状出现:刮削批量写库报并发冲突;PG→SQLite 切库后无法登录;seek / pause 后客户端显示「认证失败 HTTP 500」(实际可能是进度 upsert 踩了 FK)。
两类 Guid 列
| 类型 | 代表列 | 存储 | 读写约定 |
|---|---|---|---|
| 并发令牌 | 实体上的 ConcurrencyToken | SQLite TEXT / PG varchar | 应用写出固定 小写 D 格式 |
| 主键 / 外键 | Users.Id、AuthSessions.UserId 等 | SQLite 多为 TEXT;PG 为 uuid | 文本落库也应统一小写 |
当前约定:所有 作为文本落库 的 Guid 使用小写 8-4-4-4-12(Guid.ToString("D"))。乐观锁 UPDATE ... WHERE ConcurrencyToken = @original 比较的是 provider 侧字符串,库内必须与 converter 一致。
需要注意:PostgreSQL 上用户主键是 uuid 时,一般 不会 以「大小写不一致导致 FK 失败」的形式出现;ConcurrencyToken 仍是 varchar,乐观锁同样大小写敏感。
原因分析
SQLite TEXT 大小写敏感
"ABC..." 与 "abc..." 是两个不同值。ConcurrencyToken 与 SQLite 上的 Guid 键都以文本存储。
应用写出小写,ADO 默认绑参曾大写
- EF 的 Guid 字符串 converter 使用
ToString("D"),.NET 固定小写。 Microsoft.Data.Sqlite将Guid参数绑定为 大写 TEXT(当前版本无稳定连接串开关可改)。- 库内
Users.Id小写、插入会话仍绑大写UserId→ FK 失败。 - 库内
ConcurrencyToken大写、更新 WHERE 用小写 → 乐观锁失败。
历史迁移曾强制大写
有的迁移路径曾把写入 SQLite 的 Guid 统一 ToUpperInvariant(),动机是对齐 ADO 默认大写绑参。结果是:
- 主键暂时与大写绑参「对上了」
- ConcurrencyToken 也变大写 → 与 converter 小写冲突 → 扫描 / 刮削并发更新失败
收口方向是 全链路统一小写,而不是再在迁移里抬成大写。
raw ADO 绕过 EF 拦截器
若进度上报等路径用 GetDbConnection().CreateCommand() 直接执行 SQL,不会经过 EF 命令管线拦截器。此处若仍 DbType.Guid:
- 绑出大写 TEXT
UserId - 库内
Users.Id已是小写 - SQLite FK 失败 → 未处理异常 → HTTP 500
- 客户端可能误读成「认证失败」
硬规则:凡 SQLite 上对 Guid TEXT 列做 raw ADO 绑参,必须显式小写字符串 + DbType.String,禁止假设拦截器会兜底。
排查流程
先确认真正连哪套库
开发机 systemd 常通过环境变量覆盖 JSON 配置:
pid="$(systemctl show -p MainPID --value <service-name>)"
tr '\0' '\n' < "/proc/${pid}/environ" | rg '^Database__'
期望:Database__Provider 与健康检查里的 provider 一致;SQLite 时 Data Source 指向预期文件。
核对 ConcurrencyToken 是否全小写(SQLite)
DB="/path/to/app.db"
sqlite3 -header -column "$DB" "
SELECT 'MediaItems' AS t, COUNT(*) AS n,
SUM(CASE WHEN ConcurrencyToken = lower(ConcurrencyToken) THEN 1 ELSE 0 END) AS lower_n,
SUM(CASE WHEN ConcurrencyToken GLOB '*[A-F]*' THEN 1 ELSE 0 END) AS upper_hex
FROM MediaItems;
"
lower_n = n且upper_hex = 0→ 形态正常upper_hex > 0→ 库内仍有大写 token,乐观锁容易失败
核对 Users.Id 与会话 FK(SQLite)
sqlite3 -header -column "$DB" "
SELECT COUNT(*) AS n,
SUM(CASE WHEN Id = lower(Id) THEN 1 ELSE 0 END) AS lower_n,
SUM(CASE WHEN Id GLOB '*[A-F]*' THEN 1 ELSE 0 END) AS upper_hex
FROM Users;
"
sqlite3 "$DB" "
PRAGMA foreign_keys = ON;
SELECT COUNT(*) AS orphan_auth
FROM AuthSessions a
LEFT JOIN Users u ON u.Id = a.UserId
WHERE u.Id IS NULL;
"
| 库内 Users.Id | 应用绑参 | 症状 |
|---|---|---|
| 全大写 | 已改小写 interceptor | 新登录可能 FK 失败 |
| 全小写 | 无 interceptor / 仍绑大写 | 新登录可能 FK 失败 |
| 混写 | 任意 | 孤儿会话、间歇失败 |
PostgreSQL 侧
ConcurrencyToken 仍建议核对是否混有大写 hex。Users.Id 若是 uuid,不要 按 SQLite TEXT 方式做 lower(Id) 数据修复。
处理办法
按优先级:
- 代码对齐:converter 小写;SQLite 注册参数 interceptor;迁移工具写小写;raw ADO 显式小写。
- 发布对齐:确认运行中的二进制已包含 interceptor(旧 publish 包会「代码看起来修了、进程还是旧的」)。
- 数据清洗(SQLite 历史脏数据):停服、备份后,把相关 TEXT Guid 列
lower()到统一形态;再测登录与一次扫描写库。
不要在未备份时直接改 live 库。PostgreSQL 生产实例若主键已是 uuid 且仅 ConcurrencyToken 脏,应只修 varchar 列,避免误操作 uuid。
验证
| 检查 | 期望 |
|---|---|
| 登录创建会话 | 无 FK 失败 |
| 刮削 / 扫描批量更新 | 无持续乐观锁风暴 |
| 播放进度 seek / pause / stop | 无因 Guid 绑参导致的 500 |
| SQLite 抽检 | Guid 文本列 upper_hex = 0 |
相关阅读
注意事项
- 症状在 SQLite 上最锋利;不要把「PG 上没复现」当成「没有大小写问题」。
- 客户端报「认证失败」时,先看后端是否其实是进度 / 会话写库 500。
- 双 Provider 项目里,凡新增 raw SQL 绑 Guid,都要按 SQLite 敏感比较再审一遍。