Ngiêm cấm sử đụng PROXY và VPS vào mục đích trái pháp luật bạn sẽ phải chịu toàn bộ trách nhiệm trước pháp luật khi sử dụng dịch vụ của chúng tôi tks!
PostgreSQL SKIP LOCKED crawler queue: nhận task không chờ

PostgreSQL SKIP LOCKED crawler queue: nhận task không chờ nhau

Chuyên viên Kỹ thuật ProxyVN 03-10-2026 Đang tính... phút đọc

PostgreSQL SKIP LOCKED crawler queue là cách dùng một bảng PostgreSQL làm hàng đợi URL cho nhiều worker, trong đó mỗi worker nhận task bằng câu SELECT FOR UPDATE SKIP LOCKED. Dòng đang bị worker khác khóa sẽ bị bỏ qua thay vì phải chờ. Kết quả là nhiều worker nhận việc song song, không trùng, không xếp hàng chờ nhau. ProxyVN chạy PostgreSQL 16.2 nhúng qua gói pgserver với psycopg 3.3.7 ngày 03/11/2026, cho bốn worker nhận 400 task bằng ba cách viết, đo số lần nhận trùng và tổng thời gian chờ khóa. Bài ghi lại kết quả thật của từng cách.

PostgreSQL SKIP LOCKED crawler queue: mục tiêu và phạm vi không lặp

Phần này chốt bài toán, đầu ra và ranh giới.

Bài toán

Một crawler chạy trên nhiều máy, các worker cần lấy URL từ một chỗ chung. Nhóm đã có PostgreSQL cho dữ liệu, không muốn dựng thêm hàng đợi riêng. Câu hỏi là viết câu nhận task thế nào để hai worker không lấy cùng URL, và để worker không phải chờ nhau mỗi lần nhận.

Đầu ra nghiệm thu

Đầu ra gồm câu nhận task bằng FOR UPDATE SKIP LOCKED trong giao dịch ngắn, kết quả so với hai cách viết khác về số lần trùng và thời gian chờ khóa. Kèm theo là hành vi khi worker chết giữa giao dịch. Mỗi kết luận có số đo từ lab.

Ranh giới với PgBouncer

PgBouncer gom kết nối để giảm số kết nối tới PostgreSQL. Đó là chuyện của tầng kết nối. Bài này bàn tầng câu lệnh: khóa dòng thế nào để nhận task. Hai chuyện có liên quan ở chỗ giao dịch dài giữ cả kết nối lẫn khóa, nhưng bài không bàn cấu hình PgBouncer.

Phiên bản đã khóa

Lab dùng PostgreSQL 16.2 bản dựng cho Windows đóng gói trong pgserver, psycopg 3.3.7, Python 3.12. Cơ sở dữ liệu chạy cục bộ, các worker là tiến trình riêng trên cùng máy. Hành vi khóa giống PostgreSQL chạy trên máy chủ, thời gian thì không.

Ai nên đọc

Người đang dùng bảng PostgreSQL làm hàng đợi và gặp trùng task hoặc worker chậm. Người cân nhắc giữa PostgreSQL và hàng đợi chuyên dụng cho crawler vài máy. Người đã nghe về SKIP LOCKED nhưng chưa thấy nó khác FOR UPDATE thế nào.

Khi nào không nên

Với hàng nghìn worker hay hàng chục nghìn task mỗi giây, bảng hàng đợi trở thành điểm nóng: mọi worker cùng cập nhật một bảng, chỉ mục trên status thay đổi liên tục, và vacuum phải dọn nhiều dòng chết. Hàng đợi chuyên dụng xử lý tải này tốt hơn. Với crawler vài chục worker, chuyện đó hiếm khi xảy ra.

Vì sao dùng PostgreSQL làm hàng đợi

Dữ liệu URL đã nằm trong PostgreSQL: trạng thái tải, lần tải cuối, kết quả. Hàng đợi trong cùng cơ sở dữ liệu cho phép nhận task và cập nhật kết quả trong cùng giao dịch, truy vấn tiến độ bằng SQL, và không phải đồng bộ giữa hai hệ thống. Với vài chục worker, PostgreSQL xử lý được tốt nếu câu lệnh viết đúng.

