Hippo 將螢幕呈現給洛基,畫面上滿是紅色的錯誤訊息:
ValidationException: One or more parameter values were invalid
ConditionalCheckFailedException: The conditional request failed
ProvisionedThroughputExceededException: Request rate is too high
ResourceNotFoundException: Requested resource not found
「我不知道這些錯誤是什麼意思,」洛基氣餒地說,「雖然看起來好像知道是什麼意思,但又不知道究竟在說什麼。」
諾斯克大師仔細看了看螢幕,微笑道:「這些是 DynamoDB 在告訴你具體發生了什麼問題。今天我們要學習如何『聽懂』這些訊息。」
Hippo 接著說:「錯誤訊息可是 DynamoDB 給你的免費除錯提示,可別把它們當作噪音。」
Hipoo 向洛基展示他的第一個問題:「新增活動報名功能,一直出現 ValidationException。」,出錯的程式碼如下
async function registerForEvent(userId, eventId, timestamp) {
try {
const params = {
TableName: "IntergalacticEvents",
Item: {
PK: `USER#${userId}`,
SK: `EVENT#${eventId}`,
timestamp: timestamp,
status: "registered",
},
};
await docClient.put(params).promise();
console.log("報名成功!");
} catch (error) {
console.error("報名失敗:", error);
}
}
registerForEvent(12345, "MUSIC_FESTIVAL", "2210-05-15T10:00:00Z");
執行結果:
報名失敗: ValidationException: One or more parameter values were invalid: Number AttributeValue length is too big
洛基困惑地說:「什麼是 'Number AttributeValue length is too big'?我明明沒有用到很大的數字啊!」
大師引導他思考:「讓我們仔細看看你傳入的參數。userId 是什麼型別?」
「數字 12345。」
「在 JavaScript 中,你是怎麼傳入的?」
洛基重新檢查:「userId 12345...等等,我把它當作數字傳入,所以 PK 變成了 USER#12345
,但 12345 是數字型別!」
大師點頭:「DynamoDB 看到的是數字 12345,而不是字串 '12345'。而且在構建複合鍵 USER#${userId}
時,JavaScript 會自動轉換,但這可能產生意想不到的結果。」
洛基開始調查:「讓我看看實際發生了什麼。」
他修改程式來除錯:
async function debugRegisterForEvent(userId, eventId, timestamp) {
console.log("=== 除錯資訊 ===");
console.log("userId 型別:", typeof userId, "值:", userId);
console.log("eventId 型別:", typeof eventId, "值:", eventId);
console.log("timestamp 型別:", typeof timestamp, "值:", timestamp);
const pk = `USER#${userId}`;
const sk = `EVENT#${eventId}`;
console.log("生成的 PK:", pk, "型別:", typeof pk);
console.log("生成的 SK:", sk, "型別:", typeof sk);
const item = {
PK: pk,
SK: sk,
timestamp: timestamp,
status: "registered",
};
console.log("完整 Item:", JSON.stringify(item, null, 2));
}
debugRegisterForEvent(12345, "MUSIC_FESTIVAL", "2210-05-15T10:00:00Z");
執行結果:
=== 除錯資訊 ===
userId 型別: number 值: 12345
eventId 型別: string 值: MUSIC_FESTIVAL
timestamp 型別: string 值: 2210-05-15T10:00:00Z
生成的 PK: USER#12345 型別: string
生成的 SK: EVENT#MUSIC_FESTIVAL 型別: string
完整 Item: {
"PK": "USER#12345",
"SK": "EVENT#MUSIC_FESTIVAL",
"timestamp": "2210-05-15T10:00:00Z",
"status": "registered"
}
「看起來沒問題啊,」洛基說,「所有的字串都是字串型別。」
大師提醒:「但你忽略了一個重要細節。在 DynamoDB 中,timestamp 應該是什麼型別?」
洛基恍然大悟:「等等...我應該檢查 DynamoDB 實際收到的資料型別!」
大師解釋:「DynamoDB 有嚴格的型別系統。讓我們來理解它如何解釋你的資料。」
洛基學著加入更詳細的除錯:
// 洛基學會檢查 DynamoDB 型別
async function debugDynamoDBTypes(userId, eventId, timestamp) {
const AWS = require("aws-sdk");
const converter = AWS.DynamoDB.Converter;
const item = {
PK: `USER#${userId}`,
SK: `EVENT#${eventId}`,
timestamp: timestamp,
status: "registered",
};
console.log("=== JavaScript 物件 ===");
console.log(JSON.stringify(item, null, 2));
console.log("\n=== DynamoDB 格式 ===");
const dynamoItem = converter.marshall(item);
console.log(JSON.stringify(dynamoItem, null, 2));
// 檢查每個屬性的 DynamoDB 型別
Object.keys(dynamoItem).forEach((key) => {
const attr = dynamoItem[key];
const type = Object.keys(attr)[0];
console.log(`${key}: ${type} = ${attr[type]}`);
});
}
debugDynamoDBTypes(12345, "MUSIC_FESTIVAL", "2210-05-15T10:00:00Z");
執行結果:
=== JavaScript 物件 ===
{
"PK": "USER#12345",
"SK": "EVENT#MUSIC_FESTIVAL",
"timestamp": "2210-05-15T10:00:00Z",
"status": "registered"
}
=== DynamoDB 格式 ===
{
"PK": {"S": "USER#12345"},
"SK": {"S": "EVENT#MUSIC_FESTIVAL"},
"timestamp": {"S": "2210-05-15T10:00:00Z"},
"status": {"S": "registered"}
}
PK: S = USER#12345
SK: S = EVENT#MUSIC_FESTIVAL
timestamp: S = 2210-05-15T10:00:00Z
status: S = registered
「這看起來都正確啊,」洛基說,「所有的都是字串型別(S)。那為什麼還是報錯?」
大師引導他:「錯誤訊息提到『Number AttributeValue length is too big』。你有注意到任何數字相關的問題嗎?」
洛基重新審視:「等等...讓我嘗試用字串型別的 userId!」
// 修正版本
debugDynamoDBTypes("12345", "MUSIC_FESTIVAL", "2210-05-15T10:00:00Z");
「還是一樣!」洛基困惑。
大師提示:「讓我們回到最原始的錯誤。試試看真正執行 put 操作,但加入更詳細的錯誤處理。」
洛基修改程式:
async function detailedErrorHandling(userId, eventId, timestamp) {
try {
const params = {
TableName: "IntergalacticEvents",
Item: {
PK: `USER#${userId}`,
SK: `EVENT#${eventId}`,
timestamp: timestamp,
status: "registered",
},
};
console.log("準備發送到 DynamoDB 的參數:");
console.log(JSON.stringify(params, null, 2));
await docClient.put(params).promise();
console.log("成功!");
} catch (error) {
console.log("=== 詳細錯誤資訊 ===");
console.log("錯誤名稱:", error.name);
console.log("錯誤訊息:", error.message);
console.log("錯誤代碼:", error.code);
console.log("HTTP 狀態:", error.statusCode);
console.log("完整錯誤:", error);
}
}
detailedErrorHandling("12345", "MUSIC_FESTIVAL", "2210-05-15T10:00:00Z");
執行結果:
準備發送到 DynamoDB 的參數:
{
"TableName": "IntergalacticEvents",
"Item": {
"PK": "USER#12345",
"SK": "EVENT#MUSIC_FESTIVAL",
"timestamp": "2210-05-15T10:00:00Z",
"status": "registered"
}
}
=== 詳細錯誤資訊 ===
錯誤名稱: ValidationException
錯誤訊息: One or more parameter values were invalid: Number AttributeValue length is too big
錯誤代碼: ValidationException
HTTP 狀態: 400
洛基還是困惑:「數據看起來完全正確,為什麼還是說數字太大?」
大師微笑:「讓我們嘗試一個實驗。用一個更簡單的測試案例。」
// 大師的實驗
async function simpleTest() {
try {
await docClient
.put({
TableName: "IntergalacticEvents",
Item: {
PK: "TEST",
SK: "TEST",
value: 12345,
},
})
.promise();
console.log("成功!");
} catch (error) {
console.log("錯誤:", error.message);
}
}
simpleTest();
「成功!」洛基驚呼,「那問題到底在哪裡?」
洛基嘗試檢查表格:
// 檢查表格是否存在
async function checkTable() {
try {
const result = await docClient
.scan({
TableName: "IntergalacticEvents",
Limit: 1,
})
.promise();
console.log("表格存在,範例資料:", result.Items[0]);
} catch (error) {
console.log("表格檢查錯誤:", error.message);
}
}
checkTable();
執行結果:
表格檢查錯誤: Requested resource not found
洛基恍然大悟:「表格根本不存在!但為什麼錯誤訊息說的是 'Number AttributeValue length is too big' 而不是 'Table not found'?」
大師微笑:「這就是錯誤處理的第一課:錯誤訊息可能會誤導你。有時候真正的問題被其他問題掩蓋了。這就像醫生看病,頭痛不一定是頭的問題,可能是頸椎、眼睛,甚至是壓力造成的!」
洛基創建表格後再次測試:
// 創建表格後重試
async function retryAfterCreateTable() {
// 假設表格已創建
try {
await docClient
.put({
TableName: "IntergalacticEvents",
Item: {
PK: "USER#12345",
SK: "EVENT#MUSIC_FESTIVAL",
timestamp: "2210-05-15T10:00:00Z",
status: "registered",
},
})
.promise();
console.log("成功!原來問題真的是表格不存在!");
} catch (error) {
console.log("還是有錯誤:", error.message);
}
}
「成功了!」洛基驚呼,「但這讓我很困惑,為什麼 DynamoDB 不直接告訴我表格不存在?」
大師解釋:「這涉及到錯誤處理的順序。DynamoDB 會先驗證請求格式,再檢查資源是否存在。如果格式驗證發現問題,就不會進行後續檢查。」
洛基學會了系統性的除錯方法:
async function systematicDebug(tableName, item) {
console.log("=== 系統性除錯流程 ===");
// 步驟 1:檢查基本連線
try {
await docClient.listTables().promise();
console.log("✅ DynamoDB 連線正常");
} catch (error) {
console.log("❌ DynamoDB 連線失敗:", error.message);
return;
}
// 步驟 2:檢查表格是否存在
try {
await docClient.describeTable({ TableName: tableName }).promise();
console.log("✅ 表格存在");
} catch (error) {
if (error.code === "ResourceNotFoundException") {
console.log("❌ 表格不存在:", tableName);
return;
}
console.log("❌ 表格檢查失敗:", error.message);
return;
}
// 步驟 3:驗證資料格式
try {
const AWS = require("aws-sdk");
const marshalled = AWS.DynamoDB.Converter.marshall(item);
console.log("✅ 資料格式正確");
console.log("DynamoDB 格式:", JSON.stringify(marshalled, null, 2));
} catch (error) {
console.log("❌ 資料格式錯誤:", error.message);
return;
}
// 步驟 4:嘗試實際操作
try {
await docClient
.put({
TableName: tableName,
Item: item,
})
.promise();
console.log("✅ 操作成功");
} catch (error) {
console.log("❌ 操作失敗:", error.message);
console.log("錯誤詳情:", {
name: error.name,
code: error.code,
statusCode: error.statusCode,
});
}
}
// 測試
systematicDebug("IntergalacticEvents", {
PK: "USER#12345",
SK: "EVENT#MUSIC_FESTIVAL",
timestamp: "2210-05-15T10:00:00Z",
status: "registered",
});
成功解決第一個問題後,洛基繼續測試 Hippo 給的題目,很快就遇到了新的錯誤:
async function updateEventStatus(userId, eventId, oldStatus, newStatus) {
try {
const params = {
TableName: "IntergalacticEvents",
Key: {
PK: `USER#${userId}`,
SK: `EVENT#${eventId}`,
},
UpdateExpression: "SET #status = :newStatus",
ConditionExpression: "#status = :oldStatus",
ExpressionAttributeNames: {
"#status": "status",
},
ExpressionAttributeValues: {
":newStatus": newStatus,
":oldStatus": oldStatus,
},
};
await docClient.update(params).promise();
console.log("狀態更新成功!");
} catch (error) {
console.error("更新失敗:", error.message);
}
}
// 測試:嘗試將狀態從 'registered' 改為 'confirmed'
updateEventStatus("12345", "MUSIC_FESTIVAL", "registered", "confirmed");
執行結果:
更新失敗:The conditional request failed
洛基困惑:「ConditionalCheckFailedException...這是什麼意思?」
大師引導他思考:「條件檢查失敗。你的條件是什麼?」
「檢查目前狀態是 'registered',」洛基說,「但失敗了,所以...」
「所以目前狀態不是 'registered'!」洛基恍然大悟,「讓我檢查一下實際的狀態。」
洛基開發了診斷工具:
// 診斷條件檢查失敗
async function diagnoseConditionalCheck(userId, eventId, expectedStatus) {
console.log("=== 診斷條件檢查 ===");
// 先讀取當前狀態
try {
const result = await docClient
.get({
TableName: "IntergalacticEvents",
Key: {
PK: `USER#${userId}`,
SK: `EVENT#${eventId}`,
},
})
.promise();
if (!result.Item) {
console.log("❌ 項目不存在");
return;
}
console.log("目前項目:", result.Item);
console.log("目前狀態:", result.Item.status);
console.log("期望狀態:", expectedStatus);
if (result.Item.status === expectedStatus) {
console.log("✅ 狀態匹配,條件應該成功");
} else {
console.log("❌ 狀態不匹配,這就是條件失敗的原因");
}
} catch (error) {
console.log("❌ 讀取失敗:", error.message);
}
}
diagnoseConditionalCheck("12345", "MUSIC_FESTIVAL", "registered");
執行結果:
=== 診斷條件檢查 ===
目前項目: {
PK: 'USER#12345',
SK: 'EVENT#MUSIC_FESTIVAL',
timestamp: '2210-05-15T10:00:00Z',
status: 'pending'
}
目前狀態: pending
期望狀態: registered
❌ 狀態不匹配,這就是條件失敗的原因
「原來如此!」洛基說,「目前狀態是 'pending',不是 'registered'。但我明明記得設定為 'registered'...」
大師提醒:「也許之前的測試資料和你想的不一樣,或者有其他程式修改了狀態。這就是為什麼診斷很重要。」
洛基繼續測試,這次問題是模擬大量使用者同時報名的情況:
// 大量並發測試
async function massRegistration() {
const promises = [];
for (let i = 0; i < 100; i++) {
promises.push(
docClient
.put({
TableName: "IntergalacticEvents",
Item: {
PK: `USER#${i}`,
SK: "EVENT#MUSIC_FESTIVAL",
timestamp: new Date().toISOString(),
status: "registered",
},
})
.promise()
);
}
try {
await Promise.all(promises);
console.log("100 個用戶報名成功!");
} catch (error) {
console.error("大量報名失敗:", error.message);
}
}
massRegistration();
執行結果:
大量報名失敗:Request rate is too high
洛基問:「ProvisionedThroughputExceededException?這是什麼?」
大師解釋:「這是容量限制。你的表格設定了每秒能處理的請求數量,當超過這個限制時,DynamoDB 會拒絕額外的請求。」
洛基接著處理容量錯誤:
// 處理容量限制的智慧重試
async function intelligentMassRegistration() {
const users = Array.from({ length: 100 }, (_, i) => i);
let successCount = 0;
let failCount = 0;
console.log("開始智慧大量報名...");
for (const userId of users) {
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
try {
await docClient
.put({
TableName: "IntergalacticEvents",
Item: {
PK: `USER#${userId}`,
SK: "EVENT#MUSIC_FESTIVAL",
timestamp: new Date().toISOString(),
status: "registered",
},
})
.promise();
successCount++;
console.log(`✅ USER#${userId} 報名成功`);
break; // 成功,跳出重試循環
} catch (error) {
if (error.code === "ProvisionedThroughputExceededException") {
retryCount++;
const delay = 100 * Math.pow(2, retryCount); // 指數退避
console.log(`⏳ USER#${userId} 遇到容量限制,${delay}ms 後重試...`);
await new Promise((resolve) => setTimeout(resolve, delay));
} else {
console.log(`❌ USER#${userId} 其他錯誤:${error.message}`);
failCount++;
break; // 非容量問題,不重試
}
}
}
if (retryCount === maxRetries) {
console.log(`❌ USER#${userId} 重試次數用完,最終失敗`);
failCount++;
}
}
console.log(`\n=== 結果統計 ===`);
console.log(`成功:${successCount}`);
console.log(`失敗:${failCount}`);
}
intelligentMassRegistration();
經過多次實驗,洛基開始理解錯誤的模式:
// 洛基的錯誤分類器
function classifyError(error) {
const errorCode = error.code || error.name;
console.log(`=== 錯誤分析:${errorCode} ===`);
switch (errorCode) {
case "ValidationException":
console.log("類型:參數驗證錯誤");
console.log("原因:請求格式不正確");
console.log("策略:檢查參數格式,修正後重試");
console.log("是否可重試:否(需要修正)");
break;
case "ConditionalCheckFailedException":
console.log("類型:條件檢查失敗");
console.log("原因:資料狀態與期望不符");
console.log("策略:重新讀取資料,確認當前狀態");
console.log("是否可重試:視情況而定");
break;
case "ProvisionedThroughputExceededException":
console.log("類型:容量限制");
console.log("原因:請求速率超過表格容量");
console.log("策略:指數退避重試");
console.log("是否可重試:是");
break;
case "ResourceNotFoundException":
console.log("類型:資源不存在");
console.log("原因:表格或項目不存在");
console.log("策略:確認資源名稱,必要時創建");
console.log("是否可重試:否(需要修正)");
break;
case "InternalServerError":
console.log("類型:服務內部錯誤");
console.log("原因:AWS 內部問題");
console.log("策略:重試");
console.log("是否可重試:是");
break;
default:
console.log("類型:未知錯誤");
console.log("策略:詳細檢查錯誤訊息");
console.log("是否可重試:謹慎重試");
}
console.log("完整錯誤:", error);
}
// 測試錯誤分類器
try {
throw new Error("ConditionalCheckFailedException");
} catch (error) {
error.code = "ConditionalCheckFailedException";
classifyError(error);
}
大師觀察洛基一天的變化,微笑道:「你知道新手和專家的最大差別是什麼嗎?」
洛基思考:「技術能力?」
「不,」大師說,「是對錯誤的態度。新手害怕錯誤、逃避錯誤;專家擁抱錯誤、學習錯誤。」
Hippo 補充:「今天早上你看到錯誤訊息時是什麼感覺?」
洛基回想:「沮喪、困惑、想放棄...」
「現在呢?」
洛基笑了:「好奇、興奮,想要解決它們!」
「這就是成長,」大師說,「你已經從被錯誤困擾,變成與錯誤共舞。」
大師看著洛基滿意的笑容,突然拿出一張圖表:「今天你學會了與錯誤對話。明天,我們要面對一個更根本的挑戰。」
洛基看著那張圖表,上面畫滿了實體與關聯線:「這是...ER 圖?」
「正是,」大師說,「是時候進入下一個階段,讓我們從實務的流程來看看 DynamoDB。」
Hippo:「看看明天菜鳥是不是真的好準備變成專家了!」