iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0

「早安,大師。」洛基坐下後說,「昨天您提到了 update-item,說它能解決 put-item 在部分更新時的問題。我一直在想,在真實的星際活動報名中,可能有上萬人同時操作...」

洛基對於這個功能相當在意,畢竟 SQL 的世界他已經遠離了一些,但是 DynamoDB 又好像還沒跨進去。今天的 update-item 又會開啟它哪些新的視野呢?

「如果說 Put 是重新粉刷整面牆,那 Update 就是在牆上精準地掛一幅畫。你只改變你想改的,其他保持不變。而且,它能做到一些 Put 永遠做不到的事情——原子性操作。」大師今天氣定神閒地開始他今天的話題。

「原子性操作?聽起來非常的玄。」

「接著聽下去就不玄了,而且我說,它非常的優雅呢。」大師呵呵笑著。

Update 的優雅

「讓我們從昨天的例子繼續。」大師說,「記得那個火星防禦研討會嗎?讓我們用 update-item 來更新報名人數。」

洛基查看了昨天的資料,確認 registered 目前是 1。

「首先,最基本的 update 操作,Hippo 幫我們更新一下人數」大師喚起了 Hippo,Hippo 也馬上在螢幕上顯示出指令。

aws dynamodb update-item \
  --table-name IntergalacticEvents \
  --key '{"PK": {"S": "EVENT#MARS-2024"}}' \
  --update-expression "SET registered = :val" \
  --expression-attribute-values '{":val": {"N": "2"}}' \
  --endpoint-url http://localhost:8000

洛基注意到:「Hippo 只指定了要更新的 registered,沒有提供其他欄位的資料。」

「正是如此。」大師說,「現在查詢這筆資料看看。」

洛基執行 get-item,發現所有其他欄位都還在:name、date、capacity、location、speaker、topic、duration、status——一個都沒少,只有 registered 變成了 2。

「看到差異了嗎?」大師問。

洛基:「使用 update-item 只要告訴它我要改什麼!不需要先讀取,不需要管理整個 item 結構,更不會意外覆蓋其他欄位。這樣就方便很多,比較像是我們平常對於更新資料的認知。」

大師點頭,「但這只是開始。Update 真正的威力在於它的原子性操作。」

原子操作初探

「讓我們先看看 Update 最基本的原子操作。」大師在白板上寫下 UpdateExpression 的四個動作:

SET    - 設定屬性值
ADD    - 數值增減(原子計數器)
REMOVE - 移除屬性
DELETE - 從集合中刪除元素

「我們來試試 ADD 操作。」大師說,「假設現在有三個人同時報名。」

# 第一個人報名
aws dynamodb update-item \
  --table-name IntergalacticEvents \
  --key '{"PK": {"S": "EVENT#MARS-2024"}}' \
  --update-expression "ADD registered :inc" \
  --expression-attribute-values '{":inc": {"N": "1"}}' \
  --endpoint-url http://localhost:8000

大師解釋,「即使三個請求同時到達,DynamoDB 會自動處理併發。每個 ADD 操作都是原子的,不會互相覆蓋。」

Hippo 這時插話:「讓我模擬一下併發場景!」他同時送出了三個並行的 ADD 操作。

洛基查詢結果:registered 準確地從 2 變成了 5。

「沒有鎖,沒有重試,沒有資料遺失。」洛基讚嘆,「但是...我有點困惑。在 SQL 的世界裡,這種併發問題通常要靠鎖或交易來處理,為什麼 DynamoDB 反而說不需要?」

原子性的真正威力

大師微笑:「好問題。讓我問你,如果你是一個星際中央銀行,全星際分行的交易都要回到總行處理,會發生什麼事?」

「會塞車,」洛基立刻回答,「所有交易都要排隊等待。」

Hippo 搶著說:「沒錯。這就是傳統關聯式資料庫的處境。讓我給你看一張圖」

https://ithelp.ithome.com.tw/upload/images/20250918/201788135ELbq1xxgp.jpg

大師指著圖解釋:「MySQL 這類資料庫,每筆資料最終都要在一個節點上保持一致性。當你要更新一個值時,必須鎖定它,改完後釋放,其他人才能繼續。這就像只有一支筆,大家輪流用。」

