不停機的 DynamoDB 遷移
從 SQL 過來,一次遷移是一個 ALTER TABLE,它在重寫每一行的同時鎖住那張表。DynamoDB 沒有模式可改 —— 項是無模式的,所以新增一個屬性或一種新實體型別是免費的。
難的部分是新資料必須服務的那個訪問模式,以及在不做一次停止世界的重寫的前提下,重塑活資料去服務它。
如何在不停機的情況下遷移 DynamoDB 表?
DynamoDB 沒有 ALTER TABLE,所以遷移永遠不會鎖表。你透過 UpdateTable 線上新增屬性、新鍵形態或新 ,然後增量地重塑活資料:在讀取時惰性回填舊項,或透過受控的掃蕩來回填,並在過渡期內雙寫兩種格式。不存在一次旗幟日切換。
- 沒有
ALTER TABLE。 項是無模式的。一次”遷移”意味著新增屬性、一種新鍵形態,或一個新索引 —— 絕非重寫一組固定的列。 - 新寫入容易;舊項是問題。 現有的行不攜帶新屬性,所以任何新索引或查詢都會悄悄漏掉它們,直到你回填。
- 線上新增索引,惰性回填。
UpdateTable在一張活表上構建一個 GSI;在讀取時回填舊項(惰性),或者用一次受控的掃蕩 —— 絕非一次旗幟日切換。 - 在過渡期內雙寫。 當兩種形態共存時,把舊格式和新格式一起寫,這樣兩條讀取路徑都不會變陳舊。
把它框定成一個訪問模式,而非一列
假設你在一張表上營運一款 SaaS 工作區產品。項用 PK = "WS#<id>",而 SK 按實體過載:
| PK | SK | attributes |
|---|---|---|
| WS#a91 | META | name, tier |
| WS#a91 | DOC#2026-04-01#x7 | title, author, body |
| WS#a91 | DOC#2026-04-02#k2 | title, author, body |
現在產品想要文件上的評論,外加一個新讀取:“按時間倒序列出某個成員跨工作區寫下的每一條評論。” 最後那一句就是遷移。單單一種新實體型別微不足道;服務一個當前鍵回答不了的查詢才是真正的活。
先新增新實體型別
評論無非是同一個分割區裡的新項 —— 沒有遷移儀式,沒有新表:
| PK | SK | attributes |
|---|---|---|
| WS#a91 | DOC#2026-04-01#x7#CMT#01HZ... | author, text, createdAt |
一次 Query PK = "WS#a91" 配 SK begins_with "DOC#2026-04-01#x7#CMT#" 就已經列出一篇文件的評論了。現有文件原封不動。這一半在第一天就上線 —— 至於為什麼同一個分割區同時容納兩者,參見項集合與過載鍵。
新查詢需要一個 GSI
“某個成員的所有評論,最新優先”沒法由基表服務 —— memberId 既不是 PK 也不是某個 SK 字首。那是一個新索引,而正確地選它本身是一個決定:參見 GSI 對比 LSI(一個 LSI 必須在建表時就存在,所以對一張活表上的遷移來說,GSI 是你唯一的選項)。
新增一個通用的 GSI1,並把新屬性寫在新的評論項上:
| GSI1PK | GSI1SK |
|---|---|
| MEMBER#u44 | 2026-04-02T09:15:00Z |
Query GSI1 WHERE GSI1PK = "MEMBER#u44" 配 ScanIndexForward = false 給出每個成員的最新優先評論。
線上構建那個索引
UpdateTable 把一個 GSI 加到一張活表上而不停機。DynamoDB 在後臺把現有項回填進索引;索引報告 CREATING/正在回填,直到完成,然後翻為 ACTIVE(管理 GSI)。
這裡有兩個陷阱。第一,AWS 警告新增一個 GSI 可能限流基表寫入,如果新鍵分佈不均 —— 在一個低流量視窗新增它,並盯住 CloudWatch。第二,即便在它變成 ACTIVE 之後,索引仍然是最終一致的;一次寫入可能片刻之間在 GSI 上不可見。參見為什麼 GSI 是最終一致的。
回填舊項
GSI 只索引擁有 GSI1PK/GSI1SK 的項。你那些遷移前的評論 —— 在那個屬性存在之前寫下的 —— 永遠不出現,哪怕回填完成之後。線上 GSI 回填複製現有項,但它沒法發明那些項上沒有的屬性。你得把那些值加上去。
兩種策略:
| 策略 | 它如何工作 | 何時使用 |
|---|---|---|
| 惰性 | 在讀取一箇舊項時,把新屬性寫回去 | 舊項被頻繁讀取;把成本細水長流地攤開 |
| 掃蕩 | 一次分頁的 Scan 把每一箇舊項更新一遍 | 你需要 GSI 在某個截止期前完整 |
對掃蕩,用 Scan 翻頁,並對每一條舊評論用一次條件 UpdateItem 加上索引屬性,好讓你永不覆蓋一次並行寫入。
那個條件守護在屬性尚不存在上。用 DynamoDB 運算式構建器構建並複製那個精確的 ConditionExpression 和 UpdateExpression,而非手敲 attribute_not_exists(GSI1PK)。
在過渡期內雙寫
在每一箇舊項都攜帶新屬性之前,兩種形態共存。寫入路徑必須在每一次寫入上填充新格式 —— 新評論,以及對一條舊評論的任何更新 —— 這樣那道缺口才只會縮小。
挑一個你能驗證的回填結束條件:掃蕩翻過了整張表,或者惰性路徑已經跑得夠久、未轉換的項按設計已是陳舊的。只有到那時你才移除舊讀取路徑。跳過這一步,就是一次遷移在一小部分查詢悄悄返回不完整結果的同時“完成”的方式。

陷阱
- 新增屬性 ≠ 已回填。 一個新 GSI 對舊項一開始是空的。在你信任那個查詢之前先驗證覆蓋。
- 就地改一個鍵不是遷移 —— 而是一次重寫。 你沒法變更一個項的
PK/SK;你在新鍵之下寫一個新項並刪除舊的。把它當作複製-再刪除來規劃,中間雙讀。 - 沒有事務式切換。 沒有一個整張表翻轉的瞬間。把每一步都設計成在兩種形態都活著時都安全。
後續步驟
在單表設計裡對新鍵和過載集合做合理性檢查,並透過翻頁那張活表來確認回填完整。試用 DynoTable,去瀏覽你的表、發現未回填的項,並對著你自己的資料執行那些條件更新。


