Khi nhiều nhân viên hoặc nhiều người thử gửi cùng một CAPTCHA để giải, bạn sẽ phải trả tiền cho mỗi bản sao. Lớp chống trùng lặp sẽ xử lý các yêu cầu giống hệt nhau và trả về cùng một kết quả — tiết kiệm tín dụng API và giảm độ trễ.
Sự trùng lặp xảy ra như thế nào
| Kịch bản | nguyên nhân | Chất thải |
|---|---|---|
| Thử lại trước khi có kết quả | Logic thử lại tích cực | Chi phí gấp 2–5 lần cho mỗi CAPTCHA |
| Nhiều công nhân, cùng một mục tiêu | Không có sự phối hợp giữa các công nhân | Giải quyết lãng phí song song |
| Kích hoạt lại làm mới trang | Thử lại giao diện người dùng khi hết thời gian chờ | Giải quyết thêm mỗi lần làm mới |
| Tin nhắn hàng đợi được phát lại | Đảm bảo giao hàng ít nhất một lần | Giải quyết trùng lặp mỗi lần phát lại |
Thiết kế khóa chống trùng lặp
Tạo một khóa duy nhất từ các tham số yêu cầu:
import hashlib
def dedup_key(method, sitekey, pageurl):
"""Generate a deduplication key for a CAPTCHA solve request."""
raw = f"{method}:{sitekey}:{pageurl}"
return f"captcha:dedup:{hashlib.sha256(raw.encode()).hexdigest()[:16]}"
Thành phần chính:
| Loại CAPTCHA | Thành phần chính |
|---|---|
| reCAPTCHA v2 | method + sitekey + pageurl |
| reCAPTCHA v3 | method + sitekey + pageurl + action |
| hCaptcha | method + sitekey + pageurl |
| cửa quay | method + sitekey + pageurl |
| CAPTCHA hình ảnh | method + hàm băm của body (nội dung hình ảnh) |
Chống trùng lặp dựa trên Redis
Triển khai Python
import os
import time
import json
import hashlib
import redis
import requests
r = redis.Redis(
host=os.environ.get("REDIS_HOST", "localhost"),
port=int(os.environ.get("REDIS_PORT", 6379)),
decode_responses=True
)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
# Dedup window: how long to consider a request "in progress"
DEDUP_TTL = 180 # seconds
def dedup_key(method, sitekey, pageurl, extra=""):
raw = f"{method}:{sitekey}:{pageurl}:{extra}"
return f"captcha:dedup:{hashlib.sha256(raw.encode()).hexdigest()[:16]}"
def solve_with_dedup(sitekey, pageurl, method="userrecaptcha"):
key = dedup_key(method, sitekey, pageurl)
# Check if this request is already being solved
existing = r.get(key)
if existing:
state = json.loads(existing)
if state["status"] == "solving":
# Wait for the result
return wait_for_result(key)
elif state["status"] == "solved":
return {"solution": state["solution"], "source": "dedup_cache"}
elif state["status"] == "error":
pass # Allow retry on error
# Mark as solving
r.set(key, json.dumps({"status": "solving", "started": time.time()}), ex=DEDUP_TTL)
# Submit to CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": method,
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
r.set(key, json.dumps({"status": "error", "error": data.get("request")}), ex=30)
return {"error": data.get("request")}
captcha_id = data["request"]
# Poll for result
for _ in range(60):
time.sleep(5)
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:
solution = result["request"]
# Cache the result for other workers (short TTL since tokens expire)
r.set(key, json.dumps({
"status": "solved",
"solution": solution,
"solved_at": time.time()
}), ex=60) # Cache result for 60 seconds
return {"solution": solution, "source": "api"}
if result.get("request") != "CAPCHA_NOT_READY":
r.set(key, json.dumps({
"status": "error", "error": result.get("request")
}), ex=30)
return {"error": result.get("request")}
r.set(key, json.dumps({"status": "error", "error": "TIMEOUT"}), ex=30)
return {"error": "TIMEOUT"}
def wait_for_result(key, timeout=120):
"""Wait for another worker to finish solving."""
start = time.time()
while time.time() - start < timeout:
data = r.get(key)
if data:
state = json.loads(data)
if state["status"] == "solved":
return {"solution": state["solution"], "source": "dedup_wait"}
if state["status"] == "error":
return {"error": state.get("error", "UNKNOWN")}
time.sleep(2)
return {"error": "DEDUP_WAIT_TIMEOUT"}
Triển khai JavaScript
const Redis = require("ioredis");
const axios = require("axios");
const crypto = require("crypto");
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const DEDUP_TTL = 180;
function dedupKey(method, sitekey, pageurl) {
const raw = `${method}:${sitekey}:${pageurl}`;
const hash = crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
return `captcha:dedup:${hash}`;
}
async function solveWithDedup(sitekey, pageurl, method = "userrecaptcha") {
const key = dedupKey(method, sitekey, pageurl);
// Check existing
const existing = await redis.get(key);
if (existing) {
const state = JSON.parse(existing);
if (state.status === "solving") return await waitForResult(key);
if (state.status === "solved") return { solution: state.solution, source: "dedup_cache" };
}
// Mark as solving
await redis.set(key, JSON.stringify({ status: "solving", started: Date.now() }), "EX", DEDUP_TTL);
// Submit
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: { key: API_KEY, method, googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) {
await redis.set(key, JSON.stringify({ status: "error", error: submit.data.request }), "EX", 30);
return { error: submit.data.request };
}
const captchaId = submit.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
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) {
await redis.set(key, JSON.stringify({ status: "solved", solution: poll.data.request }), "EX", 60);
return { solution: poll.data.request, source: "api" };
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
await redis.set(key, JSON.stringify({ status: "error", error: poll.data.request }), "EX", 30);
return { error: poll.data.request };
}
}
await redis.set(key, JSON.stringify({ status: "error", error: "TIMEOUT" }), "EX", 30);
return { error: "TIMEOUT" };
}
async function waitForResult(key, timeout = 120000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const data = await redis.get(key);
if (data) {
const state = JSON.parse(data);
if (state.status === "solved") return { solution: state.solution, source: "dedup_wait" };
if (state.status === "error") return { error: state.error };
}
await new Promise((r) => setTimeout(r, 2000));
}
return { error: "DEDUP_WAIT_TIMEOUT" };
}
Thay thế khóa cơ sở dữ liệu
Đối với việc trích xuất dữ liệu dựa trên PostgreSQL không có Redis:
import psycopg2
def solve_with_pg_dedup(conn, sitekey, pageurl):
"""Use PostgreSQL advisory locks for deduplication."""
# Generate a numeric lock key from the dedup key
lock_id = hash(f"{sitekey}:{pageurl}") & 0x7FFFFFFF
cursor = conn.cursor()
# Try to acquire advisory lock (non-blocking)
cursor.execute("SELECT pg_try_advisory_lock(%s)", (lock_id,))
acquired = cursor.fetchone()[0]
if not acquired:
# Another worker is solving — wait for result
cursor.execute("SELECT pg_advisory_lock(%s)", (lock_id,))
# Lock acquired means other worker finished — check cache
cursor.execute(
"SELECT solution FROM captcha_cache "
"WHERE sitekey = %s AND pageurl = %s "
"AND created_at > NOW() - INTERVAL '60 seconds'",
(sitekey, pageurl)
)
row = cursor.fetchone()
cursor.execute("SELECT pg_advisory_unlock(%s)", (lock_id,))
if row:
return {"solution": row[0], "source": "pg_cache"}
return {"error": "NO_CACHED_RESULT"}
try:
# Solve the CAPTCHA
solution = solve_via_api(sitekey, pageurl)
if solution:
cursor.execute(
"INSERT INTO captcha_cache (sitekey, pageurl, solution) "
"VALUES (%s, %s, %s)",
(sitekey, pageurl, solution)
)
conn.commit()
return {"solution": solution} if solution else {"error": "SOLVE_FAILED"}
finally:
cursor.execute("SELECT pg_advisory_unlock(%s)", (lock_id,))
Số liệu hiệu quả Dedup
Theo dõi tiết kiệm chống trùng lặp:
def track_dedup_stats(source):
"""Increment counters for dedup tracking."""
today = time.strftime("%Y-%m-%d")
r.hincrby(f"dedup:stats:{today}", source, 1)
r.expire(f"dedup:stats:{today}", 7 * 86400)
def get_dedup_report():
today = time.strftime("%Y-%m-%d")
stats = r.hgetall(f"dedup:stats:{today}")
total = sum(int(v) for v in stats.values())
saved = int(stats.get("dedup_cache", 0)) + int(stats.get("dedup_wait", 0))
return {
"total_requests": total,
"deduplicated": saved,
"savings_pct": f"{saved / total * 100:.1f}%" if total else "0%",
"breakdown": stats
}
Khắc phục sự cố
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
| Loại bỏ xung đột khóa | Tham số băm quá ngắn hoặc thiếu | Bao gồm tất cả các thông số dành riêng cho CAPTCHA trong khóa; tăng độ dài băm |
| Đang chờ công nhân hết giờ | Nhân viên giải quyết bị rơi | TTL trên trạng thái solving tự động hết hạn (180 giây) |
| Kết quả được lưu trong bộ nhớ đệm cũ | Mã thông báo đã hết hạn nhưng bộ đệm vẫn hợp lệ | Đặt bộ đệm kết quả TTL ngắn hơn thời gian tồn tại của mã thông báo (60 giây đối với reCAPTCHA) |
| Điều kiện cuộc đua trên trường quay | Hai công nhân kiểm tra đồng thời | Sử dụng SET NX (set-if-not-exists) để thu thập khóa nguyên tử |
Câu hỏi thường gặp
Khi nào việc chống trùng lặp có giá trị phức tạp?
Khi bạn có nhiều nhân viên nhắm mục tiêu vào cùng một tổ hợp sitekey/pageurl. Tỷ lệ khấu trừ thậm chí 10% cũng giúp tiết kiệm đáng kể tín dụng API trên quy mô lớn — và giúp loại bỏ thời gian giải quyết lãng phí.
Tôi có nên loại bỏ CAPTCHA của hình ảnh không?
Có, nhưng hãy sử dụng hàm băm của nội dung hình ảnh như một phần của khóa khấu trừ. Các hình ảnh giống hệt nhau sẽ trả về cùng một văn bản, vì vậy việc loại bỏ sẽ có hiệu quả.
Còn các proxy khác nhau cho cùng một CAPTCHA thì sao?
Không bao gồm proxy trong khóa khấu trừ. Mã thông báo giải pháp hoạt động bất kể proxy nào được sử dụng để giải quyết nó. Bao gồm proxy sẽ đánh bại việc chống trùng lặp.
Các bước tiếp theo
Ngừng trả tiền cho các giải pháp CAPTCHA trùng lặp —lấy khóa API CaptchaAI của bạnvà triển khai tính năng khấu trừ ngay hôm nay.
Hướng dẫn liên quan:
- Quản lý TTL mã thông báo Redis
- Công nhân phân tán trạng thái phiên
- Phục hồi lỗi hàng loạt