「但 DynamoDB 不同。」大師繼續,「每個 item 根據主鍵被分配到特定的分區,而每個分區內部有自己的『原子計數器』機制。想像每個分區是一個獨立的收銀機,各自管理自己的帳本。」

洛基開始理解了:「所以當我對 EVENT#MARS-2024 做 ADD 操作時...」

「正是!這個 item 只存在於一個特定分區中。」大師說,「DynamoDB 在分區層級實現了一個精巧的機制:每個 ADD 操作會被轉換成一個不可分割的指令。就像收銀機的『計數按鈕』,按一次就是加一,不存在『讀取當前值、加一、寫回』這種多步驟操作。」

分散式架構的挑戰與解答

「但等等,」洛基想到另一個問題,「DynamoDB 不是有多個副本嗎?怎麼保證它們都是對的?」

大師露出讚許的表情:「關鍵在於『最終一致性』和『強一致性讀取』的設計。當你執行 ADD registered :1 時」

大師在白板上寫著

1. 請求首先到達主副本(Leader)
2. 主副本執行原子操作,立即返回結果
3. 變更異步複製到其他副本

「如果你需要絕對準確的值,使用強一致性讀取,它會從主副本讀取。但即使是最終一致性,因為每個操作都是原子的,副本之間只是時間差,不會有數值錯誤。

DynamoDB 使用 Quorum 機制確保資料一致性:寫入時至少要有 2 個副本確認,讀取強一致性時也會檢查多個副本,確保拿到最新的值。」

洛基皺起眉頭:「等等,大師。您說每個分區有個主副本處理 ADD 操作,那在高併發時,這個主副本不也會成為瓶頸嗎?這和 MySQL 的單點處理有什麼不同?」

大師眼睛一亮:「非常銳利的觀察!來,給你看真正的差異。」他在白板繼續寫著:

MySQL 處理 UPDATE count = count + 1:
1. 讀取當前值 (需要 I/O)
2. 鎖定這一行
3. 在記憶體計算 new_value = old_value + 1
4. 寫入新值 (需要 I/O)
5. 寫入 transaction log
6. 釋放鎖
總時間:假設 5ms

DynamoDB 處理 ADD count :1:
1. 直接追加操作到 log (順序寫入)
2. 返回成功
總時間:假設 0.5ms

「看到關鍵了嗎?」大師問。

洛基仔細看著:「MySQL 要做 6 步,DynamoDB 只要 2 步?」

「不只是步驟數量,更重要的是『本質』的不同。」大師解釋,「MySQL 必須執行 Read-Modify-Write 循環,每個請求都要等待完整的 I/O 往返。但 DynamoDB 的 ADD 是一個『單向指令』。」

遊戲規則的改變

大師繼續說道:「DynamoDB 的主副本不是在處理『計算』,而是在處理『日誌追加』。它收到 ADD 指令後,只是把『+1』這個操作追加到操作日誌中,這是純粹的順序寫入,極其快速。實際的計算可以異步進行。」

Hippo 這時在白板上展示了兩種高併發場景的對比:

高併發場景對比:

MySQL (1000 個 +1 請求):
請求1: [讀取][計算][寫入] ----------> 5ms
請求2:       等待...    [讀取][計算][寫入] --> 5ms
請求3:              等待...         [讀取][計算][寫入]
總計:5000ms (串行處理)

DynamoDB (1000 個 +1 請求):
請求1: [追加log] -> 0.5ms
請求2: [追加log] -> 0.5ms
請求3: [追加log] -> 0.5ms
...同時處理...
總計:可能只需 50-100ms (批次處理 + 管線化)

「而且,」大師補充,「DynamoDB 還有個絕招:它可以將多個 ADD 操作合併。如果短時間內收到 100 個 +1 請求,它可以在日誌中記錄一個 +100,而不是 100 個 +1。」

洛基驚訝:「所以根本不是在同一個層次的競爭!」

「正是如此。」大師說,「MySQL 在應用層面處理業務邏輯,每個操作都要完整的資料庫事務。DynamoDB 在儲存引擎層面就設計了原子操作,把 ADD 當作基礎指令,就像 CPU 的原子指令一樣。」