Nhận task crawl bằng FOR UPDATE SKIP LOCKED và transaction ngắn với PostgreSQL SKIP LOCKED crawler queue

Phần này giải thích cơ chế khóa dòng.

FOR UPDATE

Theo trang SELECT trong tài liệu PostgreSQL, mệnh đề khóa FOR UPDATE khóa các dòng được SELECT trả về như thể chúng sắp bị cập nhật. Giao dịch khác muốn khóa hay sửa các dòng đó phải chờ tới khi giao dịch hiện tại kết thúc.

Khóa dòng không khóa bảng

FOR UPDATE chỉ khóa những dòng được trả về, không khóa cả bảng. Worker khác vẫn đọc bảng bình thường, vẫn chèn URL mới. Chỉ những dòng đang bị khóa mới bị ảnh hưởng. Nhờ vậy, hàng đợi trên PostgreSQL không có giới hạn một writer như SQLite.

NOWAIT và SKIP LOCKED

Tài liệu nêu hai cách để không chờ. NOWAIT làm câu lệnh báo lỗi ngay nếu dòng bị khóa. SKIP LOCKED bỏ qua những dòng không khóa được ngay. Tài liệu ghi rõ SKIP LOCKED cho một cái nhìn không nhất quán về dữ liệu, nên không hợp với truy vấn thông thường. Nhưng nó hợp để tránh tranh chấp khóa khi nhiều consumer cùng truy cập một bảng dạng hàng đợi.

Ý nghĩa với hàng đợi

Chờ hay bỏ qua

Chờ hay bỏ qua

Với FOR UPDATE thường, hai worker cùng chọn 10 task đầu tiên thì worker thứ hai chờ worker thứ nhất xong. Với SKIP LOCKED, worker thứ hai bỏ qua 10 dòng đang bị khóa và lấy 10 dòng tiếp theo ngay. Không ai chờ ai.

Thứ tự và công bằng

SKIP LOCKED kèm order by id cho mỗi worker lấy nhóm dòng nhỏ nhất chưa bị khóa. URL thêm trước được tải trước, gần đúng thứ tự. Không bảo đảm thứ tự chặt giữa các worker, nhưng với crawler thì không cần. Nếu cần ưu tiên, thêm cột priority và sắp theo nó trước id.

Giao dịch ngắn

Khóa dòng tồn tại tới cuối giao dịch. Nếu worker mở giao dịch, nhận task, rồi tải trang trong khi giao dịch còn mở, khóa bị giữ suốt thời gian tải. Cách đúng là giao dịch chỉ gồm chọn và đánh dấu đã nhận, rồi commit. Việc tải trang nằm ngoài giao dịch.

PostgreSQL nhúng trong lab

pgserver đóng gói bản PostgreSQL dựng sẵn, khởi động một máy chủ cục bộ trong thư mục dữ liệu riêng và trả về chuỗi kết nối. Không cần cài PostgreSQL vào hệ thống. Máy chủ chạy như PostgreSQL bình thường, nên hành vi khóa trong lab là hành vi thật của PostgreSQL 16.2.

Đánh dấu thay vì giữ khóa

Sau khi commit, khóa không còn, nhưng dòng có status claimed nên không khớp điều kiện status bằng new nữa. Worker khác không nhận lại. Trạng thái trong bảng thay thế khóa để giữ quyền sở hữu task trong lúc tải.

Vì sao không xóa dòng khi nhận

Một cách khác là delete returning để lấy task ra khỏi bảng. Cách này gọn nhưng mất dấu vết: worker chết sau khi xóa thì task biến mất. Giữ dòng và đổi trạng thái cho phép dọn task kẹt và xem lịch sử. Có thể chuyển dòng done sang bảng lưu trữ định kỳ nếu bảng quá lớn.

Bảng hàng đợi

create table tasks(
  id serial primary key,
  url text unique,
  status text default 'new',
  worker text,
  claimed_at timestamptz)

Lab nạp 400 URL bằng generate_series. Mỗi lần chạy thử, bảng được dựng lại từ đầu.

Ba cách viết câu nhận task trong PostgreSQL SKIP LOCKED crawler queue

Phần này trình bày ba câu SELECT đem so.

Cách 1: SELECT thường

select id from tasks where status='new' order by id limit 10

