Giao diện
Lab: tự chạy thử
Nhiều trang trong sổ tay có số liệu và kết quả “đã chạy thật” trên MySQL và PostgreSQL. Toàn bộ script dùng để chạy nằm trong thư mục lab/ của repo. Trang này nói cách tự chạy lại chúng; code của từng script được nhúng ở cuối mỗi trang, trong mục Nguồn tham khảo.
Tự chạy có ích ở ba chỗ:
- Kiểm chứng: số liệu trong trang được đo trên một laptop vào một ngày cụ thể; chạy lại trên máy khác hay phiên bản database khác để xem kết luận còn đúng không.
- Thử biến thể: đổi isolation level, cỡ lô, kích thước bảng, rồi xem kết quả đổi ra sao. Cách này nhớ lâu hơn đọc bảng.
- Khi nâng phiên bản: một số kết luận chỉ đúng với MySQL 8.0 (ví dụ cascade không có trong binlog, MySQL chỉ dùng hash join khi không có index), nên lên 8.4 hay 9.x cần chạy lại.
Script chỉ chạy được trên máy của mình, vì site này là file tĩnh, không có database phía sau. Phần lớn script cần Docker; riêng latency, process-thread và event-loop chỉ cần Node.js.
Cách chạy
Cần Docker Desktop đang chạy và Node.js 20 trở lên.
bash
npm run lab:up # cài thư viện, dựng 3 container, nối replica
npm run lab -- transaction # chạy script của một trang (tên trong bảng dưới)
npm run lab -- all # chạy tất cả trừ dong-thuan, thoi-gian và redis, mất vài phút
npm run lab:down # xoá container và dữ liệulab:up dựng ba container: MySQL 8.0 làm source (cổng 33306), một MySQL replica dùng GTID (33307), và PostgreSQL 17 (55432). Tất cả chỉ mở trên 127.0.0.1, mật khẩu pw.
| Lệnh | Trang | Chạy những gì |
|---|---|---|
npm run lab -- viet-sql | Viết SQL khi phỏng vấn | Dữ liệu mẫu, mọi câu SQL trong trang, các khác biệt giữa MySQL và PostgreSQL |
npm run lab -- phan-tich-bai-toan | Phân tích bài toán | Dung lượng thật của 1 triệu dòng so với dữ liệu thô, throughput của point select và insert từng dòng / theo lô trên MySQL và PostgreSQL; khoảng 1 phút |
npm run lab -- time-series | Dữ liệu chuỗi thời gian | Partition theo ngày: DELETE một ngày so với bỏ partition (thời gian, binlog/WAL), partition pruning; BRIN so với B-tree; bảng tổng hợp theo giờ; khoảng 1 phút |
npm run lab -- bo-dem | Bộ đếm, hot row & bảng tổng hợp | 32 connection cùng tăng một bộ đếm: một dòng, chia 4/16/64 dòng, cập nhật ở đầu và cuối transaction; COUNT(*) và số ước tính; hạng trong bảng xếp hạng; khoảng 1 phút |
npm run lab -- full-text-search | Full-text search | 500.000 tên sản phẩm tiếng Việt: LIKE so với FULLTEXT, GIN trên tsvector, pg_trgm; từ hai chữ, tìm không dấu với collation ai_ci và unaccent; khoảng 30 giây |
npm run lab -- da-ngon-ngu | Đa ngôn ngữ | Bảng dịch và ngôn ngữ dự phòng, cột JSON có index và CHECK, thứ tự sắp xếp tiếng Việt theo từng collation; vài giây |
npm run lab -- dia-ly | Dữ liệu địa lý | 1 triệu cửa hàng trên MySQL: tìm trong bán kính bằng ST_Distance_Sphere so với khung bao có spatial index; sai số của công thức phẳng; khoảng 3 phút |
npm run lab -- migration | Migration trong team | ALTER INSTANT, INPLACE, COPY trên bảng 1 triệu dòng khi có client ghi; ALTER xếp hàng sau transaction mở lâu, có và không có lock timeout, trên MySQL và PostgreSQL; khoảng 20 giây |
npm run lab -- test | Test với database | Cùng câu lệnh trên SQLite (node:sqlite), MySQL, PostgreSQL; chi phí mỗi test của rollback, TRUNCATE, DELETE, database từ template; khoảng 1 phút |
npm run lab -- giam-sat | Giám sát database | Tải giả lập (N+1, query thiếu index, query thường) rồi xem performance_schema và pg_stat_statements; history list length và dead tuple khi có transaction mở lâu; hit rate; khoảng 30 giây, dựng tạm container lab-pg-stat |
npm run lab -- backup | Backup & khôi phục | mysqldump, xoá nhầm, replica nhận lệnh xoá sau bao lâu, khôi phục tới ngay trước lệnh xoá bằng START REPLICA UNTIL trên một MySQL tạm; pg_dump/pg_restore; khoảng 20 giây |
npm run lab -- bao-mat | Bảo mật database | SQL injection khi nối chuỗi và khi dùng placeholder, câu lệnh chồng, ORDER BY ?, LIKE với %, quyền của user chỉ đọc ghi dữ liệu; vài giây |
npm run lab -- mui-gio | Thời gian & múi giờ | TIMESTAMP/DATETIME, timestamptz/timestamp khi đổi múi giờ của phiên; giá trị mysql2 và pg ghi khi process Node có TZ khác nhau; CONVERT_TZ; doanh thu theo ngày UTC và ngày giờ Việt Nam; vài giây |
npm run lab -- oltp-olap | OLTP vs OLAP | 5 triệu dòng bán hàng trên PostgreSQL và DuckDB (lưu theo cột, chạy trong process): dung lượng, query báo cáo, tra theo khoá, UPDATE từng dòng; khoảng 20 giây |
npm run lab -- view-trigger | View, trigger, stored procedure | View, trigger (kể cả cascade, TRUNCATE), procedure, binlog khi cascade, chi phí trigger |
npm run lab -- transaction | Transaction & isolation | 6 kịch bản × 4 isolation level × 2 database; ghi lại dữ liệu cho mô phỏng; DDL trong transaction |
npm run lab -- cau-truc-luu-tru | Cấu trúc lưu trữ | Heap PostgreSQL (ctid, HOT, bảng phình, Heap Fetches), InnoDB |
npm run lab -- join | Thuật toán join | 100.000 khách × 1.000.000 đơn; các thuật toán join và cách ép |
npm run lab -- xu-ly-theo-lo | Xử lý dữ liệu lớn theo lô | Một câu so với chia lô (lock, replica lag), OFFSET so với keyset, import, export, xoá theo lô |
npm run lab -- tach-doc-ghi | Tách đọc / ghi | Đọc replica ngay sau khi ghi, chờ GTID, pool xoay vòng. Mặc định chỉ đo lúc rảnh; thêm FULL=1 để đo lúc có tải ghi nặng (khoảng 5 phút, ghi nhiều binlog) |
npm run lab -- latency | Latency | Độ bận và latency, fan-out, hedged request, coordinated omission. Chỉ cần Node.js, không cần lab:up; khoảng 5–6 phút |
npm run lab -- process-thread | Process, thread & concurrency | Số thread của process Node, chi phí process/worker/async function, concurrency với parallelism. Chỉ cần Node.js; khoảng 20 giây |
npm run lab -- dong-thuan | Đồng thuận | Cluster etcd 3 node (dựng riêng bằng npm run lab -- raft:up, xoá bằng raft:down, không cần lab:up): latency, cắt follower, cắt leader và đo thời gian bầu leader mới, mất đa số; khoảng 30 giây |
npm run lab -- thoi-gian | Thời gian & thứ tự | Đồng hồ máy so với NTP (sntp, chỉ macOS), đồng hồ container Docker so với máy host bằng công thức NTP, Date.now() so với performance.now(); khoảng 40 giây, chỉ cần Docker. Trên Windows và Linux, phần sntp được bỏ qua |
npm run lab -- redis | Redis | Redis 8.4 (dựng riêng bằng npm run lab -- redis:up, xoá bằng redis:down, không cần lab:up): thread của Redis, redis-benchmark có và không có pipelining, io-threads, ba cách gửi lệnh từ Node, lệnh chậm chặn client khác, encoding và bộ nhớ, MULTI/EXEC, WATCH, script Lua, lazy và active expire, noeviction và LRU xấp xỉ, fork và copy-on-write, RDB và AOF khi bị kill, WAIT, hash tag; khoảng 3 phút, khởi động lại lab-redis và pause replica |
npm run lab -- commit-phan-tan | Commit phân tán | XA của MySQL và PREPARE TRANSACTION của PostgreSQL: chi phí, transaction in doubt giữ lock và còn sau restart, coordinator nhỏ chuyển tiền giữa hai database; khoảng 30 giây, restart container MySQL và PostgreSQL |
npm run lab -- nhat-quan | Nhất quán & CAP | Semi-sync replication của MySQL: latency commit so với async, docker pause replica để thấy commit chờ tới timeout rồi chuyển về async; khoảng 15 giây |
npm run lab -- distributed-lock | Distributed lock | Bảng lease kèm fencing token trên MySQL và PostgreSQL: tạo dòng, acquire lấy token trong một lệnh, hết hạn, release sai chủ, ghi bằng token cũ, 20 client tranh lock; khoảng 5 giây |
npm run lab -- xu-ly-loi | Xử lý lỗi & idempotency | Timeout sau khi commit với và không có idempotency key, hai cách lưu key, retry storm với bốn chiến lược, circuit breaker, phát hiện node chết bằng heartbeat và SWIM (10–60 node UDP thật, mất gói, node bị treo). Phần idempotency cần lab:up, các phần còn lại chỉ cần Node.js; khoảng 3 phút |
npm run lab -- event-loop | Event loop | Thứ tự callback (CommonJS, ES module), thread pool, event loop bị chặn, 10.000 request trên một thread. Chỉ cần Node.js; khoảng 1 phút |
npm run lab -- kieu-so-sanh | Kiểu dữ liệu & so sánh | Kết quả thật của các phép so sánh và phép tính hay bị hỏi. Chỉ cần Node.js; vài giây |
npm run lab -- scope-closure | Scope, closure, this, prototype | Hoisting, TDZ, closure trong vòng lặp, this, prototype, closure giữ bộ nhớ. Chỉ cần Node.js; vài giây |
npm run lab -- async | Promise & async/await | Hành vi thật của Promise trên Node: thứ tự, lỗi không ai bắt, các hàm gộp promise. Chỉ cần Node.js; vài giây |
npm run lab -- module | Module: CommonJS & ESM | CommonJS và ES module trên Node 24, vòng phụ thuộc, named import từ CommonJS. Chỉ cần Node.js; vài giây |
npm run lab -- libuv-thread-pool | Non-blocking I/O & thread pool | Thread pool dùng chung: pbkdf2 chiếm hết pool làm đọc file chậm theo; mỗi UV_THREADPOOL_SIZE một process. Chỉ cần Node.js; khoảng 10 giây |
npm run lab -- stream | Stream & backpressure | Bộ nhớ khi chép file bằng readFile so với pipeline, đọc từng dòng, bỏ qua và tôn trọng backpressure, pipe() so với pipeline() khi nguồn lỗi. Tạo file mẫu khoảng 90 MB trong thư mục tạm rồi xoá. Chỉ cần Node.js; khoảng 25 giây |
npm run lab -- bo-nho-gc | Bộ nhớ & GC | Giới hạn heap, OOM, minor và major GC, leak của cache và listener, WeakMap, chụp heap snapshot (file khoảng 160 MB, xoá sau khi đo). Chỉ cần Node.js; khoảng 15 giây |
npm run lab -- node-xu-ly-loi | Xử lý lỗi & graceful shutdown | Lỗi trong callback, nuốt uncaughtException làm pool cạn, bốn cách xử lý SIGTERM với client giữ keep-alive, Error có cause. Chỉ cần Node.js; khoảng 20 giây |
npm run lab -- concurrency | Race condition trong Node.js | Rút tiền cùng lúc qua await, mutex trong một và hai process, ghi có điều kiện ở "database" giả lập, giới hạn số việc song song. Chỉ cần Node.js; khoảng 10 giây |
npm run lab -- scale | Scale Node.js | Throughput của cluster theo số worker, cách chia request cho worker, session trong bộ nhớ khi có nhiều worker. Chỉ cần Node.js; khoảng 20 giây |
npm run lab -- hieu-nang | Hiệu năng & profiling | Khởi động lạnh, ảnh hưởng của cách ghi log, --cpu-prof khi có tải; ghi lại dữ liệu cho mô phỏng flame graph. Chỉ cần Node.js; khoảng 25 giây |
npm run lab -- framework | Framework: Express & NestJS | Handler async ném lỗi trên Express 4 và 5, thứ tự middleware, error middleware, validation bằng zod, giới hạn body. Chỉ cần Node.js; vài giây |
npm run lab -- node-bao-mat | Bảo mật Node.js | Prototype pollution và các cách chặn, permission model, process con kế thừa biến môi trường, npm audit (cần mạng). Chỉ cần Node.js; vài giây |
npm run lab -- nodejs-database | Node.js với database | Transaction bằng pool.query, quên release(), kích thước pool và max_connections, kiểu BIGINT/DECIMAL của pg và mysql2, AsyncLocalStorage, stream 500.000 dòng. Cần lab:up (database nodedb); khoảng 20 giây |
npm run lab -- kafka | Kafka | Cụm Kafka 4.1 ba broker (dựng riêng bằng npm run lab -- kafka:up, xoá bằng kafka:down, không cần lab:up): thứ tự theo key, tăng số partition, consumer group 1–8 consumer, consumer bị kill, acks và min.insync.replicas khi tắt hay kill follower, ba cách commit offset, log compaction. Khoảng 7–8 phút |
npm run lab -- hang-doi | Hàng đợi message | RabbitMQ 4.1 và ElasticMQ (giả lập SQS), dựng riêng bằng npm run lab -- hang-doi:up, xoá bằng hang-doi:down, không cần lab:up: prefetch, auto-ack và ack khi consumer chết, fan-out, message độc và delivery limit, thứ tự với hai consumer; visibility timeout, dead letter queue, FIFO theo message group, long polling. Khoảng 1 phút |
npm run lab -- building-blocks | Building blocks | Bốn cách chia request (round-robin, random, least outstanding, power of two choices) cho 4 server, khi các server như nhau và khi một server chậm. Chỉ cần Node.js; khoảng 1 phút |
npm run lab -- chan-event-loop | Chặn event loop & xử lý CPU | JSON, vòng lặp O(n²), ReDoS, chi phí gửi dữ liệu sang worker, CPU tăng tốc chậm sau lúc rảnh, so sánh chạy thẳng / chia nhỏ / pool worker khi có tải, --cpu-prof, --trace-sync-io, dừng regex chạy quá lâu. Chỉ cần Node.js; khoảng 2 phút |
Chạy xong thì npm run lab:down: các phép đo ghi rất nhiều, và volume của MySQL có thể lớn nhanh nếu để lâu.
Đọc kết quả
- Client
mysqlkhông in gì khi kết quả rỗng, cònpsqlin(0 rows). Mỗi bước trong file.sqlcó một dòng nhãn--- …, để biết bảng kết quả nào thuộc bước nào. - Các file
.sqlchạy tiếp khi gặp lỗi, vì nhiều bước cố ý gây lỗi để xem thông báo. - Thời gian dao động nhiều giữa các lần chạy, nhất là trên laptop. So bậc độ lớn và tỉ lệ giữa các cách làm, không so con số tuyệt đối. Các trang ghi khoảng giá trị qua nhiều lượt chạy vì lý do này.
Môi trường
lab/docker-compose.yml
yml
# Môi trường chạy thử cho sổ tay: MySQL 8.0 (source), một MySQL replica dùng GTID, PostgreSQL 17.
# Dựng: npm run up (trong lab/) hoặc npm run lab:up (ở thư mục gốc). Xoá: npm run down.
name: interview-lab
x-mysql-health: &mysql-health
# Kết nối qua TCP: lúc khởi tạo lần đầu MySQL chạy một server tạm không mở TCP,
# nên healthcheck chỉ qua khi server thật đã sẵn sàng.
test: ['CMD', 'mysql', '--protocol=TCP', '-h127.0.0.1', '-uroot', '-ppw', '-e', 'SELECT 1']
interval: 3s
timeout: 5s
retries: 60
services:
mysql:
image: mysql:8.0
container_name: lab-mysql
# Các phép đo cập nhật hàng triệu dòng, mỗi lần ghi vài trăm MB binlog. Không giới hạn thì binlog tích luỹ
# qua nhiều lần chạy (đã từng lên hơn 40 GB): xoay file khi đạt 100 MB, xoá file cũ hơn 30 phút.
command: --server-id=1 --gtid-mode=ON --enforce-gtid-consistency=ON --max-binlog-size=104857600 --binlog-expire-logs-seconds=1800
environment:
MYSQL_ROOT_PASSWORD: pw
ports: ['127.0.0.1:33306:3306']
healthcheck: *mysql-health
replica:
image: mysql:8.0
container_name: lab-replica
# Replica không cần binlog riêng (không có ai đọc từ nó); relay log tự xoá sau khi áp dụng.
command: --server-id=2 --gtid-mode=ON --enforce-gtid-consistency=ON --skip-log-bin
environment:
MYSQL_ROOT_PASSWORD: pw
ports: ['127.0.0.1:33307:3306']
healthcheck: *mysql-health
postgres:
image: postgres:17-alpine
container_name: lab-pg
environment:
POSTGRES_PASSWORD: pw
ports: ['127.0.0.1:55432:5432']
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
interval: 2s
timeout: 5s
retries: 30lab/common/run-sql.mjs: chạy file .sql bằng client trong container
js
// Chạy một file .sql bằng client dòng lệnh trong container (mysql hoặc psql) và in kết quả.
// Dùng client thay vì driver để giữ đúng thông báo lỗi và lệnh riêng của psql như \timing, \echo.
// node common/run-sql.mjs mysql viet_sql viet-sql/common.sql
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { CONTAINERS } from './config.mjs'
const LAB = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
function docker(args, input) {
const r = spawnSync('docker', args, { input, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 })
if (r.error) throw r.error
return (r.stdout ?? '') + (r.stderr ?? '')
}
export function ensureDb(engine, db) {
if (engine === 'mysql') {
docker(['exec', '-i', CONTAINERS.mysql, 'mysql', '-uroot', '-ppw', '-e', `CREATE DATABASE IF NOT EXISTS ${db}`])
} else {
const has = docker(['exec', CONTAINERS.pg, 'psql', '-U', 'postgres', '-tAc', `SELECT 1 FROM pg_database WHERE datname = '${db}'`])
if (!has.includes('1')) docker(['exec', CONTAINERS.pg, 'createdb', '-U', 'postgres', db])
}
}
// --force: chạy tiếp sau lỗi, vì nhiều phép thử cố ý gây lỗi để xem thông báo.
// --default-character-set=utf8mb4: thiếu cờ này thì chữ có dấu bị lỗi bảng mã.
export function runSql(engine, db, file) {
ensureDb(engine, db)
const sql = fs.readFileSync(path.resolve(LAB, file), 'utf8')
const out = engine === 'mysql'
? docker(['exec', '-i', CONTAINERS.mysql, 'mysql', '--default-character-set=utf8mb4', '--force', '-t', '-uroot', '-ppw', db], sql)
: docker(['exec', '-i', CONTAINERS.pg, 'psql', '-U', 'postgres', '-q', '-d', db], sql)
return out.split('\n').filter((l) => !l.includes('Using a password on the command line')).join('\n')
}
export function show(engine, db, file) {
console.log(`\n========== ${engine === 'mysql' ? 'MySQL' : 'PostgreSQL'} · ${file} ==========`)
console.log(runSql(engine, db, file))
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const [engine, db, file] = process.argv.slice(2)
show(engine, db, file)
}