「還有一點,」大師指向白板,「分區的威力:」

DynamoDB 的擴展性:
- EVENT#MARS-2024 的 +1 操作 -> 分區 A 的 Leader
- EVENT#VENUS-2024 的 +1 操作 -> 分區 B 的 Leader  
- EVENT#EARTH-2024 的 +1 操作 -> 分區 C 的 Leader
三個完全獨立,平行處理!

MySQL 的侷限:
- 所有 UPDATE 都要經過同一個主節點
- 即使分表,在單一交易中仍是串行

「所以 DynamoDB 的『優雅』在於,」洛基總結,「它不是把傳統資料庫的鎖機制做得更快,而是從根本上改變了遊戲規則。用 append-only log 取代 read-modify-write,用分區隔離取代全局鎖定。」

「而且每個分區可以獨立擴展。」大師補充,「當某個熱門活動報名量暴增時,DynamoDB 可以自動分割該分區,增加更多處理能力,而不影響其他活動。」

「我終於明白了,」洛基說,「這不是優化,是革命。」

大師點頭:「現在你明白了。當你使用 ADD、DELETE 這些原子操作時,你其實是在利用 DynamoDB 分散式架構的精髓。每個操作都是一個自給自足的指令,不依賴外部狀態,所以可以在任何分區、任何時間、以任何順序執行,結果都是正確的。」

「而且即使在一個操作中組合多個動作,這個原子式操作的本質一樣不受影響。」大師展示:

aws dynamodb update-item \
  --table-name IntergalacticEvents \
  --key '{"PK": {"S": "EVENT#MARS-2024"}}' \
  --update-expression "SET #status = :status, lastModified = :time ADD registered :inc" \
  --expression-attribute-names '{"#status": "status"}' \
  --expression-attribute-values '{
    ":status": {"S": "ACTIVE"},
    ":time": {"S": "2024-03-20T10:00:00Z"},
    ":inc": {"N": "1"}
  }' \
  --endpoint-url http://localhost:8000

「一次請求完成三個更新:狀態改為 ACTIVE、更新時間戳、報名人數加一。」大師說。

「哇,這樣我完全不用再利用 Queue 或是一些錯開請求的方式來處理資料寫入了。不過...」洛基注意到一個細節:「為什麼 status 要用 #status?」

「好觀察。status 是 DynamoDB 的保留字,所以需要用 expression-attribute-names 來替換。」大師解釋,「這是一個小技巧,但很重要。」

Hippo 這時補充說:「也有人建議與其去查詢 DynamoDB 有哪些保留字,不如把所有使用到的 attribute name 都改成 # 開頭的表示法,雖然麻煩了一些,但可避免因撞到保留字而發生錯誤的問題。」

洛基點了點頭,「聽起來也是個務實的辦法,要記住哪些是保留字的確是不太可能。」

條件更新的智慧

「現在你理解了原子操作的威力,」大師說,「讓我們看看如何結合條件表達式,實現更精確的控制。比如,如何防止活動超額報名?」

洛基思考:「需要檢查當前報名人數是否小於上限...」

「沒錯。」大師寫下:

# 只有在未滿額時才允許報名
aws dynamodb update-item \
  --table-name IntergalacticEvents \
  --key '{"PK": {"S": "EVENT#MARS-2024"}}' \
  --update-expression "ADD registered :inc" \
  --condition-expression "registered < capacity" \
  --expression-attribute-values '{":inc": {"N": "1"}}' \
  --endpoint-url http://localhost:8000

「如果活動已滿,這個操作會失敗,返回 ConditionalCheckFailedException。」大師說,「這確保了資料的業務邏輯完整性。」

「另外,你還可以使用舊值來計算新值。」大師補充:

# 將報名費設為人數的10倍(動態計算)
aws dynamodb update-item \
  --table-name IntergalacticEvents \
  --key '{"PK": {"S": "EVENT#MARS-2024"}}' \
  --update-expression "SET fee = registered * :multiplier" \
  --expression-attribute-values '{":multiplier": {"N": "10"}}' \
  --endpoint-url http://localhost:8000

進階資料結構操作

「Update 還能操作複雜的資料結構。」大師建立了一個新的測試資料:

