279 lines
7.5 KiB
Markdown
279 lines
7.5 KiB
Markdown
# 服务器重启数据加载修复说明
|
||
|
||
## 问题描述
|
||
|
||
服务器重启后没有正确载入底层数据库中的数据,导致内存存储为空。
|
||
|
||
## 根本原因
|
||
|
||
在之前的实现中,服务器启动时只创建了空的 `MemoryStore`,但没有从数据库中加载已有的数据到内存中。这导致:
|
||
|
||
1. 服务器重启后,内存中的数据丢失
|
||
2. 查询操作无法找到已有数据
|
||
3. 插入操作可能产生重复数据
|
||
|
||
## 修复方案
|
||
|
||
### 1. 添加 `ListCollections` 方法到数据库适配器
|
||
|
||
为所有数据库适配器实现了 `ListCollections` 方法,用于获取数据库中所有现有的表(集合):
|
||
|
||
**文件修改:**
|
||
- `internal/database/base.go` - 添加基础方法声明
|
||
- `internal/database/sqlite/adapter.go` - SQLite 实现
|
||
- `internal/database/postgres/adapter.go` - PostgreSQL 实现
|
||
- `internal/database/dm8/adapter.go` - DM8 实现
|
||
|
||
**SQLite 示例代码:**
|
||
```go
|
||
// ListCollections 获取所有集合(表)列表
|
||
func (a *SQLiteAdapter) ListCollections(ctx context.Context) ([]string, error) {
|
||
query := `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`
|
||
rows, err := a.GetDB().QueryContext(ctx, query)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var tables []string
|
||
for rows.Next() {
|
||
var table string
|
||
if err := rows.Scan(&table); err != nil {
|
||
return nil, err
|
||
}
|
||
tables = append(tables, table)
|
||
}
|
||
|
||
return tables, rows.Err()
|
||
}
|
||
```
|
||
|
||
### 2. 添加 `Initialize` 方法到 MemoryStore
|
||
|
||
在 `internal/engine/memory_store.go` 中添加了 `Initialize` 方法:
|
||
|
||
```go
|
||
// Initialize 从数据库加载所有现有集合到内存
|
||
func (ms *MemoryStore) Initialize(ctx context.Context) error {
|
||
if ms.adapter == nil {
|
||
log.Println("[INFO] No database adapter, skipping initialization")
|
||
return nil
|
||
}
|
||
|
||
// 获取所有现有集合
|
||
tables, err := ms.adapter.ListCollections(ctx)
|
||
if err != nil {
|
||
// 如果 ListCollections 未实现,返回 nil(不加载)
|
||
if err.Error() == "not implemented" {
|
||
log.Println("[WARN] ListCollections not implemented, skipping initialization")
|
||
return nil
|
||
}
|
||
return fmt.Errorf("failed to list collections: %w", err)
|
||
}
|
||
|
||
log.Printf("[INFO] Found %d collections in database", len(tables))
|
||
|
||
// 逐个加载集合
|
||
loadedCount := 0
|
||
for _, tableName := range tables {
|
||
// 从数据库加载所有文档
|
||
docs, err := ms.adapter.FindAll(ctx, tableName)
|
||
if err != nil {
|
||
log.Printf("[WARN] Failed to load collection %s: %v", tableName, err)
|
||
continue
|
||
}
|
||
|
||
// 创建集合并加载文档
|
||
ms.mu.Lock()
|
||
coll := &Collection{
|
||
name: tableName,
|
||
documents: make(map[string]types.Document),
|
||
}
|
||
for _, doc := range docs {
|
||
coll.documents[doc.ID] = doc
|
||
}
|
||
ms.collections[tableName] = coll
|
||
ms.mu.Unlock()
|
||
|
||
loadedCount++
|
||
log.Printf("[DEBUG] Loaded collection %s with %d documents", tableName, len(docs))
|
||
}
|
||
|
||
log.Printf("[INFO] Successfully loaded %d collections from database", loadedCount)
|
||
return nil
|
||
}
|
||
```
|
||
|
||
### 3. 在服务器启动时调用初始化
|
||
|
||
修改 `cmd/server/main.go`,在创建内存存储后立即调用初始化:
|
||
|
||
```go
|
||
// 创建内存存储
|
||
store := engine.NewMemoryStore(adapter)
|
||
|
||
// 从数据库加载现有数据到内存
|
||
log.Println("[INFO] Initializing memory store from database...")
|
||
if err := store.Initialize(ctx); err != nil {
|
||
log.Printf("[WARN] Failed to initialize memory store: %v", err)
|
||
// 不阻止启动,继续运行
|
||
}
|
||
|
||
// 创建 CRUD 处理器
|
||
crud := engine.NewCRUDHandler(store, adapter)
|
||
```
|
||
|
||
## 工作流程
|
||
|
||
```
|
||
服务器启动流程:
|
||
1. 连接数据库
|
||
↓
|
||
2. 创建 MemoryStore
|
||
↓
|
||
3. 【新增】调用 Initialize() 从数据库加载数据
|
||
↓
|
||
4. 创建 CRUDHandler
|
||
↓
|
||
5. 启动 HTTP/TCP 服务器
|
||
```
|
||
|
||
## 测试方法
|
||
|
||
### 快速测试(推荐)
|
||
|
||
```bash
|
||
cd /home/kingecg/code/gomog
|
||
./test_quick.sh
|
||
```
|
||
|
||
**预期输出:**
|
||
```
|
||
✅ 成功!服务器重启后正确加载了数据库中的数据
|
||
=== 测试结果:SUCCESS ===
|
||
```
|
||
|
||
### 详细测试
|
||
|
||
```bash
|
||
./test_reload_simple.sh
|
||
```
|
||
|
||
### 手动测试
|
||
|
||
1. **启动服务器并插入数据**
|
||
```bash
|
||
./bin/gomog -config config.yaml
|
||
|
||
# 在另一个终端插入数据
|
||
curl -X POST http://localhost:8080/api/v1/testdb/users/insert \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"documents": [{"name": "Alice", "age": 30}]}'
|
||
```
|
||
|
||
2. **验证数据已存入数据库**
|
||
```bash
|
||
sqlite3 gomog.db "SELECT * FROM users;"
|
||
```
|
||
|
||
3. **停止并重启服务器**
|
||
```bash
|
||
# Ctrl+C 停止服务器
|
||
./bin/gomog -config config.yaml
|
||
```
|
||
|
||
4. **查询数据(验证是否加载)**
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/v1/testdb/users/find \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"filter": {}}'
|
||
```
|
||
|
||
应该能看到之前插入的数据。
|
||
|
||
## 日志输出示例
|
||
|
||
成功的初始化日志:
|
||
```
|
||
2026/03/14 22:00:00 [INFO] Connected to sqlite database
|
||
2026/03/14 22:00:00 [INFO] Initializing memory store from database...
|
||
2026/03/14 22:00:00 [INFO] Found 1 collections in database
|
||
2026/03/14 22:00:00 [DEBUG] Loaded collection users with 2 documents
|
||
2026/03/14 22:00:00 [INFO] Successfully loaded 1 collections from database
|
||
2026/03/14 22:00:00 Starting HTTP server on :8080
|
||
2026/03/14 22:00:00 Gomog server started successfully
|
||
```
|
||
|
||
## 关键技术细节
|
||
|
||
### 集合名称映射
|
||
|
||
由于 HTTP API 使用 `dbName.collection` 格式(如 `testdb.users`),而 SQLite 数据库中表名不带前缀(如 `users`),实现了智能名称映射:
|
||
|
||
**1. Initialize 方法加载数据**
|
||
```go
|
||
// 从数据库加载时使用纯表名(例如:users)
|
||
ms.collections[tableName] = coll
|
||
```
|
||
|
||
**2. GetCollection 方法支持两种查找方式**
|
||
```go
|
||
// 首先尝试完整名称(例如:testdb.users)
|
||
coll, exists := ms.collections[name]
|
||
if exists {
|
||
return coll, nil
|
||
}
|
||
|
||
// 如果找不到,尝试去掉数据库前缀(例如:users)
|
||
if idx := strings.Index(name, "."); idx > 0 {
|
||
tableName := name[idx+1:]
|
||
coll, exists = ms.collections[tableName]
|
||
if exists {
|
||
return coll, nil
|
||
}
|
||
}
|
||
```
|
||
|
||
这样确保了:
|
||
- 新插入的数据可以使用 `testdb.users` 格式
|
||
- 重启后加载的数据也能通过 `testdb.users` 查询到
|
||
- 向后兼容,不影响现有功能
|
||
|
||
## 容错处理
|
||
|
||
修复实现了多层容错机制:
|
||
|
||
1. **无数据库适配器**:如果未配置数据库,跳过初始化
|
||
2. **ListCollections 未实现**:如果数据库不支持列出表,记录警告但不阻止启动
|
||
3. **单个集合加载失败**:记录错误但继续加载其他集合
|
||
4. **初始化失败**:记录错误但服务器继续运行(不会崩溃)
|
||
|
||
## 影响范围
|
||
|
||
- ✅ 服务器重启后数据自动恢复
|
||
- ✅ HTTP API 和 TCP 协议行为一致
|
||
- ✅ 支持 SQLite、PostgreSQL、DM8 三种数据库
|
||
- ✅ 向后兼容,不影响现有功能
|
||
- ✅ 优雅降级,初始化失败不影响服务器启动
|
||
|
||
## 相关文件清单
|
||
|
||
### 修改的文件
|
||
- `internal/engine/memory_store.go` - 添加 Initialize 方法
|
||
- `internal/database/base.go` - 添加 ListCollections 基础方法
|
||
- `internal/database/sqlite/adapter.go` - SQLite 实现
|
||
- `internal/database/postgres/adapter.go` - PostgreSQL 实现
|
||
- `internal/database/dm8/adapter.go` - DM8 实现
|
||
- `cmd/server/main.go` - 启动时调用初始化
|
||
|
||
### 新增的文件
|
||
- `test_reload.sh` - 自动化测试脚本
|
||
- `RELOAD_FIX.md` - 本文档
|
||
|
||
## 后续优化建议
|
||
|
||
1. **增量加载**:对于大数据量场景,可以考虑分页加载
|
||
2. **懒加载**:只在第一次访问集合时才从数据库加载
|
||
3. **并发加载**:并行加载多个集合以提高启动速度
|
||
4. **加载进度监控**:添加更详细的进度日志和指标
|