Xa hơn nhiều người nghĩ, và đây thường là lựa chọn đúng cho hệ thống vừa vì tránh được một thành phần hạ tầng.
Hàng đợi job — SELECT ... FOR UPDATE SKIP LOCKED:
with next_job as (
select id from jobs
where status = 'pending' and run_at <= now()
order by run_at
for update skip locked
limit 1
)
update jobs set status = 'running', started_at = now()
from next_job where jobs.id = next_job.id
returning jobs.*;SKIP LOCKED cho nhiều worker lấy job song song mà không chờ nhau và không lấy trùng. Được lợi nguyên bản: job nằm cùng transaction với dữ liệu nghiệp vụ (không cần outbox), retry/lịch sử/quan sát đều bằng SQL thường.
Cache — bảng UNLOGGED: không ghi WAL nên ghi nhanh hơn đáng kể; đổi lại bảng bị truncate khi crash — đúng ngữ nghĩa của cache. Hết hạn thì thêm cột expires_at và một job dọn định kỳ.
Tín hiệu — LISTEN/NOTIFY: đủ cho việc đánh thức worker hoặc invalidate cache trong cụm nhỏ.
Giới hạn — nên chuyển sang Redis/Kafka khi:
- Throughput hàng đợi vượt vài nghìn job/giây, hoặc job ngắn khiến contention và bloat tăng nhanh (mỗi UPDATE sinh một tuple mới, VACUUM phải theo kịp).
- Cache cần độ trễ dưới mili-giây, tỉ lệ đọc rất cao, hoặc eviction theo bộ nhớ tự động (maxmemory + LRU/LFU) — Postgres không có.
- Cần cấu trúc dữ liệu chuyên biệt: sorted set cho leaderboard, counter nguyên tử tần suất cao, rate limiter, distributed lock.
- NOTIFY không bền: message mất nếu không có listener nào đang kết nối, và payload bị giới hạn kích thước.
Nguyên tắc: bắt đầu bằng Postgres, thêm Redis khi có số đo cho thấy đã chạm giới hạn — chứ không phải vì kiến trúc tham chiếu nào cũng vẽ một ô Redis.