Không khóa gì. Worker đọc 10 id, rồi chạy câu update đánh dấu claimed. Giữa lúc đọc và lúc update, worker khác cũng có thể đọc đúng 10 id đó.

Lỗi kiểu này khó phát hiện

SELECT thường rồi UPDATE chạy không lỗi, không cảnh báo. Với một worker, kết quả đúng. Lỗi chỉ xuất hiện khi có nhiều worker cùng lúc, và chỉ thấy được khi đếm số lần tải mỗi URL. Nhiều hệ thống chạy với lỗi này lâu ngày, tốn gấp đôi băng thông mà không ai biết.

Cách 2: FOR UPDATE

select id from tasks where status='new' order by id limit 10 for update

Khóa 10 dòng trả về. Worker khác cùng muốn 10 dòng này phải chờ.

Cùng điều kiện, khác cách khóa

Ba câu lệnh có cùng điều kiện, cùng thứ tự, cùng limit. Chỉ khác mệnh đề khóa ở cuối. Một thay đổi nhỏ trong cú pháp tạo khác biệt lớn về hành vi khi có nhiều worker. Đây là lý do nên thử nghiệm với nhiều worker thật trước khi tin câu nhận task.

Cách 3: FOR UPDATE SKIP LOCKED

select id from tasks where status='new' order by id limit 10
for update skip locked

Khóa 10 dòng không bị ai khóa. Dòng đang bị khóa bị bỏ qua.

Cả ba đều chạy trong giao dịch

Ba cách viết đều nằm trong cùng một khối giao dịch với câu update. Giao dịch không cứu được SELECT thường, vì mức cô lập mặc định của PostgreSQL không ngăn hai giao dịch cùng đọc và cùng ghi một dòng mà không khóa. Chỉ mệnh đề khóa mới tạo ra loại trừ lẫn nhau.

Vòng lặp worker

with c.transaction():
    ids = [r[0] for r in c.execute(SQL, (10,)).fetchall()]
    if not ids: break
    c.execute("update tasks set status='claimed', worker=%s, claimed_at=now() "
              "where id = any(%s)", (name, ids))
time.sleep(0.02)   # xử lý bên ngoài giao dịch

Mỗi vòng một giao dịch: chọn, đánh dấu, commit. Sau đó nghỉ 20 mili giây mô phỏng xử lý. Lab đo thời gian câu SELECT, tức thời gian chờ khóa.

Kết nối giữ suốt vòng lặp

Mỗi worker mở một kết nối và giữ nó suốt vòng lặp, mỗi vòng mở một giao dịch mới trên kết nối đó. Mở kết nối mới mỗi vòng tốn thời gian bắt tay và xác thực. Với hàng chục worker trên nhiều máy, số kết nối có thể chạm giới hạn của máy chủ, đó là lúc PgBouncer có ích.

Bốn worker khởi động cùng lúc

Bốn tiến trình được tạo trước, kết nối xong, rồi chờ một sự kiện chung. Khi sự kiện bật, cả bốn bắt đầu nhận cùng lúc. Trên Windows, tạo tiến trình chậm, không đồng bộ thì worker đầu có thể làm hết việc trước khi worker khác kịp bắt đầu.

Mô phỏng xử lý bằng sleep

Lab dùng time.sleep để mô phỏng thời gian tải trang, không tải thật. Thời gian khóa trong cơ sở dữ liệu chỉ phụ thuộc việc giao dịch mở bao lâu, không phụ thuộc giao dịch làm gì trong thời gian đó. Sleep 50 mili giây trong giao dịch tương đương một lần tải trang 50 mili giây trong giao dịch.

Hai chế độ xử lý

Chế độ thứ nhất xử lý ngoài giao dịch như trên. Chế độ thứ hai cố ý xử lý 50 mili giây bên trong giao dịch, trước khi commit, để thấy hậu quả của giao dịch dài với từng cách viết.

Đo trùng và thời gian chờ khóa với bốn worker của PostgreSQL SKIP LOCKED crawler queue

Phần này đọc kết quả.

SELECT thường: trùng 520 lần

SELECT thường trùng 520

SELECT thường trùng 520

Bốn worker nhận tổng 920 lần cho 400 id khác nhau. 520 lần nhận là trùng. Mỗi worker nhận 230 lần. Gần như mọi task bị hai hoặc ba worker cùng nhận.

