Lập trình 22/09/2026 8 phút

Phân trang offset và keyset: đo thật trên 400.000 dòng, trang sâu chậm 90 lần và trùng dữ liệu khi có bài mới

Mở đầu sê-ri thiết kế API bằng thứ gần như API nào cũng có: phân trang. Mình dựng HTTP server Go với hai cách — `?page=N` dùng OFFSET và `?after=<id>` dùng keyset — trên bảng reviews 400.000 dòng và đo qua HTTP. Ở trang 1–100 hai cách bằng nhau; tới trang 19.999 offset mất 13,62 ms còn keyset vẫn 0,15 ms. EXPLAIN lộ lý do: offset đọc 200.000 dòng index để trả 20. Duyệt hết bảng: 28,5 giây so với 0,96 giây. Và khi có 5 review mới chèn vào giữa hai lần lật trang, offset trả 5 dòng trùng, keyset không trùng dòng nào.

Lập trình 22/09/2026 9 phút

Idempotency-Key: đo thật client timeout rồi retry — không khoá tạo 6 đơn cho một lần mua, có khoá tạo đúng 1

Client gọi POST /orders, server đã tạo đơn nhưng phản hồi không về kịp, client retry. Mình dựng server Go với ba bản: không khoá, khoá idempotency trong Redis bằng SET NX, và khoá trong PostgreSQL bằng ON CONFLICT nằm cùng giao dịch với đơn hàng. Đo thật: client timeout 10 ms retry 5 lần — bản không khoá tạo 6 đơn, hai bản có khoá tạo đúng 1. 50 request đồng thời cùng key: 50 đơn so với 1. Hai bản có khoá khác nhau ở chỗ xử lý request trùng đang chạy: Redis từ chối ngay bằng 409, PostgreSQL bắt chờ rồi trả lại phản hồi cũ. Chi phí: khoảng 0,6 ms mỗi request, và request lặp lại chỉ còn 0,22 ms.

Lập trình 22/09/2026 7 phút

Thiết kế payload: đo thật N+1 qua HTTP — 51 request mất 172,7 ms so với 1 request 3,1 ms, và expand mà server tự N+1 vào database thì chỉ đổi chỗ vấn đề

Một màn hình cần 50 review kèm tên và giá sản phẩm. Client làm N+1 qua HTTP: 51 request, 51 query, 172,7 ms với RTT giả lập 2 ms; chạy song song 10 luồng còn 31,2 ms nhưng vẫn 51 request. Endpoint bulk ?ids= còn 2 request, 7,6 ms. ?expand=product còn 1 request, 3,1 ms — nhưng chỉ khi server gom id thành một query IN; bản server tự lặp từng sản phẩm vẫn tốn 51 query. Chọn trường ?fields=id,ten,gia cắt 3,0 KB xuống 0,4 KB vì mỗi sản phẩm mang theo mô tả 256 ký tự màn hình không hiện. Ở RTT 20 ms, N+1 là 1,17 giây cho 50 dòng.

Lập trình 22/09/2026 7 phút

Validation và hợp đồng đầu vào: đo thật — để lỗi lọt tới database là 1.000 rollback, kiểm-rồi-chèn tạo 48 dòng trùng cho một email, và gom lỗi giảm 6 vòng gửi còn 2

Cùng một request sai, 1.000 lần: để database bắt bằng CHECK tốn 110 µs và 1.000 rollback thật đếm từ pg_stat_database, thân lỗi mang tên constraint; kiểm ở biên tốn 41 µs, 0 rollback, thân lỗi mang tên trường. Form sai 5 trường: dừng ở lỗi đầu bắt client gửi 6 vòng, gom mọi lỗi còn 2. Nhưng kiểm ở biên không thay được ràng buộc: 50 request đăng ký cùng email đến cùng lúc, có bước băm bcrypt 60 ms giữa SELECT và INSERT, tạo 48 dòng trùng; UNIQUE trong database giữ đúng 1 và trả 409. Thân 20 MB: không giới hạn thì đọc hết rồi mới chê sau 66,6 ms, MaxBytesReader 1 MB trả 413 sau 2,1 ms.

Lập trình 22/09/2026 7 phút

Sao chép PostgreSQL primary–replica: đo thật — lag 0,32 ms khi rảnh và 52,67 ms p99 khi bận, đọc ngay từ replica không thấy thứ vừa ghi 500/500 lần, và hai cách chữa với giá của từng cách

Một primary PostgreSQL 16 và một streaming replica dựng bằng pg_basebackup. Lag đo bằng cách ghi rồi poll replica: p50 0,32 ms khi primary rảnh, 3,26 ms và p99 52,67 ms khi hai luồng chèn lô nền. Đọc ngay từ replica sau khi ghi: 500/500 lần không thấy, kể cả khi rảnh. Chữa phía đọc bằng cách chờ replica replay tới LSN của lần ghi: 0/500 — với điều kiện LSN lấy sau commit, lấy trong cùng câu INSERT thì vẫn 187/500 sai. Chữa phía ghi bằng synchronous_commit: remote_apply cho 0/300 với giá 0,24 so với 0,07 ms mỗi commit, và khi replica đồng bộ dừng, primary treo cho tới khi đổi tham số.

Lập trình 22/09/2026 8 phút

Giao dịch phân tán: đo thật 2PC bằng PREPARE TRANSACTION trên hai PostgreSQL — đắt gấp 1,5 lần, điều phối chết giữa chừng là khoá bị giữ vô thời hạn, và saga rẻ hơn nhưng người ngoài thấy trạng thái dở 34,6% thời gian

Trừ ví ở PostgreSQL này, tạo đơn ở PostgreSQL kia. Hai commit rời: bước hai hỏng là tiền mất không dấu vết, 100/100 lần. 2PC bằng PREPARE TRANSACTION rồi COMMIT PREPARED: đúng 100/100, giá 0,45 so với 0,30 ms vì thêm hai lượt mạng và fsync cho bản ghi prepare; điều phối viên chết giữa hai pha thì hai giao dịch treo trong pg_prepared_xacts, sống qua restart, giữ khoá dòng vô thời hạn. Saga với bước bù: đúng 100/100, rẻ nhất 0,20 ms, nhưng người đọc ngoài thấy ví và đơn không khớp 34,6% thời gian; 2PC cũng lộ 11,5% vì hai COMMIT PREPARED không đồng thời.