Files
note/课题一/AI 视觉对话助手/持久化技术选型.md
2026-06-12 15:36:35 +08:00

251 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [数据库, PostgreSQL, 持久化, 架构设计, 拓展功能]
create time: 2026-06-12 15:33
---
# 持久化技术选型
## 概述
本文档是 [[项目架构与技术栈]] 的拓展阅读——在核心功能(实时视觉对话)跑通之后,如果需要**保存对话历史、统计用量成本、管理用户偏好**,就需要引入持久化层。本文对比主流数据库方案,解释为什么推荐 PostgreSQL以及在什么情况下应该选择其他方案。
> [!info] 定位
> 这是一篇**拓展选型文档**,不阻塞 MVP 开发。MVP 阶段用 Redis 做会话存储即可;当产品需要"历史可查、成本可算"时,再引入本节方案。
## 正文
### 项目的数据特征
选数据库之前,先搞清楚我们的数据长什么样:
| 数据 | 结构特征 | 读写模式 | 数据量级 |
|------|---------|---------|---------|
| 对话消息 | 强结构化(角色/内容/时间/关联图像) | 写多读少,按会话聚合读取 | 中(每用户日均 ~100 条) |
| 会话元信息 | 强结构化(用户/时间/状态) | 写少读少 | 低 |
| 对话上下文 | 半结构化JSON 数组,含图像引用) | 高频读写TTL 过期 | 低(仅当前窗口) |
| 用量统计 | 强结构化(数字/日期/聚合) | 写多,定期聚合读 | 低(日粒度汇总后很小) |
| 用户偏好 | 强结构化KV 配置) | 写极少读少 | 极低 |
| 关键帧图像 | 非结构化二进制 | 写少,按需读 | 大(单张 100KB~1MB |
> [!question] 思考
> 从上表可以看出,核心数据(对话、会话、统计)都是**强结构化**的,有明确的字段和关联关系。这意味着关系型数据库天然适配。但"对话上下文"是半结构化 JSON这就需要数据库对 JSON 有良好支持。
### 候选方案全景对比
```mermaid
graph TD
A["持久化技术选型"] --> B["关系型数据库"]
A --> C["文档型数据库"]
A --> D["嵌入式数据库"]
B --> B1["PostgreSQL"]
B --> B2["MySQL"]
B --> B3["TiDB"]
C --> C1["MongoDB"]
D --> D1["SQLite"]
```
| 维度 | PostgreSQL | MySQL | SQLite | MongoDB | TiDB |
|------|-----------|-------|--------|---------|------|
| **数据模型** | 关系型 + JSONB | 关系型 | 关系型(嵌入式) | 文档型BSON | 关系型(分布式) |
| **JSON 支持** | JSONB 原生索引 | JSON 类型,索引弱 | 无原生支持 | 天生擅长 | 兼容 MySQL JSON |
| **关联查询** | 强 | 强 | 强 | 弱(需嵌套/lookup | 强 |
| **聚合统计** | 窗口函数/CTE 完善 | 基础聚合 | 基础聚合 | 聚合管道 | 强 |
| **并发能力** | 高MVCC | 中 | 低(单写锁) | 高 | 极高(分布式) |
| **运维复杂度** | 中 | 低 | 极低 | 中 | 高 |
| **Go 生态** | pgx / GORM | go-sql-driver | go-sqlite3 | mongo-go-driver | 兼容 MySQL 驱动 |
| **部署方式** | Docker / 云服务 | Docker / 云服务 | 单文件嵌入 | Docker / Atlas | 集群部署 |
| **成本** | 开源免费 | 开源免费 | 开源免费 | 社区版免费 | 开源免费 |
### 逐个分析:为什么不选它们?
#### SQLite —— 太轻了
```mermaid
graph LR
A["SQLite"] --> B["单文件数据库"]
B --> C{"适合本项目?"}
C -->|"否"| D["写并发受限"]
C -->|"否"| E["无法多实例共享"]
```
- **优点**:零配置,一个 `.db` 文件搞定,开发阶段极方便
- **致命问题****写锁是全局的**——同一时刻只能有一个写操作。当 WebSocket 并发写入对话消息时,会频繁锁等待
- **另一个问题**:多个 Go Gateway 实例无法共享同一个 SQLite 文件(除非用 NFS但性能极差
> [!tip] 什么时候选 SQLite
> 如果是**单机部署的桌面应用或 CLI 工具**SQLite 是最佳选择。但我们的项目是 Web 服务、多实例部署,不合适。
#### MySQL —— 能用,但 JSON 处理弱
```mermaid
graph LR
A["MySQL"] --> B["JSON 支持"]
B --> C{"够用吗?"}
C -->|"勉强"| D["JSON 索引弱"]
C -->|"否"| E["无 JSONB 二进制存储"]
```
- **优点**:运维简单,社区庞大,很多团队更熟悉
- **本项目的痛点**:对话上下文是 JSON 数组含图像引用、角色标记MySQL 的 JSON 类型支持**索引能力弱**,无法对 JSON 内部字段高效查询
- **另一个痛点**:缺少 `gen_random_uuid()` 等原生函数,需要应用层生成 UUID
> [!question] 思考
> 如果团队只熟悉 MySQL能不能用**完全可以**。JSON 上的差异在 MVP 阶段几乎无感,只是后续做复杂查询(如"找出所有包含某关键词的对话")时会比 PostgreSQL 麻烦一些。
#### MongoDB —— 文档型,关联查询弱
```mermaid
graph LR
A["MongoDB"] --> B["天然 JSON 存储"]
B --> C{"适合本项目?"}
C -->|"否"| D["关联查询弱"]
C -->|"否"| E["聚合统计不如 SQL 直观"]
```
- **优点**Schema-less存 JSON 天然舒适,水平扩展能力强
- **本项目的痛点**
- `messages` 需要按 `session_id` 关联 `sessions`,再按 `user_id` 聚合——这在 MongoDB 中要用 `$lookup`,写法复杂且性能不如 SQL JOIN
- 用量统计的"按天聚合"用 SQL 的 `GROUP BY + SUM` 一句话搞定MongoDB 的聚合管道代码量多 3-5 倍
> [!info] 什么时候选 MongoDB
> 如果数据模型是**高度嵌套的文档**如博客系统、CMS且很少跨文档关联查询MongoDB 是好选择。我们的数据关联性强,不太适合。
#### TiDB —— 杀鸡用牛刀
- **优点**:兼容 MySQL 协议,分布式架构,水平扩展无上限
- **本项目的痛点**:部署复杂(至少 3 个 PD + 3 个 TiKV + 2 个 TiDB运维成本高
- **结论**:单机 PostgreSQL 完全够用,引入 TiDB 纯属过度设计
> [!tip] 什么时候选 TiDB
> 数据量过亿、需要跨地域部署、单机 PostgreSQL 已经扛不住时。对于本项目,短期内不会遇到这个瓶颈。
### 为什么选 PostgreSQL
综合以上分析PostgreSQL 在本项目的核心需求上**全面契合**
```mermaid
graph TD
A["项目需求"] --> B["强结构化数据"]
A --> C["JSON 半结构化"]
A --> D["关联查询"]
A --> E["聚合统计"]
A --> F["Go 生态"]
B --> PG["PostgreSQL"]
C --> PG
D --> PG
E --> PG
F --> PG
```
逐条对应:
| 项目需求 | PostgreSQL 的匹配点 |
|---------|-------------------|
| 对话历史是强结构化数据 | 原生关系型SQL 标准完备 |
| 对话上下文含 JSON图像引用/角色标记) | **JSONB** 类型支持索引、路径查询、部分更新 |
| messages ↔ sessions 外键关联 | 完整的 FK 约束 + JOIN 支持 |
| 用量按天/周/月聚合 | 窗口函数、CTE、`DATE_TRUNC` 等分析能力 |
| Go 后端对接 | `pgx` 驱动性能优秀GORM/Ent ORM 支持成熟 |
| 后续可能存图像元信息 | 可结合对象存储PG 只存引用路径 |
| 未来可能加全文搜索 | 内置 `tsvector` 全文检索,无需额外引入 Elasticsearch |
### Go 后端集成示例
使用 `pgx` 驱动连接 PostgreSQL
```go
import "github.com/jackc/pgx/v5/pgxpool"
// 初始化连接池
pool, _ := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/vision_ai")
// 保存一条对话消息
func SaveMessage(ctx context.Context, pool *pgxpool.Pool, msg *Message) error {
_, err := pool.Exec(ctx,
`INSERT INTO messages (session_id, role, content, image_url, tokens_used)
VALUES ($1, $2, $3, $4, $5)`,
msg.SessionID, msg.Role, msg.Content, msg.ImageURL, msg.TokensUsed,
)
return err
}
// 查询用户近 7 天用量汇总
func GetWeeklyUsage(ctx context.Context, pool *pgxpool.Pool, userID string) ([]UsageRow, error) {
rows, _ := pool.Query(ctx,
`SELECT date, llm_tokens, estimated_cost
FROM usage_daily
WHERE user_id = $1 AND date >= CURRENT_DATE - INTERVAL '7 days'
ORDER BY date`, userID)
defer rows.Close()
// ... scan rows
}
```
JSONB 查询示例——在对话上下文中搜索包含特定关键词的消息:
```sql
-- 在 messages.contentJSONB中搜索包含"花"的用户消息
SELECT id, content, created_at
FROM messages
WHERE role = 'user'
AND content @> '{"text": "花"}'
ORDER BY created_at DESC
LIMIT 20;
```
### 选型决策流程图
遇到新项目时,可以按这个流程快速决策:
```mermaid
flowchart TD
START["需要持久化?"] -->|"否"| REDIS["继续用 Redis"]
START -->|"是"| STRUCT{"数据强结构化?"}
STRUCT -->|"是"| SCALE{"数据量级?"}
STRUCT -->|"否, 高度嵌套"| MONGO["考虑 MongoDB"]
SCALE -->|"< 100GB, 单机可扛"| PG["PostgreSQL"]
SCALE -->|"海量, 需水平扩展"| TIDB["考虑 TiDB / CockroachDB"]
SCALE -->|"极小, 单文件即可"| SQLITE["考虑 SQLite"]
PG --> JSON{"有 JSON 需求?"}
JSON -->|"是"| PG_OK["PostgreSQL (JSONB)"]
JSON -->|"否"| MYSQL{"团队熟悉 MySQL?"}
MYSQL -->|"是"| MYSQL_OK["MySQL 也行"]
MYSQL -->|"否"| PG_OK
```
> [!question] 思考
> 技术选型没有"绝对正确",只有"更适合"。PostgreSQL 在本项目中胜出,核心原因是**数据模型匹配 + JSONB 能力 + Go 生态成熟**这三点的交集。如果换一个纯 KV 场景如缓存Redis 才是正确答案。
### 与现有架构的整合
引入 PostgreSQL 后,存储层变为**冷热分离**架构:
```mermaid
graph LR
subgraph Hot["热数据层"]
REDIS["Redis"]
end
subgraph Cold["冷数据层"]
PG["PostgreSQL"]
end
subgraph App["Go Gateway"]
WRITE["写入路径"]
READ["读取路径"]
end
WRITE -->|"实时会话状态"| REDIS
WRITE -->|"对话历史 + 用量"| PG
READ -->|"当前上下文(快)"| REDIS
READ -->|"历史记录(慢)"| PG
```
> [!info] 写入策略
> 建议采用**异步写入**——实时对话消息先写 Redis然后异步批量刷入 PostgreSQL。这样不会因为数据库写入延迟影响对话体验。可以用 Go channel + goroutine 实现简单的异步写入队列。
## 关联笔记
- [[项目架构与技术栈]]
- [[成本控制]]
- [[项目架构与技术栈/技术名词解释]]