Lý do đơn giản: cả bốn worker đọc gần như cùng lúc, cùng thấy 10 dòng new đầu tiên, cùng update chúng thành claimed. Không có gì ngăn họ. Câu update không kiểm lại status, nên ai update sau cũng thành công.

Kiểm trùng bằng tập hợp

Lab gom mọi id các worker trả về, so số lần nhận với số id khác nhau. Với SELECT thường, 920 lần nhận chỉ có 400 id. Phép kiểm này đơn giản, nên chạy mỗi khi đổi câu nhận task hay thêm worker.

FOR UPDATE: không trùng

Bốn worker nhận đúng 400 lần cho 400 id, mỗi worker 100. Tổng thời gian chờ khóa 0,06 giây, toàn bộ chạy 0,35 giây. Khi giao dịch ngắn, khóa giữ rất ngắn, worker chờ nhau gần như không đáng kể.

Kết quả ít hơn limit

Vì PostgreSQL loại dòng đã đổi trạng thái sau khi chờ, một lần SELECT FOR UPDATE limit 10 có thể trả ít hơn 10 dòng, thậm chí không dòng nào dù bảng vẫn còn task new. Worker nhận được danh sách rỗng có thể tưởng hết việc và dừng. Đây là lý do một worker trong ca giao dịch dài nhận 0 task: nó luôn chờ rồi thấy dòng đã bị lấy.

Vì sao FOR UPDATE không trùng

Worker thứ hai chờ khóa của worker thứ nhất. Khi khóa được nhả, PostgreSQL kiểm lại dòng: status đã thành claimed, không còn khớp điều kiện status bằng new. Dòng đó bị loại khỏi kết quả của worker thứ hai. Nhờ cơ chế kiểm lại sau khi chờ, FOR UPDATE không cho trùng.

SKIP LOCKED: không trùng, không chờ

Bốn worker nhận 400 lần cho 400 id, mỗi worker 100. Tổng thời gian chờ khóa 0,04 giây, toàn bộ 0,34 giây. Với giao dịch ngắn, SKIP LOCKED và FOR UPDATE gần như bằng nhau.

Giao dịch dài: FOR UPDATE xếp hàng

Với 50 mili giây xử lý bên trong giao dịch, FOR UPDATE vẫn không trùng, nhưng tổng thời gian chờ khóa của bốn worker lên 5,46 giây, toàn bộ 2,19 giây. Việc chia không đều: 200, 50, 0 và 150. Một worker không nhận được task nào vì cứ chờ khóa rồi thấy dòng đã bị lấy.

Vì sao 0,84 giây chứ không 0,34

Ca giao dịch dài với SKIP LOCKED mất 0,84 giây, gấp đôi ca giao dịch ngắn. Thêm 50 mili giây mỗi vòng, mỗi worker 10 vòng, là thêm khoảng 0,5 giây. Thời gian tăng đúng bằng thời gian xử lý, không thêm thời gian chờ. Đó là dấu hiệu worker chạy song song thật.

Giao dịch dài: SKIP LOCKED vẫn song song

Giao dịch dài

Giao dịch dài

Cùng 50 mili giây trong giao dịch, SKIP LOCKED không trùng, mỗi worker 100 task, tổng chờ khóa 0,04 giây, toàn bộ 0,84 giây. Mỗi worker bỏ qua dòng đang bị khóa và lấy nhóm khác, nên bốn worker làm việc song song thật.

Bảng kết quả

Bảng: Bảng kết quả

Cách viết

Xử lý trong giao dịch

Lần nhận

Trùng

Chia

Chờ khóa

Thời gian

SELECT thường

0

920

520

230 mỗi worker

0,04 s

0,62 s

FOR UPDATE

0

400

0

100 mỗi worker

0,06 s

0,35 s

SKIP LOCKED

0

400

0

100 mỗi worker

0,04 s

0,34 s

FOR UPDATE

50 ms

400

0

200, 50, 0, 150

5,46 s

2,19 s

SKIP LOCKED

50 ms

400

0

100 mỗi worker

0,04 s

0,84 s

Điều kiện dừng của worker