{
  "PK": {"S": "EVENT#ADVANCED-2024"},
  "speakers": {"L": [
    {"S": "諾斯克大師"},
    {"S": "戰神將軍"}
  ]},
  "tags": {"SS": ["防禦", "戰略", "星際"]},
  "metadata": {"M": {
    "level": {"S": "advanced"},
    "prerequisites": {"N": "3"}
  }}
}

「現在,我們來添加一個新的講者:」

# 在列表末尾添加新講者
aws dynamodb update-item \
  --table-name IntergalacticEvents \
  --key '{"PK": {"S": "EVENT#ADVANCED-2024"}}' \
  --update-expression "SET speakers = list_append(speakers, :new_speaker)" \
  --expression-attribute-values '{":new_speaker": {"L": [{"S": "洛基上尉"}]}}' \
  --endpoint-url http://localhost:8000

「甚至可以更新嵌套的屬性:」

# 更新 metadata 中的 level
aws dynamodb update-item \
  --table-name IntergalacticEvents \
  --key '{"PK": {"S": "EVENT#ADVANCED-2024"}}' \
  --update-expression "SET metadata.#level = :new_level" \
  --expression-attribute-names '{"#level": "level"}' \
  --expression-attribute-values '{":new_level": {"S": "expert"}}' \
  --endpoint-url http://localhost:8000

洛基驚訝:「這個使用彈性真是驚人。」

「這只是冰山一角。」大師說,「但記住,不要為了炫技而使用複雜功能。一般而言,選擇最簡單、最清晰的方案就是最好的方案。」

智慧的選擇

「現在,讓我們總結一下何時用 Put,何時用 Update。」大師在白板上畫了一個對比表:

Put-Item 適用場景:
✓ 建立全新的 item
✓ 需要完整替換(如狀態轉換)
✓ 冪等性寫入(如每日重置)
✓ 資料結構簡單且完整

Update-Item 適用場景:
✓ 部分欄位更新
✓ 原子計數器(庫存、計數)
✓ 併發更新場景
✓ 複雜資料結構操作
✓ 基於舊值計算新值

「Put 是畫家的畫布,每次都重新創作。Update 是雕刻家的鑿子,精準地修改細節。」大師總結。

「兩者都有其價值,關鍵是知道何時使用哪一個。」洛基理解了。

走向查詢

「這幾天我們學了 put-item 和 update-item,資料已經妥善存入 DynamoDB。」大師說,「但寫入是為了讀取。明天,我們要學習如何有效地查詢資料。 但今天,好好消化 Update 的概念,它會是你最常用的工具之一。」

洛基起身敬禮:「謝謝大師。Update 的原子性操作確實解決了我很多疑慮。」

走出工作室時,基回頭看了一眼白板上的圖表。Put 和 Update,簡單和精準,各有所用,他始理解 DynamoDB 的設計哲學了。

Hippo 的課外教學

UpdateExpression 完整語法

Hippo:「來,讓我教你 UpdateExpression 的完整武器庫!」

1. SET 操作

# 設定單一值
SET attribute = :value

# 設定多個值
SET attr1 = :val1, attr2 = :val2

# 使用函數
SET attribute = if_not_exists(attribute, :default)
SET counter = attribute + :increment
SET list = list_append(list, :new_items)

2. ADD 操作(只用於數字和集合)

# 數字增減
ADD counter :increment    # 可以是正數或負數

# 集合添加元素
ADD tags :new_tags       # 添加到 String Set

3. REMOVE 操作

# 移除屬性
REMOVE attribute

# 移除多個屬性
REMOVE attr1, attr2

# 移除列表元素(按索引)
REMOVE list[0]

4. DELETE 操作(只用於集合)

# 從集合中刪除特定元素
DELETE tags :tags_to_remove

實用函數

  1. if_not_exists(path, value)

    • 如果屬性不存在,使用提供的值
    • 如果存在,保持原值
    SET view_count = if_not_exists(view_count, :zero) + :increment
    
  2. list_append(list1, list2)

    • 合併兩個列表
    SET messages = list_append(messages, :new_messages)
    SET messages = list_append(:new_messages, messages)  # 前置
    

