iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0

「$487.32」

洛基看著茶室投影螢幕上的數字,這是他模擬系統這個月的 DynamoDB 儲存成本。

大師站在螢幕旁,沒有說話,等著洛基自己發現問題。

洛基切換到資料統計:

  • 活躍活動:1,200 個
  • 已結束活動:98,000 個
  • 使用者 session:500,000 筆(大部分已過期)
  • 歷史通知:1,200,000 筆

他皺起眉頭:「已結束的活動是活躍活動的 80 倍...大量過期資料佔用儲存空間,增加不必要的成本。」

「發現問題了?」大師問。

「我應該定期刪除這些過期資料,」洛基說,「寫個 Lambda 函數,每天跑一次批次刪除?」

「Hippo,麻煩你計算一下洛基的方案。」大師說。

白板上瞬間顯示結果,Hippo 說:「沒那麼菜的菜鳥的方案是這樣。」

Lambda 批次刪除方案:
- Scan 找出過期資料:100,000 RCU
- Delete 刪除:100,000 WCU
- Lambda 執行成本:每月 $10
- 總成本:約 $30/月

但你還需要:
- 寫刪除邏輯
- 處理錯誤重試
- 監控刪除狀態
- 維護程式碼

洛基看著這個方案,覺得有點複雜。

「有沒有更簡單的方法?」他問。

大師微笑,在白板上寫下兩個字:「TTL」


自動清理的魔法:TTL

大師解釋:「TTL,Time To Live。DynamoDB 的自動資料清理機制。」

大師飛快地在白板上畫出運作流程:

TTL 的運作方式:

1. 你在項目中設定一個數字屬性(Unix timestamp)
   {
     PK: 'SESSION#abc123',
     userId: 'alice',
     ttl: 1710590400  // 2210-03-16 20:00:00
   }

2. 啟用 TTL 功能,指定哪個屬性是過期時間
   表設定:TTL attribute = "ttl"

3. DynamoDB 背景程序定期掃描
   ├─ 檢查 ttl < 當前時間
   └─ 自動刪除過期項目

4. 完全免費
   ├─ 不消耗 RCU
   ├─ 不消耗 WCU
   └─ 不需要寫程式

洛基驚訝:「刪除不消耗 WCU?」

「對,」大師說,「這是 DynamoDB 提供的免費服務。」


TTL 的基本使用

大師展示第一個實例:

// 場景:使用者 session,24 小時後自動過期

const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand } = require("@aws-sdk/lib-dynamodb");
const { UpdateTimeToLiveCommand } = require("@aws-sdk/client-dynamodb");

const client = new DynamoDBClient({ region: "us-east-1" });
const docClient = DynamoDBDocumentClient.from(client);

// 建立 session,設定 24 小時後過期
async function createSession(userId, sessionData) {
  const now = Math.floor(Date.now() / 1000); // Unix timestamp(秒)
  const ttl = now + 24 * 60 * 60; // 24 小時後

  await docClient.send(
    new PutCommand({
      TableName: "UserSessions",
      Item: {
        PK: `SESSION#${generateId()}`,
        SK: "METADATA",
        userId: userId,
        data: sessionData,
        createdAt: now,
        ttl: ttl, // TTL 屬性
      },
    })
  );
}

// 啟用 TTL(在表設定中執行一次)
async function enableTTL() {
  await client.send(
    new UpdateTimeToLiveCommand({
      TableName: "UserSessions",
      TimeToLiveSpecification: {
        Enabled: true,
        AttributeName: "ttl", // 指定 TTL 屬性名稱
      },
    })
  );
}

「重點是什麼?」大師問。

洛基分析:「TTL 屬性必須是 Unix timestamp,而且單位是秒。」

「正確,」大師說,「這是最常見的錯誤——用毫秒會讓項目永遠不會過期。」

他展示錯誤範例:

// ❌ 錯誤:使用毫秒
const wrongTTL = {
  ttl: Date.now(), // 毫秒!會變成西元 51790 年
};

// ❌ 錯誤:使用 ISO 字串
const wrongTTL2 = {
  ttl: "2210-03-15T10:00:00Z", // 字串!DynamoDB 無法處理
};

// ✓ 正確:Unix timestamp(秒)
const correctTTL = {
  ttl: Math.floor(Date.now() / 1000) + 86400, // 當前時間 + 24 小時(秒)
};

TTL 的延遲特性

大師繼續:「TTL 有個重要特性——延遲。」

洛基困惑:「延遲?」

「TTL 不是即時刪除,」大師解釋,「DynamoDB 保證在 48 小時內刪除過期項目,通常是幾分鐘到數小時。」