Worker trong lab dừng khi SELECT trả danh sách rỗng. Với SKIP LOCKED, danh sách rỗng có thể chỉ có nghĩa là mọi task còn lại đang bị worker khác khóa trong giao dịch, chưa commit. Worker dừng sớm như vậy không sai vì các task đó sẽ được worker khác xử lý. Với crawler chạy liên tục, worker nên nghỉ rồi hỏi lại thay vì dừng hẳn.

Đọc bảng

SELECT thường sai về logic, không dùng được. FOR UPDATE đúng nhưng nhạy với độ dài giao dịch. SKIP LOCKED đúng và giữ được song song ngay cả khi giao dịch dài. Với hàng đợi crawl, SKIP LOCKED là lựa chọn mặc định hợp lý, kèm quy tắc giao dịch ngắn.

Worker chết, khóa còn giữ và lock_timeout trong PostgreSQL SKIP LOCKED crawler queue

Phần này thử các ca lỗi.

Worker chết giữa giao dịch

Một tiến trình mở giao dịch, khóa 5 dòng bằng SKIP LOCKED, update chúng thành claimed, rồi thoát đột ngột trước khi commit. Kiểm lại: 20 trên 20 dòng vẫn new. PostgreSQL phát hiện kết nối đứt, hủy giao dịch, nhả khóa. Task không bị mất, không bị kẹt.

Phát hiện kết nối đứt

PostgreSQL biết kết nối đứt khi tiến trình worker thoát và hệ điều hành đóng socket. Nếu máy worker mất mạng đột ngột, PostgreSQL có thể mất một lúc mới nhận ra, tùy cấu hình keepalive. Trong thời gian đó, khóa vẫn bị giữ. SKIP LOCKED giúp worker khác không bị chặn, chỉ bỏ qua những dòng đó.

Worker chết sau khi commit

Ca nguy hiểm hơn là worker commit trạng thái claimed rồi mới chết khi đang tải. Dòng ở trạng thái claimed mãi mãi. Khóa không còn để PostgreSQL tự nhả. Cột claimed_at cho phép một câu dọn dẹp trả task claimed quá lâu về new. Lab không cài câu dọn dẹp.

Bỏ qua không có nghĩa là mất

Dòng bị bỏ qua vẫn nằm đó với status new. Khi giao dịch giữ khóa kết thúc bằng rollback, dòng sẵn sàng cho lần nhận sau. Khi kết thúc bằng commit đổi status, dòng không còn là new. Trong cả hai trường hợp, không task nào bị bỏ sót.

SKIP LOCKED khi có dòng bị giữ lâu

Một kết nối A khóa 10 dòng đầu và giữ giao dịch mở. Kết nối B chạy câu SKIP LOCKED: nhận ngay 10 dòng tiếp theo, từ id 11. B không bị A làm chậm chút nào.

FOR UPDATE thường khi có dòng bị giữ lâu

B đặt lock_timeout bằng 1 giây rồi chạy FOR UPDATE thường trên dòng đầu tiên. Sau 1 giây, PostgreSQL hủy câu lệnh với LockNotAvailable: canceling statement due to lock timeout. Không có lock_timeout, B sẽ chờ tới khi A kết thúc, có thể là mãi mãi nếu A bị treo.

statement_timeout

Ngoài lock_timeout, statement_timeout giới hạn thời gian của mọi câu lệnh. Câu nhận task bình thường chạy vài mili giây. Đặt statement_timeout vài giây cho kết nối của worker bắt được những câu chạy chậm bất thường, ví dụ khi thiếu chỉ mục trên bảng lớn.

Đặt lock_timeout cho worker

Ngay cả khi dùng SKIP LOCKED, đặt lock_timeout vài giây cho kết nối của worker là biện pháp an toàn. Nếu có câu lệnh nào khác trong worker vô tình chờ khóa, nó sẽ báo lỗi thay vì treo cả worker.

Giao dịch quên commit

Lỗi lập trình hay gặp là mở giao dịch rồi quên commit vì một nhánh ngoại lệ. Kết nối đứng trong trạng thái giao dịch mở, giữ khóa. Với psycopg 3, dùng khối with c.transaction() bảo đảm commit khi thoát bình thường và rollback khi có ngoại lệ. Lab dùng đúng cách này.

Vacuum và dòng chết

