「$487.32」
洛基看著茶室投影螢幕上的數字,這是他模擬系統這個月的 DynamoDB 儲存成本。
大師站在螢幕旁,沒有說話,等著洛基自己發現問題。
洛基切換到資料統計:
他皺起眉頭:「已結束的活動是活躍活動的 80 倍...大量過期資料佔用儲存空間,增加不必要的成本。」
「發現問題了?」大師問。
「我應該定期刪除這些過期資料,」洛基說,「寫個 Lambda 函數,每天跑一次批次刪除?」
「Hippo,麻煩你計算一下洛基的方案。」大師說。
白板上瞬間顯示結果,Hippo 說:「沒那麼菜的菜鳥的方案是這樣。」
Lambda 批次刪除方案:
- Scan 找出過期資料:100,000 RCU
- Delete 刪除:100,000 WCU
- Lambda 執行成本:每月 $10
- 總成本:約 $30/月
但你還需要:
- 寫刪除邏輯
- 處理錯誤重試
- 監控刪除狀態
- 維護程式碼
洛基看著這個方案,覺得有點複雜。
「有沒有更簡單的方法?」他問。
大師微笑,在白板上寫下兩個字:「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 提供的免費服務。」
大師展示第一個實例:
// 場景:使用者 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 不是即時刪除,」大師解釋,「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 負責最終的清理,但階段轉換需要應用層邏輯。」
大師繼續:「記得第 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 前:
- 總資料量:約 1,800,000 筆(大部分是過期資料)
- 儲存成本:$487/月
使用 TTL 後(設定自動清理):
- 活躍資料:約 156,200 筆
- 已結束活動:保留 30 天
- Session:保留 24 小時
- 通知:保留 7 天
- 儲存成本:約 $50/月
節省:約 90%
TTL 實施成本:$0(不消耗 WCU、無需 Lambda、完全自動化)
洛基看著這個對比,驚訝:「節省接近 90% 的儲存成本,而且完全不需要額外開發。」
「這就是 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 策略
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 年),程式碼範例為確保正確執行,使用對應的西元年份。