保留字處理

DynamoDB 有很多保留字,常見的包括:

  • status, name, count, data, date, day, group, language, last, level, list, location, message, method, name, number, option, order, password, path, resource, role, size, source, start, state, status, string, system, table, tag, text, time, timestamp, to, token, type, user, value, values, version, view

使用 expression-attribute-names 來處理:

--expression-attribute-names '{
  "#s": "status",
  "#n": "name",
  "#t": "time"
}'

效能小撇步

  1. 批次處理 vs 原子操作

    • 如果要更新多個相關聯的計數器,用一個 update-item 比多個請求更高效
  2. 條件表達式的使用

    • 善用條件表達式可以減少不必要的讀取操作
    • 但過於複雜的條件會影響效能
  3. 返回值的選擇

    • 只在需要時才要求返回值
    • NONE(預設)是最高效的
  4. 版本控制模式

    # 樂觀鎖定
    --update-expression "SET data = :new_data, version = version + :one" \
    --condition-expression "version = :current_version"
    

Hippo 的返回值深度解析

UpdateItem 返回值完整解析

// UpdateItem 返回值選項詳解
const returnValuesGuide = {
  NONE: {
    描述: "不返回任何值(預設)",
    使用場景: "當你不需要確認更新結果時",
    優點: "最高效,最少網路傳輸",
    範例回應: {
      // 只有 ConsumedCapacity 等元數據
    }
  },

  ALL_OLD: {
    描述: "返回更新前的完整項目",
    使用場景: "需要備份或審計舊值",
    優點: "可以知道被替換的完整資料",
    範例回應: {
      Attributes: {
        id: "USER#123",
        name: "舊名字",
        score: 100,
        updated: "2024-01-01"
      }
    }
  },

  ALL_NEW: {
    描述: "返回更新後的完整項目",
    使用場景: "需要立即使用更新後的資料",
    優點: "省去額外的 GetItem 操作",
    範例回應: {
      Attributes: {
        id: "USER#123",
        name: "新名字",
        score: 150,  // 更新了
        updated: "2024-07-15"  // 更新了
      }
    }
  },

  UPDATED_OLD: {
    描述: "只返回被更新屬性的舊值",
    使用場景: "只關心哪些欄位被改變",
    優點: "精確追蹤變更,減少傳輸量",
    範例回應: {
      Attributes: {
        // 只有被更新的屬性
        score: 100,
        updated: "2024-01-01"
      }
    }
  },

  UPDATED_NEW: {
    描述: "只返回被更新屬性的新值",
    使用場景: "確認更新是否成功應用",
    優點: "最小傳輸量,精確確認",
    範例回應: {
      Attributes: {
        // 只有被更新的屬性
        score: 150,
        updated: "2024-07-15"
      }
    }
  }
};

返回值選擇決策樹

// 如何選擇正確的返回值
function chooseReturnValue(scenario) {
  const decisionTree = {
    "需要舊值做備份?": "ALL_OLD",
    "需要立即使用更新後的完整資料?": "ALL_NEW",
    "只關心哪些欄位被改變?": "UPDATED_OLD",
    "只需確認更新的值?": "UPDATED_NEW",
    "純粹更新,不需要任何返回?": "NONE"
  };

  return decisionTree[scenario];
}

PutItem vs UpdateItem 返回值對比

// 關鍵差異對比
const comparisonChart = {
  PutItem: {
    支援選項: ["NONE", "ALL_OLD"],
    原因: "完全替換操作,新值已知",
    典型用法: "替換前備份"
  },

  UpdateItem: {
    支援選項: ["NONE", "ALL_OLD", "ALL_NEW", "UPDATED_OLD", "UPDATED_NEW"],
    原因: "部分更新,需要追蹤變化",
    典型用法: "精確控制和追蹤"
  }
};

Hippo 總結:「記住,返回值的選擇直接影響效能和成本。只在真正需要時才要求返回值,大部分情況下 NONE 就足夠了!」


上一篇
Day 3: put-item 的操作與限制與雷區
下一篇
Day 5:Query 與 Get 的差異
系列文
DynamoDB銀河傳說首部曲-打造宇宙都打不倒的高效服務6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言