Mỗi lần update status, PostgreSQL tạo phiên bản dòng mới, phiên bản cũ thành dòng chết chờ vacuum dọn. Hàng đợi cập nhật liên tục tạo nhiều dòng chết. Autovacuum thường lo được với tải vừa phải. Với tải lớn, theo dõi kích thước bảng và chỉ mục, điều chỉnh autovacuum cho riêng bảng hàng đợi.

Xem ai đang giữ khóa

PostgreSQL có các view hệ thống cho biết kết nối nào đang chờ khóa, kết nối nào đang giữ giao dịch mở lâu. Khi worker chậm bất thường, xem các view này trước khi nghi ngờ mạng hay trang web. Lab không truy vấn các view này.

Đầu ra nghiệm thu: câu nhận task, so sánh ba cách viết và ca lỗi của PostgreSQL SKIP LOCKED crawler queue

Phần cuối gom kết quả thành bộ bàn giao.

Bộ bàn giao

Bộ bàn giao gồm lab.py khởi động PostgreSQL nhúng, dựng bảng, chạy năm cấu hình với bốn worker, thử worker chết, thử khóa giữ lâu. Người nhận chạy lại, phải thấy SELECT thường trùng hàng trăm lần, hai cách có khóa không trùng, FOR UPDATE chậm hẳn khi giao dịch dài còn SKIP LOCKED thì không.

Chỉ mục cho câu nhận task

Câu nhận task lọc status bằng new rồi sắp theo id. Với bảng lớn, chỉ mục một phần trên id với điều kiện status bằng new giúp PostgreSQL tìm nhanh các dòng cần, và chỉ mục nhỏ vì chỉ chứa dòng new. Lab có 400 dòng nên không cần.

Câu nhận task đề xuất

with picked as (
  select id from tasks where status='new' order by id limit 10
  for update skip locked)
update tasks t set status='claimed', worker=$1, claimed_at=now()
from picked where t.id = picked.id
returning t.id, t.url

Gộp chọn và cập nhật vào một câu, trả về luôn URL cần tải. Lab dùng hai câu riêng trong cùng giao dịch, kết quả tương đương. Lab chưa chạy câu gộp này.

Nhận theo nhóm bao nhiêu

Lab nhận 10 task mỗi lần. Nhóm lớn giảm số giao dịch nhưng một worker giữ nhiều task, nếu nó chết thì nhiều task bị kẹt chờ dọn. Nhóm nhỏ thì ngược lại. Với task tải trang vài giây, nhóm bằng số trang worker tải song song là hợp lý.

Báo task mới bằng NOTIFY

Worker hỏi định kỳ khi hàng đợi rỗng là cách đơn giản. PostgreSQL có LISTEN và NOTIFY để báo ngay khi có task mới, giảm độ trễ và số lần hỏi rỗng. Lab không thử. Với crawler, độ trễ vài giây hiếm khi quan trọng, hỏi định kỳ là đủ.

Tiêu chí đạt

Không task nào bị nhận hai lần. Worker không phải chờ nhau khi nhận. Giao dịch chỉ gồm chọn và đánh dấu. Worker chết giữa giao dịch không làm mất task. Có cơ chế trả task claimed quá lâu về new.

Khi nào cần proxy với PostgreSQL SKIP LOCKED crawler queue

Kết nối tới PostgreSQL là kết nối nội bộ, không nên đi qua proxy dùng cho crawl. Proxy thuộc về bước tải trang mà worker làm sau khi nhận task và commit. Có thể lưu nhãn cấu hình mạng đã dùng vào bảng kết quả để biết trang nào tải qua vùng nào.

Với website được phép thu thập, có lúc cần IP tại Việt Nam để nhận đúng nội dung trong nước. Khi đó anh em có thể mua proxy tĩnh Việt Nam tại trang chủ ProxyVN. Proxy không làm hàng đợi nhanh hơn và không sửa câu nhận task sai. Vai trò hợp lệ của proxy được giải thích ở bài Web Scraping proxy là gì.

Lưu vùng tải cùng kết quả

Bảng tasks hoặc bảng kết quả có thể thêm cột vùng tải để biết mỗi URL được tải qua cấu hình mạng nào. Khi so dữ liệu giữa hai lần tải thấy khác biệt, cột này giúp loại trừ nguyên nhân do đổi vùng. Chỉ lưu nhãn, không lưu chuỗi proxy có mật khẩu.

