進階閱讀時間 3 分鐘

不停機的 DynamoDB 遷移

從 SQL 過來,一次遷移是一個 ALTER TABLE,它在重寫每一行的同時鎖住那張表。DynamoDB 沒有模式可改 —— 項是無模式的,所以新增一個屬性或一種新實體型別是免費的。

難的部分是新資料必須服務的那個訪問模式,以及在不做一次停止世界的重寫的前提下,重塑活資料去服務它。

如何在不停機的情況下遷移 DynamoDB 表?

DynamoDB 沒有 ALTER TABLE,所以遷移永遠不會鎖表。你透過 UpdateTable 線上新增屬性、新鍵形態或新 ,然後增量地重塑活資料:在讀取時惰性回填舊項,或透過受控的掃蕩來回填,並在過渡期內雙寫兩種格式。不存在一次旗幟日切換。

  • 沒有 ALTER TABLE 項是無模式的。一次”遷移”意味著新增屬性、一種新鍵形態,或一個新索引 —— 絕非重寫一組固定的列。
  • 新寫入容易;舊項是問題。 現有的行不攜帶新屬性,所以任何新索引或查詢都會悄悄漏掉它們,直到你回填。
  • 線上新增索引,惰性回填。 UpdateTable 在一張活表上構建一個 GSI;在讀取時回填舊項(惰性),或者用一次受控的掃蕩 —— 絕非一次旗幟日切換。
  • 在過渡期內雙寫。 當兩種形態共存時,把舊格式和新格式一起寫,這樣兩條讀取路徑都不會變陳舊。

把它框定成一個訪問模式,而非一列

假設你在一張表上營運一款 SaaS 工作區產品。項用 PK = "WS#<id>",而 SK 按實體過載:

PKSKattributes
WS#a91METAname, tier
WS#a91DOC#2026-04-01#x7title, author, body
WS#a91DOC#2026-04-02#k2title, author, body

現在產品想要文件上的評論,外加一個新讀取:“按時間倒序列出某個成員跨工作區寫下的每一條評論。” 最後那一句就是遷移。單單一種新實體型別微不足道;服務一個當前鍵回答不了的查詢才是真正的活。

先新增新實體型別

評論無非是同一個分割區裡的新項 —— 沒有遷移儀式,沒有新表:

PKSKattributes
WS#a91DOC#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,並把新屬性寫在新的評論項上:

GSI1PKGSI1SK
MEMBER#u442026-04-02T09:15:00Z

Query GSI1 WHERE GSI1PK = "MEMBER#u44"ScanIndexForward = false 給出每個成員的最新優先評論。

線上構建那個索引

UpdateTable 把一個 GSI 加到一張活表上而不停機。DynamoDB 在後臺把現有項回填進索引;索引報告 CREATING/正在回填,直到完成,然後翻為 ACTIVE管理 GSI)。

UpdateTable:新增 GSI1索引狀態:CREATING回填現有項狀態:ACTIVE查詢 GSI1 安全

這裡有兩個陷阱。第一,AWS 警告新增一個 GSI 可能限流基表寫入,如果新鍵分佈不均 —— 在一個低流量視窗新增它,並盯住 CloudWatch。第二,即便在它變成 ACTIVE 之後,索引仍然是最終一致的;一次寫入可能片刻之間在 GSI 上不可見。參見為什麼 GSI 是最終一致的

回填舊項

GSI 只索引擁有 GSI1PK/GSI1SK 的項。你那些遷移前的評論 —— 在那個屬性存在之前寫下的 —— 永遠不出現,哪怕回填完成之後。線上 GSI 回填複製現有項,但它沒法發明那些項上沒有的屬性。你得把那些值加上去。

兩種策略:

策略它如何工作何時使用
惰性在讀取一箇舊項時,把新屬性寫回去舊項被頻繁讀取;把成本細水長流地攤開
掃蕩一次分頁的 Scan 把每一箇舊項更新一遍你需要 GSI 在某個截止期前完整

對掃蕩,用 Scan 翻頁,並對每一條舊評論用一次條件 UpdateItem 加上索引屬性,好讓你永不覆蓋一次並行寫入。

那個條件守護在屬性尚不存在上。用 DynamoDB 運算式構建器構建並複製那個精確的 ConditionExpressionUpdateExpression,而非手敲 attribute_not_exists(GSI1PK)

在過渡期內雙寫

在每一箇舊項都攜帶新屬性之前,兩種形態共存。寫入路徑必須在每一次寫入上填充新格式 —— 新評論,以及對一條舊評論的任何更新 —— 這樣那道缺口才只會縮小。

挑一個你能驗證的回填結束條件:掃蕩翻過了整張表,或者惰性路徑已經跑得夠久、未轉換的項按設計已是陳舊的。只有到那時你才移除舊讀取路徑。跳過這一步,就是一次遷移在一小部分查詢悄悄返回不完整結果的同時“完成”的方式。

在 DynoTable 中翻頁一張表,以在回填期間發現缺少新索引屬性的項。
在 DynoTable 中翻頁一張表,以在回填期間發現缺少新索引屬性的項。

陷阱

  • 新增屬性 ≠ 已回填。 一個新 GSI 對舊項一開始是空的。在你信任那個查詢之前先驗證覆蓋。
  • 就地改一個鍵不是遷移 —— 而是一次重寫。 你沒法變更一個項的 PK/SK;你在新鍵之下寫一個新項並刪除舊的。把它當作複製-再刪除來規劃,中間雙讀。
  • 沒有事務式切換。 沒有一個整張表翻轉的瞬間。把每一步都設計成在兩種形態都活著時都安全。

後續步驟

單表設計裡對新鍵和過載集合做合理性檢查,並透過翻頁那張活表來確認回填完整。試用 DynoTable,去瀏覽你的表、發現未回填的項,並對著你自己的資料執行那些條件更新。

已更新