# 壓縮與垃圾回收

本頁說明 ToriiDB 如何把資料庫重寫成新的 snapshot、移除被取代的檔案，以及 `Store.Close()` 關閉時做了什麼。

## 壓縮

`compact()` 只在持有資料庫寫鎖時執行（來自寫入路徑或 `Store.Close()`）：

| 步驟 | 動作 |
|---|---|
| 1 | 重播失敗時以 `loadErr` 拒絕，避免不完整的 map 覆蓋良好的 snapshot |
| 2 | 無資料且資料庫目錄不存在時略過 |
| 3 | 將未過期項目序列化到 `N.snap.tmp`，執行 `write` + `fsync` + `close` |
| 4 | `os.Rename` 為 `N.snap`，再對目錄 `fsync` |
| 5 | 開啟 `N+1.log` 並換上，之後才關閉舊 log |
| 6 | 於背景執行垃圾回收 |

`N` 是目前的 log 編號。直接使用 `os.Rename`，因為 go-pkg 的搬移函式在跨裝置時會退化為複製 + 刪除，不是原子操作。先開新 log 再關舊 log，資料庫就不會出現 log handle 為 nil 的空窗。

序列化以 `CreatedAt` 作為時間戳且不帶 `UpdatedAt`，因此從 snapshot 重啟後，項目不會有更新時間。

## 垃圾回收

壓縮成 snapshot `N` 後，會刪除 `< N-1` 的 snapshot 與 `<= N-1` 的 log。前一世代（`N-1.snap` + `N.log`）保留在磁碟上，作為重播的退回來源。

`Store.Close()` 以 `sync.Once` 保證冪等，並對每個已載入資料庫：

1. 等待背景向量工作。
2. 停止過期計時器。
3. 在寫鎖下壓縮。
4. **僅在壓縮成功時**同步執行垃圾回收，因為背景 goroutine 在 process 結束前不會被排程。
5. 關閉 log 檔。

回傳第一個壓縮錯誤。壓縮失敗時略過垃圾回收，可避免一個異常的高編號 snapshot 導致有效的舊檔案被刪除。

## 相關頁面

- [檔案與寫入](/zh/persistence)
- [重播與復原](/zh/persistence-replay)
- [固定限制](/zh/configuration-limits)