Tách kết nối cơ sở dữ liệu khỏi proxy

Nếu worker đặt biến môi trường proxy để tải trang, thư viện kết nối PostgreSQL không dùng biến đó, vì giao thức PostgreSQL không phải HTTP. Nhưng nếu worker còn gọi API nội bộ qua HTTP, cần loại các địa chỉ nội bộ bằng NO_PROXY. Cách viết chuỗi proxy và cấu hình loại trừ có trong bài định dạng proxy.

Kết hợp với kết quả trong cùng giao dịch

Lợi thế lớn của hàng đợi trong PostgreSQL là có thể ghi kết quả tải và đánh dấu done trong cùng một giao dịch. Hoặc cả hai thành công, hoặc cả hai không. Không có trạng thái task done mà kết quả chưa ghi. Với hàng đợi bên ngoài, phải tự xử lý trường hợp này.

Những gì lab chưa thử

Lab chưa thử PostgreSQL trên máy chủ riêng qua mạng, chưa thử hàng chục worker, chưa thử bảng hàng triệu dòng với chỉ mục trên status. Lab chưa chạy câu CTE gộp, chưa cài câu dọn task kẹt, chưa thử LISTEN và NOTIFY để báo task mới. Thời gian đo trên một máy.

Checklist PostgreSQL SKIP LOCKED crawler queue

Checklist

Checklist

  • Không bao giờ dùng SELECT thường rồi UPDATE để nhận task.
  • FOR UPDATE SKIP LOCKED trong giao dịch ngắn.
  • Giao dịch chỉ chọn và đánh dấu claimed, tải trang ngoài giao dịch.
  • Dùng with c.transaction() để không quên commit.
  • Đặt lock_timeout vài giây cho kết nối worker.
  • Cột claimed_at và câu dọn task kẹt.
  • Chỉ mục trên status khi bảng lớn.
  • Kết nối cơ sở dữ liệu không đi qua proxy crawl.

Câu hỏi thường gặp về PostgreSQL SKIP LOCKED crawler queue

SKIP LOCKED khác FOR UPDATE thế nào?

FOR UPDATE chờ dòng bị khóa. SKIP LOCKED bỏ qua dòng bị khóa và lấy dòng khác. Lab thấy khi giao dịch dài, FOR UPDATE chờ tổng 5,46 giây, SKIP LOCKED chỉ 0,04 giây.

SELECT thường rồi UPDATE có sao không?

Có. Lab với bốn worker nhận 920 lần cho 400 task, trùng 520 lần.

FOR UPDATE có làm trùng task không?

Không. Lab không trùng, vì PostgreSQL kiểm lại dòng sau khi chờ khóa. Nhưng nó chậm khi giao dịch dài.

Worker chết thì task có mất không?

Nếu chết trước commit, không. Lab thấy 20 trên 20 dòng vẫn new. Nếu chết sau commit, cần câu dọn task claimed quá lâu.

Có nên xử lý trang trong giao dịch không?

Không. Giao dịch dài giữ khóa và kết nối. Lab cho thấy nó làm FOR UPDATE chậm gấp sáu lần.

Hàng đợi PostgreSQL có cần proxy không?

Không. Proxy thuộc về bước tải trang của worker.

PostgreSQL SKIP LOCKED crawler queue và những điều cần nhớ

PostgreSQL SKIP LOCKED crawler queue cho nhiều worker nhận task song song mà không trùng và không chờ nhau. Lab trên PostgreSQL 16.2 cho thấy SELECT thường làm trùng 520 lần trên 400 task. FOR UPDATE đúng nhưng xếp hàng khi giao dịch dài, còn SKIP LOCKED giữ được mỗi worker 100 task với thời gian chờ khóa gần bằng không. Giao dịch ngắn và cột claimed_at là hai phần không thể thiếu. Cần IP tĩnh cho bước tải trang của worker dùng PostgreSQL SKIP LOCKED crawler queue, liên hệ Proxy.vn - Nhà cung cấp dịch vụ proxy chất lượng hàng đầu Việt Nam.

TIN TỨC LIÊN QUAN