他在白板上畫時間軸:

TTL 刪除時間軸:

10:00 - 項目建立,ttl = 11:00
11:00 - 項目過期(ttl 時間到達)
11:05 - DynamoDB 掃描發現過期
11:10 - 項目被刪除

實際延遲:10 分鐘(可能更短或更長)
保證時間:48 小時內必定刪除

洛基問:「這個延遲會造成問題嗎?」

「取決於你的需求,」大師說,「我們來看幾個場景。」

場景 延遲影響 處理方式
Session 管理 可接受 應用層檢查 ttl < 當前時間,過期視為不存在
限時優惠 不可接受 應用層檢查 endTime,TTL 只用於清理儲存
歷史日誌 完全可接受 直接使用 TTL,不需額外檢查

「所以,」洛基總結,「TTL 適合用於資料清理,不適合用於業務邏輯的時效控制。」

「完全正確,」大師點頭。


多階段資料生命週期設計

大師提出一個更複雜的場景:

「星際活動結束後,資料生命週期分三個階段:」

階段 1:活躍期(0-7 天)
- 使用者可以查看、評論
- 資料在主表

階段 2:歷史期(7-90 天)
- 使用者可以查看,不能評論
- 資料仍在主表,但標記為 HISTORICAL

階段 3:歸檔期(90 天後)
- 資料被刪除或移至 S3
- 使用 TTL 自動清理

他展示設計:

// 活動資料結構
const eventLifecycle = {
  PK: "EVENT#SY210-03-15-001",
  SK: "METADATA",
  eventName: "火星科學大會",
  status: "ACTIVE", // ACTIVE → COMPLETED → HISTORICAL
  endTime: 1710432000, // 活動結束時間(Unix timestamp)

  // 生命週期控制
  historicalTime: null, // 進入歷史期的時間(endTime + 7 天)
  ttl: null, // 刪除時間(endTime + 90 天)
};

// 活動結束時的處理
async function onEventEnd(eventId) {
  const now = Math.floor(Date.now() / 1000);
  const sevenDays = 7 * 24 * 60 * 60;
  const ninetyDays = 90 * 24 * 60 * 60;

  await docClient.send(
    new UpdateCommand({
      TableName: "Events",
      Key: { PK: eventId, SK: "METADATA" },
      UpdateExpression: `
      SET #status = :completed,
          historicalTime = :histTime,
          ttl = :ttlTime
    `,
      ExpressionAttributeNames: {
        "#status": "status",
      },
      ExpressionAttributeValues: {
        ":completed": "COMPLETED",
        ":histTime": now + sevenDays,
        ":ttlTime": now + ninetyDays,
      },
    })
  );
}

// 查詢時判斷階段
function getEventStage(event) {
  const now = Math.floor(Date.now() / 1000);

  if (event.status === "ACTIVE") {
    return "ACTIVE"; // 活躍期
  }

  if (now < event.historicalTime) {
    return "COMPLETED"; // 剛結束,仍可互動
  }

  return "HISTORICAL"; // 歷史期,唯讀
}

洛基看著這個設計:「所以 TTL 只是生命週期管理的一部分,不是全部。」

「對,」大師說,「TTL 負責最終的清理,但階段轉換需要應用層邏輯。」


TTL 與稀疏索引的配合

大師繼續:「記得第 25 天學的稀疏索引嗎?」

洛基點頭:「只為部分項目建立索引。」

「TTL 可以配合稀疏索引使用,」大師展示設計:

場景:「最近活動」列表(只顯示 7 天內的活動)

方案 做法 問題/優點
方案 A 掃描所有活動,用 FilterExpression 過濾 消耗大量 RCU,效能差
方案 B 稀疏索引 + TTL 只掃描最近活動,TTL 自動清理,成本低
// 方案 B 實作:分離 marker 項目
const event = {
  PK: "EVENT#SY210-03-15-001",
  SK: "METADATA",
  eventName: "火星科學大會",
  // 主項目無 TTL
};

const recentMarker = {
  PK: "EVENT#SY210-03-15-001",
  SK: "RECENT_MARKER",
  GSI_RecentPK: "RECENT",
  ttl: now + 7 * 24 * 60 * 60, // 7 天後自動刪除
};

洛基理解了:「用不同的 SK 分離資料,主項目保留,只有 marker 被 TTL 刪除。」

「這就是 TTL 的進階應用,」大師說。


TTL 的成本優勢

大師回到開場時的成本問題:

「現在我們用 TTL 重新設計資料清理策略。」

