Просмотр исходного кода

docs: 修正为 MySQL 环境 - 改用 SELECT ... FOR UPDATE 行锁

后端实际是 MySQL (asyncmy),条件唯一索引是 PostgreSQL 专有语法不适用。
改为在同一事务内对 dialogue_session 行加 SELECT ... FOR UPDATE 行锁,
serialize 同一 session 的 greeting 请求。不需要 schema 变更。

表名修正 dialogue_messages → dialogue_message (单数)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jimmylee 2 месяцев назад
Родитель
Сommit
1055717cf1
1 измененных файлов с 10 добавлено и 10 удалено
  1. 10 10
      doc/DialogueSessionSplitDesign.md

+ 10 - 10
doc/DialogueSessionSplitDesign.md

@@ -95,9 +95,10 @@ Response: { aiMessage: string }
 2. 查该 session `round=1 && role='ai'` 消息,若存在 → 直接返回其 content
 3. 否则按原 `create_session` 步骤 2-3 调 LLM + 落 `DialogueMessage` + commit
 
-**幂等性实现重点:**
-- 后端加 DB 层兜底:在 `dialogue_messages` 表上建条件唯一索引 `UNIQUE(session_id, round, role) WHERE role='ai' AND round=1`(PostgreSQL 条件索引语法 `CREATE UNIQUE INDEX ... WHERE ...`)。只约束 round=1 的 AI 消息以限定作用域,避免干扰后续轮次的常规 INSERT。并发 INSERT 时第二个会被 DB 拒掉,捕获 `IntegrityError` 后重查已有消息返回即可。
-- 前端配合做按钮 disabled 锁(§4.6),防止用户双击派生两个并发请求。前端锁 + 后端约束双层保险。
+**幂等性实现重点(MySQL 环境):**
+- 后端在同一事务内对 `dialogue_session` 行加行锁:`SELECT * FROM dialogue_session WHERE uuid = :uuid FOR UPDATE`(SQLAlchemy `.with_for_update()`)。并发请求 serialize 到同一 session,后到者会阻塞等待,拿到锁后查 round=1 AI 消息已存在 → 直接返回。
+- 不新增 schema / 索引;MySQL 不支持 PostgreSQL 的条件唯一索引,而全局 `UNIQUE(session_id, round, role)` 对历史数据风险太大,放弃 DB 约束路线。
+- 前端配合做按钮 disabled 锁(§4.6),防止用户双击派生两个并发请求。前端锁 + 后端行锁双层保险。
 
 ### 3.3 其它接口
 
@@ -362,17 +363,16 @@ onUnmounted(() => {
 |---|---|
 | UI | "开始对话"按钮 `sessionCreating` 期间 disabled;error-card "重新生成"按钮在 `greetingInflight` 期间 disabled |
 | 前端逻辑 | `generateGreeting` / `retryGreeting` 入口处检 `greetingInflight` ref,in-flight 直接 return |
-| 后端 | `dialogue_messages` 加条件唯一索引 `UNIQUE(session_id, round, role) WHERE role='ai' AND round=1`(PostgreSQL `CREATE UNIQUE INDEX ... WHERE ...`);并发 INSERT 由 DB 保证唯一,第二个捕 `IntegrityError` → 重查已有消息返回 |
+| 后端 | 在 `dialogue_session` 行上加行锁 `SELECT ... FOR UPDATE`(SQLAlchemy `.with_for_update()`),同一 session 的并发 greeting 请求 serialize;后到者拿锁后查已有 round=1 AI 消息 → 直接返回。MySQL 原生支持,无需 schema 变更 |
 
 ---
 
 ## 5. 实施顺序
 
-1. **后端**
-   1. `migrations/`:新增 migration,在 `dialogue_messages` 上建条件唯一索引 `UNIQUE(session_id, round, role) WHERE role='ai' AND round=1`
-   2. `dialogue_service.py`:`create_session` 拆成 `create_session_only` + `generate_greeting`(含幂等查现有消息 + 捕获 `IntegrityError` 重查兜底)
-   3. `api/dialogue.py`:`/session` 返回去掉 `aiMessage`;新增 `POST /session/{session_id}/greeting` 路由
-   4. 单测:session 只建不生成、greeting 幂等(重复调返回同一内容)、greeting 对非 active session 返 409、greeting 对不存在 session 返 404、并发 greeting 只产一条 AI 消息
+1. **后端** (MySQL,表名 `dialogue_session` / `dialogue_message`)
+   1. `dialogue_service.py`:`create_session` 拆成 `create_session_only`(仅建 session,立即 commit) + `generate_greeting`(`with_for_update()` 行锁 → 查 round=1 AI → 有则返回 / 无则调 LLM 落消息 commit)
+   2. `api/dialogue.py`:`/session` 响应去掉 `aiMessage`;新增 `POST /session/{session_id}/greeting` 路由
+   3. 单测:session 只建不生成、greeting 幂等(重复调返回同一内容且 DB 只一条)、greeting 对非 active session 返 409、greeting 对不存在 session 返 404
 2. **前端**
    1. `llmService.ts`:新增 `DialogueApiError` + `createDialogueApi` 工厂;`DialogueAPI` 接口扩展;Real/Mock 都实现 `generateGreeting(signal)`;`createSession` 返回类型更新
    2. `useDialogueEngine.ts`:拆 `initSession` → `attachSession` + `generateGreeting`(含 `greetingInflight` 锁 + `greetingAbortController`) + `retryGreeting`;`onUnmounted` 追加 abort
@@ -394,7 +394,7 @@ onUnmounted(() => {
 - [ ] `POST /session` 响应体不含 `aiMessage`
 - [ ] `POST /session/{id}/greeting` 对同一 session 重复调用返回完全相同的 `aiMessage` 且 DB 中仅一条 round=1 AI 消息
 - [ ] `POST /session/{id}/greeting` 对非 active session 返 409,对不存在 session 返 404
-- [ ] 并发两次 greeting 请求(模拟双击)DB 仅产生一条 round=1 AI 消息,其中一个请求由 `IntegrityError` 路径重查后返回相同内容
+- [ ] 并发两次 greeting 请求(模拟双击)DB 仅产生一条 round=1 AI 消息;两个请求返回相同 `aiMessage`,第二个通过行锁等待后走"已存在直接返回"路径
 - [ ] Ready 页点击"开始对话"期间按钮 disabled + loading,session 创建失败后按钮恢复可点且下方显示错误文案,再点击可成功重试
 - [ ] 进入聊天页后立刻出现 typing-bubble(greeting 期间),greeting 到达后变为正常 AI 消息气泡
 - [ ] greeting 500/网络失败时,error-card 按钮文案 "重新生成",点击后复用同一 sessionId 再请求;in-flight 期间按钮 disabled