DynamoDB phù hợp một cách tự nhiên với quy trình làm việc của CAPTCHA không có máy chủ - không cần phải đau đầu khi gộp nhóm kết nối, TTL tích hợp để tự động dọn dẹp và hiệu suất nhất quán ở mọi quy mô. Hướng dẫn này đề cập đến thiết kế bảng, cấu trúc mục và mẫu truy vấn để theo dõi cách giải CAPTCHA trong kiến trúc dựa trên Lambda.
Thiết kế bảng
Mẫu bàn đơn
Một bảng DynamoDB xử lý lịch sử giải quyết, tác vụ đang hoạt động và số liệu thống kê tổng hợp:
| Khóa phân vùng (PK) | Khóa sắp xếp (SK) | Mục đích |
|---|---|---|
SOLVE#{captcha_id} |
META |
Giải quyết bản ghi |
SITE#{sitekey} |
SOLVE#{timestamp} |
Lịch sử giải quyết trên mỗi trang web |
STATS#{date} |
TYPE#{captcha_type} |
Số liệu thống kê tổng hợp hàng ngày |
ACTIVE#{captcha_id} |
TASK |
Theo dõi nhiệm vụ trên chuyến bay |
Định nghĩa bảng
{
"TableName": "CaptchaSolves",
"KeySchema": [
{ "AttributeName": "PK", "KeyType": "HASH" },
{ "AttributeName": "SK", "KeyType": "RANGE" }
],
"AttributeDefinitions": [
{ "AttributeName": "PK", "KeyType": "S" },
{ "AttributeName": "SK", "KeyType": "S" },
{ "AttributeName": "GSI1PK", "KeyType": "S" },
{ "AttributeName": "GSI1SK", "KeyType": "S" }
],
"GlobalSecondaryIndexes": [
{
"IndexName": "GSI1",
"KeySchema": [
{ "AttributeName": "GSI1PK", "KeyType": "HASH" },
{ "AttributeName": "GSI1SK", "KeyType": "RANGE" }
],
"Projection": { "ProjectionType": "ALL" }
}
],
"BillingMode": "PAY_PER_REQUEST",
"TimeToLiveSpecification": {
"AttributeName": "ttl",
"Enabled": true
}
}
Triển khai Python
thiết lập
import os
import time
from datetime import datetime, timezone
import boto3
import requests
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ.get("DYNAMODB_TABLE", "CaptchaSolves"))
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
Giải quyết và theo dõi
def solve_and_track(sitekey, pageurl, captcha_type="recaptcha_v2", project=None):
now = datetime.now(timezone.utc)
timestamp = now.isoformat()
ttl_90_days = int(now.timestamp()) + (90 * 24 * 3600)
# Submit to CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
# Store error record
table.put_item(Item={
"PK": f"SITE#{sitekey}",
"SK": f"SOLVE#{timestamp}",
"captcha_type": captcha_type,
"pageurl": pageurl,
"status": "error",
"error": data.get("request"),
"submitted_at": timestamp,
"project": project or "default",
"ttl": ttl_90_days,
"GSI1PK": f"STATUS#error",
"GSI1SK": timestamp
})
return {"error": data.get("request")}
captcha_id = data["request"]
# Track active task
table.put_item(Item={
"PK": f"ACTIVE#{captcha_id}",
"SK": "TASK",
"sitekey": sitekey,
"pageurl": pageurl,
"captcha_type": captcha_type,
"submitted_at": timestamp,
"ttl": int(now.timestamp()) + 600 # Auto-clean in 10 min
})
# Poll for result
polls = 0
for _ in range(60):
time.sleep(5)
polls += 1
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
solved_at = datetime.now(timezone.utc).isoformat()
elapsed_ms = int(
(datetime.now(timezone.utc) - now).total_seconds() * 1000
)
# Store success record
table.put_item(Item={
"PK": f"SOLVE#{captcha_id}",
"SK": "META",
"captcha_type": captcha_type,
"sitekey": sitekey,
"pageurl": pageurl,
"status": "solved",
"submitted_at": timestamp,
"solved_at": solved_at,
"elapsed_ms": elapsed_ms,
"polls": polls,
"project": project or "default",
"ttl": ttl_90_days,
"GSI1PK": f"STATUS#solved",
"GSI1SK": timestamp
})
# Also store in site history
table.put_item(Item={
"PK": f"SITE#{sitekey}",
"SK": f"SOLVE#{timestamp}",
"captcha_id": captcha_id,
"status": "solved",
"elapsed_ms": elapsed_ms,
"ttl": ttl_90_days
})
# Remove active task
table.delete_item(Key={
"PK": f"ACTIVE#{captcha_id}", "SK": "TASK"
})
# Update daily stats
update_daily_stats(captcha_type, True, elapsed_ms)
return {"solution": result["request"]}
if result.get("request") != "CAPCHA_NOT_READY":
table.put_item(Item={
"PK": f"SITE#{sitekey}",
"SK": f"SOLVE#{timestamp}",
"captcha_id": captcha_id,
"status": "error",
"error": result.get("request"),
"ttl": ttl_90_days
})
table.delete_item(Key={
"PK": f"ACTIVE#{captcha_id}", "SK": "TASK"
})
update_daily_stats(captcha_type, False, 0)
return {"error": result.get("request")}
table.delete_item(Key={"PK": f"ACTIVE#{captcha_id}", "SK": "TASK"})
update_daily_stats(captcha_type, False, 0)
return {"error": "TIMEOUT"}
def update_daily_stats(captcha_type, success, elapsed_ms):
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
update_expr = "SET total_solves = if_not_exists(total_solves, :zero) + :one"
expr_values = {":zero": 0, ":one": 1}
if success:
update_expr += ", successful = if_not_exists(successful, :zero) + :one"
update_expr += ", total_elapsed = if_not_exists(total_elapsed, :zero) + :elapsed"
expr_values[":elapsed"] = elapsed_ms
else:
update_expr += ", failed = if_not_exists(failed, :zero) + :one"
table.update_item(
Key={"PK": f"STATS#{date_str}", "SK": f"TYPE#{captcha_type}"},
UpdateExpression=update_expr,
ExpressionAttributeValues=expr_values
)
Mẫu truy vấn
def get_site_history(sitekey, limit=50):
"""Get recent solves for a specific site key."""
response = table.query(
KeyConditionExpression="PK = :pk",
ExpressionAttributeValues={":pk": f"SITE#{sitekey}"},
ScanIndexForward=False,
Limit=limit
)
return response["Items"]
def get_daily_stats(date_str=None):
"""Get stats for a specific date (default: today)."""
if not date_str:
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
response = table.query(
KeyConditionExpression="PK = :pk",
ExpressionAttributeValues={":pk": f"STATS#{date_str}"}
)
return response["Items"]
def get_active_tasks():
"""List all currently active CAPTCHA tasks."""
response = table.query(
IndexName="GSI1",
KeyConditionExpression="GSI1PK = :pk",
ExpressionAttributeValues={":pk": "STATUS#polling"}
)
return response["Items"]
Triển khai JavaScript
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand, QueryCommand, UpdateCommand } = require("@aws-sdk/lib-dynamodb");
const axios = require("axios");
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.DYNAMODB_TABLE || "CaptchaSolves";
const API_KEY = process.env.CAPTCHAAI_API_KEY;
async function solveAndTrack(sitekey, pageurl, type = "recaptcha_v2") {
const now = new Date();
const timestamp = now.toISOString();
const ttl = Math.floor(now.getTime() / 1000) + 90 * 24 * 3600;
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: { key: API_KEY, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) {
await client.send(new PutCommand({
TableName: TABLE,
Item: { PK: `SITE#${sitekey}`, SK: `SOLVE#${timestamp}`, status: "error", error: submit.data.request, ttl },
}));
return { error: submit.data.request };
}
const captchaId = submit.data.request;
let polls = 0;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
polls++;
const poll = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (poll.data.status === 1) {
const elapsed = Date.now() - now.getTime();
await client.send(new PutCommand({
TableName: TABLE,
Item: {
PK: `SOLVE#${captchaId}`, SK: "META", captcha_type: type,
sitekey, pageurl, status: "solved", submitted_at: timestamp,
solved_at: new Date().toISOString(), elapsed_ms: elapsed, polls, ttl,
},
}));
return { solution: poll.data.request };
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
return { error: poll.data.request };
}
}
return { error: "TIMEOUT" };
}
async function getSiteHistory(sitekey, limit = 50) {
const result = await client.send(new QueryCommand({
TableName: TABLE,
KeyConditionExpression: "PK = :pk",
ExpressionAttributeValues: { ":pk": `SITE#${sitekey}` },
ScanIndexForward: false,
Limit: limit,
}));
return result.Items;
}
Tối ưu hóa chi phí
| Chiến lược | tác động |
|---|---|
| Sử dụng thanh toán theo yêu cầu cho khối lượng công việc thay đổi | Không cung cấp quá mức |
| Bật TTL để tự động dọn dẹp bản ghi | Giảm chi phí lưu trữ |
| Dự án chỉ cần các thuộc tính cần thiết trong truy vấn | Tiêu thụ đơn vị đọc thấp hơn |
Viết hàng loạt bằng BatchWriteItem |
Ít lệnh gọi API hơn |
| Sử dụng Luồng DynamoDB để phân tích | Giảm tải tổng hợp cho Lambda |
Khắc phục sự cố
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
ProvisionedThroughputExceededException |
Quá nhiều lần viết mỗi giây | Chuyển sang thanh toán theo yêu cầu hoặc tăng WCU |
| Các mục TTL không bị xóa ngay lập tức | Việc xóa DynamoDB TTL cuối cùng sẽ xảy ra (~48 giờ) | Đừng dựa vào TTL để dọn dẹp theo thời gian thực; lọc các mục đã hết hạn trong truy vấn |
Phân vùng nóng trên STATS#{date} |
Tất cả công nhân ghi vào cùng một phân vùng | Sử dụng hậu tố ngẫu nhiên: STATS#{date}#shard{0-9} |
| Truy vấn trả về quá nhiều mục | Khóa phân vùng rộng | Thêm điều kiện SK để thu hẹp kết quả |
Câu hỏi thường gặp
Tại sao DynamoDB thay vì RDS để theo dõi CAPTCHA không có máy chủ?
DynamoDB không có giới hạn kết nối — hoàn hảo cho Lambda nơi mỗi lệnh gọi sẽ mở ra một kết nối mới. RDS yêu cầu tổng hợp kết nối (RDS Proxy), điều này làm tăng thêm chi phí và độ phức tạp.
DynamoDB tốn bao nhiêu tiền cho việc theo dõi CAPTCHA?
Với thanh toán theo yêu cầu: ~ 1,25 USD trên một triệu lượt ghi và ~ 0,25 USD trên một triệu lượt đọc. Với 10.000 giải/day, chi phí lưu trữ và truy cập sẽ dưới $1/month.
Tôi có thể truy vấn tất cả các loại CAPTCHA không?
Sử dụng chỉ mục GSI1 để truy vấn theo trạng thái trên các loại. Đối với phân tích nhiều loại, hãy tổng hợp bằng cách sử dụng Luồng DynamoDB và hàm Lambda ghi vào phân vùng STATS#.
Các bước tiếp theo
Xây dựng tính năng theo dõi CAPTCHA không cần máy chủ có khả năng tự động chia tỷ lệ —lấy khóa API CaptchaAI của bạn.
Hướng dẫn liên quan:
- AWS Lambda + CaptchaAI
- Lịch sử CAPTCHA MongoDB
- Quản lý TTL mã thông báo Redis