使用 TTL 前:
- 總資料量:約 1,800,000 筆(大部分是過期資料)
- 儲存成本:$487/月

使用 TTL 後(設定自動清理):
- 活躍資料:約 156,200 筆
- 已結束活動:保留 30 天
- Session:保留 24 小時
- 通知:保留 7 天
- 儲存成本:約 $50/月

節省:約 90%
TTL 實施成本:$0(不消耗 WCU、無需 Lambda、完全自動化)

洛基看著這個對比,驚訝:「節省接近 90% 的儲存成本,而且完全不需要額外開發。」

「這就是 TTL 的價值,」大師說,「簡單、免費、自動化。」


TTL 的注意事項

大師在白板上列出使用 TTL 時的重要提醒:

TTL 使用注意事項:

1. 延遲特性
   ✓ 用於資料清理
   ✗ 不能用於即時業務邏輯

2. 不可逆
   ✓ 刪除前確認資料不再需要
   ✗ 沒有「軟刪除」或「回收站」

3. Unix timestamp 單位
   ✓ 必須是秒(不是毫秒)
   ✗ 用毫秒會導致項目永不過期

4. 刪除是整個項目
   ✓ 分離需要不同生命週期的資料
   ✗ 不能只刪除部分屬性

5. Streams 事件
   ✓ TTL 刪除會觸發 REMOVE 事件
   ✓ 可以用於歸檔或通知

6. GSI 的 TTL 屬性
   ✓ GSI 可以投影 TTL 屬性
   ✓ 用於應用層過濾判斷

7. 監控
   ✓ CloudWatch 有 TTL 刪除指標
   ✓ 可監控刪除數量和速率

洛基記下這些要點。

大師補充:「特別是第 5 點——TTL 刪除會觸發 Streams 事件。這個我們第 27 天會深入學習,可以用於在刪除前做資料歸檔。」


實戰:完整的 TTL 設計模式

大師給出最後一個完整範例:

// 星際活動系統的完整 TTL 策略

const { UpdateCommand } = require("@aws-sdk/lib-dynamodb");

// 1. Session:寫入時設定 TTL,讀取時檢查過期
async function createSession(userId, data) {
  const ttl = Math.floor(Date.now() / 1000) + 24 * 60 * 60;
  await docClient.send(
    new PutCommand({
      TableName: "Sessions",
      Item: { PK: `SESSION#${id}`, SK: "METADATA", userId, data, ttl },
    })
  );
}

// 2. 活動生命週期:結束時設定 30 天 TTL
async function endEvent(eventId) {
  const ttl = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60;
  await docClient.send(
    new UpdateCommand({
      TableName: "Events",
      Key: { PK: eventId, SK: "METADATA" },
      UpdateExpression: "SET #status = :completed, ttl = :ttl",
      ExpressionAttributeNames: { "#status": "status" },
      ExpressionAttributeValues: { ":completed": "COMPLETED", ":ttl": ttl },
    })
  );
}

// 3. 通知:7 天 TTL
// 4. 臨時資料(驗證碼):15 分鐘 TTL

洛基看著這個完整設計:「TTL 適用的場景很廣——從 session 到通知,到臨時資料。」

「對,」大師說,「任何有時效性的資料,都應該考慮用 TTL。這是大規模系統的標準實踐。」


洛基回到模擬系統,開始為各個資料表啟用 TTL。

Session 表:24 小時過期。
通知表:7 天過期。
已結束活動:30 天過期。

設定很簡單,就是指定一個屬性名稱,DynamoDB 會自動處理剩下的事。

他再次檢視投影螢幕上的成本預估——從 $487 降到約 $50,90% 的節省。

大師站在一旁:「TTL 刪除會觸發 DynamoDB Streams 的事件,可以在資料被刪除前做最後處理,例如歸檔到 S3。」

洛基抬起頭:「Streams?像是資料庫的 trigger?」

「類似,但更強大,」大師說,「它可以串接 Lambda,捕捉所有資料變更——包括 TTL 刪除。這是明天的主題。」

洛基點點頭,繼續調整 TTL 設定。他發現自己思考方式開始改變:不再是「資料要不要刪」,而是「資料的生命週期是什麼」。

設計時就規劃好資料何時該消失,而不是等問題發生再來清理。

這才是系統設計該有的樣子。


時間設定說明:故事中使用星際曆(SY210 = 西元 2210 年),程式碼範例為確保正確執行,使用對應的西元年份。


上一篇
Day 25:索引策略完整設計
下一篇
Day 27:Streams 與事件驅動架構
系列文
DynamoDB銀河傳說首部曲-打造宇宙都打不倒的高效服務30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言