Danh sách bài viết

Bài 2: Redis Architecture — Event Loop, I/O Threads, RESP

Bài này mổ xẻ kiến trúc bên trong của Redis 7.x: vì sao command execution chạy trên một thread duy nhất nên mọi lệnh đều atomic mà không cần lock, cách event loop kết hợp I/O multiplexing (epoll / kqueue) phục vụ hàng nghìn connection trên một thread, và làm rõ hiểu lầm phổ biến rằng Redis hoàn toàn single-threaded khi từ Redis 6.0 đã có io-threads. Bạn cũng sẽ hiểu giao thức RESP, vì sao Redis hiếm khi CPU-bound, và hệ quả nguy hiểm khi chạy lệnh O(N) như KEYS * trên production.

25/05/2026
15 phút đọc
0 lượt xem
1

Mục Tiêu Bài Học

  • Hiểu vì sao Redis thực thi command trên một thread duy nhất và hệ quả: mọi lệnh đều atomic, không cần lock ở phía Redis.
  • Nắm cách event loop kết hợp I/O multiplexing (epoll trên Linux, kqueue trên BSD/macOS) phục vụ hàng nghìn connection chỉ với một thread.
  • Sửa hiểu lầm "Redis hoàn toàn single-threaded": phân biệt I/O threads (đọc/ghi/parse socket) và command execution (vẫn trên main thread).
  • Giải thích vì sao Redis nhanh dù single-threaded execution.
  • Đọc được giao thức RESP (RESP2 vs RESP3) và hình dung wire format.
  • Phân biệt CPU-boundnetwork/memory-bound; hiểu vì sao thêm CPU core không tự tăng throughput command.
  • Nhận ra mối nguy của lệnh O(N) (KEYS *, HGETALL hash khổng lồ, SMEMBERS set lớn) block toàn bộ server.
2

Single-threaded Command Execution

Đặc điểm kiến trúc quan trọng nhất của Redis, và cũng là chìa khoá tư duy cho toàn series: việc thực thi command diễn ra trên duy nhất một thread chính (main thread). Mọi lệnh từ mọi client xếp hàng và được xử lý tuần tự, từng lệnh một.

Hệ quả trực tiếp: mỗi lệnh Redis là atomic theo nghĩa không có lệnh nào khác chen vào giữa chừng. Khi INCR counter đang chạy, không có client nào khác đọc/ghi counter tại đúng thời điểm đó. Bạn không cần lock ở phía Redis để bảo vệ một thao tác đơn lẻ — điều mà với một database đa luồng bình thường bạn phải tự lo.

Hãy hình dung: 100 client cùng gửi INCR pageviews. Redis không chạy song song 100 lệnh rồi tranh chấp; nó đặt chúng vào một hàng đợi và chạy lần lượt. Kết quả cuối cùng luôn cộng đủ 100 — không bao giờ "mất tích" vì race condition.

127.0.0.1:6379> SET counter 0
OK
# Dù 100 client cùng INCR, kết quả luôn chính xác = 100
127.0.0.1:6379> INCR counter
(integer) 1
127.0.0.1:6379> INCR counter
(integer) 2

Cần phân biệt rõ hai khái niệm "atomic":

  • Atomic theo từng lệnh: mặc định, do single-threaded execution. INCR, SETNX, LPUSH... đều an toàn.
  • Atomic theo nhóm lệnh: nếu cần nhiều lệnh chạy liền nhau không bị xen, bạn dùng MULTI/EXEC (transaction) hoặc Lua script (EVAL) — Redis sẽ chạy cả khối như một đơn vị không thể chia cắt.

Đây không phải hạn chế cần khắc phục, mà là một lựa chọn thiết kế có chủ đích: bỏ đi sự phức tạp của locking, condition variable, và race condition vốn là nguồn cơn bug khó tái hiện nhất trong hệ thống đa luồng.

3

Event Loop & I/O Multiplexing

Câu hỏi tự nhiên: nếu chỉ một thread, làm sao Redis phục vụ được hàng nghìn connection cùng lúc mà không cần một thread cho mỗi client? Câu trả lời là event loop dựa trên I/O multiplexing.

Mô hình "một thread mỗi connection" (thread-per-connection) tốn kém: mỗi thread chiếm bộ nhớ stack, và context switch giữa hàng nghìn thread khiến CPU bận chuyển ngữ cảnh thay vì làm việc thật. Redis đi hướng khác: một thread duy nhất hỏi hệ điều hành "trong tất cả socket tôi đang giữ, cái nào đã sẵn sàng để đọc hoặc ghi?" rồi chỉ xử lý đúng những socket đó.

Cơ chế hỏi này do kernel cung cấp, tuỳ hệ điều hành Redis chọn loại nhanh nhất:

  • Linux: epoll — hiệu quả với số lượng connection lớn, độ phức tạp gần như không phụ thuộc tổng số socket.
  • BSD / macOS: kqueue.
  • Solaris: evport; và select làm fallback cho nền tảng cũ.

Redis bọc các cơ chế này trong một lớp event library nội bộ tên ae (a-simple-event-driven-library), tự chọn backend tốt nhất khi biên dịch. Vòng lặp về bản chất hoạt động như sau:

# Pseudocode mô tả event loop của Redis
while (server_đang_chạy):
    # 1. Hỏi kernel: socket nào sẵn sàng? (epoll/kqueue), có timeout
    events = poll(các_socket, timeout)

    # 2. Với mỗi socket sẵn sàng, gọi handler tương ứng
    for ev in events:
        if ev là READABLE:    đọc request, parse, THỰC THI command, xếp reply
        if ev là WRITABLE:    ghi reply ra socket

    # 3. Chạy các tác vụ định kỳ (expire key, RDB/AOF, thống kê...)
    process_time_events()

Điểm mấu chốt: vì xử lý là non-blocking và sự kiện đến rất nhanh, một thread vẫn quay vòng đủ nhanh để mọi client cảm thấy như đang được phục vụ song song. Nhưng "song song" ở đây là concurrency (xen kẽ rất nhanh), không phải parallelism (chạy thật sự cùng lúc trên nhiều core).

4

I/O Threads: Sửa Hiểu Lầm "Hoàn Toàn Single-threaded"

Câu nói "Redis hoàn toàn single-threaded" là không chính xác và là hiểu lầm phổ biến nhất về Redis. Thực tế Redis dùng thread phụ cho nhiều việc từ lâu (lazy free, ghi AOF, đóng file lớn trong background), và đặc biệt từ Redis 6.0 có thêm tính năng I/O threads (tham số io-threads).

Vậy I/O threads làm gì, và ranh giới với single-threaded nằm ở đâu? Hãy tách một request thành các giai đoạn:

  1. Read & parse: đọc byte từ socket, parse chuỗi RESP thành lệnh.
  2. Execute: thực thi lệnh, đọc/ghi vào cấu trúc dữ liệu trong bộ nhớ.
  3. Write: serialize kết quả thành RESP và ghi ra socket.

Khi bật io-threads, Redis dùng nhiều thread để song song hoá giai đoạn (1) và (3) — phần đọc/ghi socket và parse, vốn tốn thời gian khi có nhiều connection và payload lớn. NHƯNG giai đoạn (2) — thực thi command — vẫn diễn ra trên main thread, vẫn tuần tự, vẫn atomic.

# Mô hình khi bật io-threads (Redis 6.0+)
#
#   [io-thread 1] đọc+parse socket A ┐
#   [io-thread 2] đọc+parse socket B ├─ song song
#   [io-thread 3] đọc+parse socket C ┘
#                       │
#                       ▼
#   [main thread]  EXECUTE lệnh A, rồi B, rồi C   ← VẪN tuần tự, atomic
#                       │
#   [io-thread 1] ghi reply socket A ┐
#   [io-thread 2] ghi reply socket B ├─ song song
#   [io-thread 3] ghi reply socket C ┘

Vì vậy bật I/O threads không phá vỡ tính atomic của từng lệnh, và cũng không biến Redis thành "command chạy song song". Nó chỉ giúp gỡ nút thắt ở khâu I/O khi máy có nhiều core và lượng kết nối lớn.

Lưu ý thực tế:

  • Mặc định io-threads 1 — tức tắt, mọi thứ trên main thread.
  • Chỉ cân nhắc bật khi Redis thực sự bị nghẽn ở I/O (nhiều connection, payload lớn) và máy có nhiều core rảnh. Với đa số workload nhỏ, bật I/O threads không cải thiện, thậm chí thêm overhead.
  • Số thread khuyến nghị thường nhỏ (ví dụ 2–4); đừng đặt bằng tổng số core.
5

Vì Sao Redis Nhanh

Nghịch lý dễ gây ngạc nhiên: command execution chỉ một thread, vậy mà Redis đạt hàng trăm nghìn đến hàng triệu ops/giây. Lý do không nằm ở số thread, mà ở những yếu tố sau:

  • In-memory: dữ liệu nằm trong RAM, mỗi thao tác chỉ là truy cập bộ nhớ (nano giây) thay vì đọc đĩa (mili giây). Đây là yếu tố lớn nhất.
  • Không lock contention: single-threaded execution loại bỏ hoàn toàn chi phí khoá, chờ khoá, và context switch để tranh chấp tài nguyên — thứ ngốn rất nhiều thời gian trong DB đa luồng.
  • Cấu trúc dữ liệu tối ưu: Redis cài đặt cẩn thận từng kiểu (hash table, skiplist cho sorted set, các encoding gọn như listpack/intset cho dữ liệu nhỏ) để mỗi thao tác đạt độ phức tạp tốt nhất có thể.
  • I/O multiplexing: một thread phục vụ hàng nghìn connection mà không tốn chi phí thread-per-connection.
  • Giao thức gọn: RESP đơn giản, parse nhanh, ít overhead — sẽ nói ở phần sau.

Tư duy rút ra: tốc độ Redis đến từ việc mỗi đơn vị công việc cực kỳ rẻ và không phải chờ nhau, chứ không phải từ việc làm nhiều việc song song. Đó cũng là lý do vì sao một lệnh "đắt" duy nhất lại có thể làm hỏng bức tranh — chủ đề ở section 8.

6

RESP Protocol: RESP2 vs RESP3

Client và server giao tiếp qua RESP (REdis Serialization Protocol) — một giao thức dạng text, dễ đọc bằng mắt người, theo mô hình request-response: client gửi một lệnh, server trả về một reply.

Mỗi phần của message bắt đầu bằng một byte chỉ kiểu, kết thúc mỗi dòng bằng \r\n (CRLF). Trong RESP2, các kiểu cơ bản:

  • + Simple String — ví dụ +OK\r\n
  • - Error — ví dụ -ERR unknown command\r\n
  • : Integer — ví dụ :1000\r\n
  • $ Bulk String (kèm độ dài) — ví dụ $5\r\nhello\r\n
  • * Array (kèm số phần tử) — ví dụ *2\r\n...\r\n...\r\n

Client gửi lệnh dưới dạng array các bulk string. Ví dụ lệnh PING trên wire chính xác là:

# Client gửi: PING
*1\r\n$4\r\nPING\r\n
#  ^   ^
#  |   └─ bulk string dài 4 byte: "PING"
#  └───── array gồm 1 phần tử

# Server trả: +PONG\r\n

Một ví dụ phong phú hơn — lệnh SET mykey hello:

# Client gửi: SET mykey hello  (array 3 phần tử)
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nhello\r\n

# Server trả: +OK\r\n

RESP2 vs RESP3:

  • RESP2: mặc định lịch sử, chỉ có vài kiểu cơ bản. Thiếu kiểu như map/set/boolean nên client phải tự suy diễn cấu trúc trả về (ví dụ HGETALL trả về một array phẳng key, value xen kẽ).
  • RESP3: giới thiệu từ Redis 6.0, bổ sung nhiều kiểu mới — Map (%), Set (~), Double (,), Boolean (#), Big number, Verbatim string, Null rõ ràng (_)... giúp reply giàu ngữ nghĩa hơn và cho phép tính năng client-side caching (push message qua cùng connection).

Connection mặc định bắt đầu ở RESP2; client chủ động nâng cấp bằng lệnh HELLO 3. Đa số client library hiện đại tự xử lý việc này; là người dùng bạn thường không phải đọc RESP thủ công, nhưng hiểu nó giúp debug và lý giải hành vi (ví dụ vì sao HGETALL trả map ở RESP3 nhưng array ở RESP2).

# Nâng cấp connection lên RESP3 và xem thông tin server
127.0.0.1:6379> HELLO 3
# ... trả về map gồm server, version, proto: 3, ...
7

CPU-bound vs Network/Memory-bound

Vì mỗi lệnh đơn giản (như GET/SET) tốn rất ít CPU, Redis hiếm khi bị giới hạn bởi CPU. Nút thắt thực tế thường là:

  • Network: round-trip time giữa client và server, băng thông, và số lượng request mỗi giây mà network kịp truyền. Đây là lý do pipelining (gộp nhiều lệnh trong một lần gửi) cải thiện throughput rất mạnh — chủ đề của Bài 3.
  • Memory: dung lượng RAM giới hạn lượng dữ liệu lưu được; memory bandwidth giới hạn tốc độ với value lớn.

Hệ quả thực tế cực kỳ quan trọng cho việc capacity planning:

Vì command execution single-threaded, thêm CPU core KHÔNG tự động tăng throughput command của một instance Redis.

Một instance Redis chỉ dùng một core cho execution. Nếu bạn cấp cho nó 32 core, 31 core còn lại không giúp gì cho việc chạy lệnh nhanh hơn (chỉ phần I/O threads và background task tận dụng được một ít). Muốn scale throughput, bạn không tăng core mà:

  • Chạy nhiều instance Redis trên cùng máy (mỗi instance một core), hoặc
  • Dùng Redis Cluster để sharding dữ liệu ra nhiều node (chủ đề Module 9), hoặc
  • Thêm replica để chia tải đọc.

Đây là một trong những hiểu lầm tốn tiền nhất khi vận hành Redis: nâng cấp instance lên nhiều vCPU và kỳ vọng nhanh hơn, trong khi thứ cần làm là tối ưu network (pipelining), giảm payload, hoặc sharding.

8

Hệ Quả: Lệnh O(N) Block Toàn Bộ Server

Đây là hệ quả thực tế quan trọng nhất của single-threaded execution, và là thứ phải khắc cốt ghi tâm khi vận hành Redis production.

Vì main thread chạy lệnh tuần tự, nếu một lệnh chạy lâu (độ phức tạp O(N) trên N rất lớn) thì toàn bộ các client khác phải chờ cho đến khi lệnh đó xong. Trong khoảng thời gian ấy, Redis về cơ bản treo: không trả lời ai, latency của mọi lệnh khác tăng vọt.

Các thủ phạm kinh điển:

  • KEYS * — quét toàn bộ keyspace, O(N) theo tổng số key. Trên DB triệu key, một lệnh có thể block hàng trăm mili giây đến cả giây.
  • HGETALL bigHash — lấy toàn bộ một hash khổng lồ, O(N) theo số field.
  • SMEMBERS bigSet / LRANGE biglist 0 -1 — trả về toàn bộ một collection lớn.
  • FLUSHALL/FLUSHDB đồng bộ, sort/aggregate trên tập lớn, v.v.

Tư duy đúng: luôn hỏi "lệnh này O bao nhiêu, N có thể lớn đến đâu?" trước khi chạy bất cứ gì trên production. Mỗi trang docs của command đều ghi rõ time complexity.

Giải pháp thay thế an toàn:

  • Thay KEYS bằng SCAN — duyệt keyspace theo từng lô nhỏ qua cursor, không block; tương tự có HSCAN, SSCAN, ZSCAN.
  • Thay HGETALL / SMEMBERS trên collection lớn bằng đọc theo phạm vi hoặc HSCAN/SSCAN.
  • Thiết kế dữ liệu để tránh "big key": chia nhỏ hash/set khổng lồ thành nhiều key.
  • Tận dụng SLOWLOGredis-cli --latency để phát hiện lệnh chậm (xem section 9).
9

Code Minh Hoạ: Atomic, io-threads, Đo Độ Chậm

(a) Tính atomic — nhiều client INCR đồng thời vẫn đúng. Dù chạy concurrent, kết quả luôn bằng tổng số lần gọi vì execution tuần tự trên main thread.

TypeScript với ioredis:

import Redis from "ioredis";

const redis = new Redis({ host: "127.0.0.1", port: 6379 });

const KEY = "atomic:counter";
const CLIENTS = 100;
const PER_CLIENT = 1000;

async function main(): Promise<void> {
  await redis.del(KEY);

  // Mô phỏng 100 "client" cùng tăng counter, mỗi bên 1000 lần
  const workers = Array.from({ length: CLIENTS }, async () => {
    for (let i = 0; i < PER_CLIENT; i++) {
      await redis.incr(KEY);
    }
  });

  await Promise.all(workers);

  const total = await redis.get(KEY);
  console.log(`Kết quả = ${total}`); // Luôn = 100000, không bao giờ mất tích
  console.log(`Kỳ vọng = ${CLIENTS * PER_CLIENT}`);

  await redis.quit();
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Python với redis-py (dùng thread để tạo concurrency):

import threading
import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

KEY = "atomic:counter"
CLIENTS = 100
PER_CLIENT = 1000


def worker() -> None:
    # Mỗi thread dùng kết nối riêng nhưng Redis execution vẫn tuần tự
    conn = redis.Redis(host="127.0.0.1", port=6379)
    for _ in range(PER_CLIENT):
        conn.incr(KEY)


def main() -> None:
    r.delete(KEY)

    threads = [threading.Thread(target=worker) for _ in range(CLIENTS)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    total = int(r.get(KEY))
    print(f"Kết quả = {total}")            # Luôn = 100000
    print(f"Kỳ vọng = {CLIENTS * PER_CLIENT}")
    assert total == CLIENTS * PER_CLIENT   # Không cần lock phía Redis


if __name__ == "__main__":
    main()

(b) Bật io-threads trong redis.conf. Chỉ bật khi thực sự nghẽn I/O và máy nhiều core rảnh.

# redis.conf — phần I/O threads
#
# Số lượng I/O threads cho việc đọc/ghi + parse socket.
# Mặc định 1 (tắt). Khuyến nghị giá trị nhỏ, ví dụ 2-4.
io-threads 4

# Mặc định I/O threads chỉ tăng tốc phần GHI reply.
# Bật dòng dưới để cũng song song hoá phần ĐỌC + parse request.
io-threads-do-reads yes

# LƯU Ý: command execution VẪN trên main thread, vẫn atomic.
# Khởi động lại Redis để áp dụng (không đổi io-threads runtime).
# Kiểm tra cấu hình io-threads đang chạy
127.0.0.1:6379> CONFIG GET io-threads
1) "io-threads"
2) "4"

(c) Đo độ chậm của lệnh O(N) bằng SLOWLOG và --latency.

# 1) Cấu hình SLOWLOG ghi lại lệnh chạy quá ngưỡng (đơn vị micro giây)
#    Ví dụ: log mọi lệnh chạy lâu hơn 10ms = 10000 microseconds
127.0.0.1:6379> CONFIG SET slowlog-log-slower-than 10000
OK
127.0.0.1:6379> CONFIG SET slowlog-max-len 128
OK

# 2) Tạo dữ liệu lớn rồi chạy một lệnh O(N) để thấy nó vào slowlog
127.0.0.1:6379> DEBUG POPULATE 1000000      # tạo 1 triệu key
OK
127.0.0.1:6379> KEYS *                       # O(N) — đừng làm trên prod!
# ... treo một lúc ...

# 3) Xem các lệnh chậm gần đây
127.0.0.1:6379> SLOWLOG GET 5
# Mỗi entry: id, timestamp, thời gian chạy (microsecond), lệnh, client...
127.0.0.1:6379> SLOWLOG RESET                # xoá slowlog
# Đo latency tổng thể của server (ngoài terminal, không trong redis-cli)
# Liên tục gửi PING và in latency — thấy spike khi server bị block
redis-cli --latency

# Theo dõi theo cửa sổ thời gian, hiện min/max/avg
redis-cli --latency-history

# Cách an toàn để duyệt keyspace thay cho KEYS:
redis-cli --scan --pattern 'user:*' | head
10

Pitfalls & Anti-patterns

  • Chạy lệnh O(N) trên production. KEYS *, HGETALL hash khổng lồ, SMEMBERS set lớn, LRANGE list 0 -1 — đều block main thread và làm treo mọi client khác. Luôn ước lượng N trước khi chạy.
  • Dùng KEYS thay vì SCAN. KEYS là cái bẫy số một. SCAN/HSCAN/SSCAN/ZSCAN duyệt theo cursor, không block, dù tổng thời gian dài hơn nhưng không làm sập server.
  • Tưởng Redis scale theo số core. Một instance chỉ dùng một core cho execution. Thêm vCPU không tăng throughput command — phải sharding (Cluster), thêm replica, hoặc chạy nhiều instance.
  • Tưởng Redis "hoàn toàn single-threaded". I/O threads (từ 6.0) song song hoá đọc/ghi socket, nhưng execution vẫn một thread. Đừng dựa vào I/O threads để mong lệnh chạy nhanh hơn.
  • Bật io-threads bừa bãi. Với workload nhỏ, bật I/O threads không cải thiện mà còn thêm overhead. Chỉ bật khi đo được nghẽn ở I/O và máy có core rảnh; giữ số thread nhỏ.
  • Lạm dụng MULTI/EXEC để mong tăng tốc. Transaction đảm bảo nhóm lệnh chạy liền không bị xen, nhưng không làm Redis nhanh hơn và một khối lớn vẫn block main thread.
  • Bỏ qua time complexity trong docs. Trang docs mỗi command ghi rõ độ phức tạp; đọc nó là thói quen rẻ tiền giúp tránh sự cố đắt giá.
11

Tổng Kết & Quiz

Tổng kết

  • Redis thực thi command trên một thread duy nhất, tuần tự → mỗi lệnh atomic, không cần lock ở phía Redis. Đây là chìa khoá tư duy của toàn series.
  • Event loop + I/O multiplexing (epoll trên Linux, kqueue trên BSD/macOS) cho phép một thread phục vụ hàng nghìn connection (concurrency, không phải parallelism).
  • "Redis hoàn toàn single-threaded" là sai: từ 6.0 có I/O threads song song hoá đọc/ghi/parse socket, nhưng execution vẫn trên main thread và vẫn atomic.
  • Redis nhanh nhờ in-memory, không lock contention, cấu trúc dữ liệu tối ưu, I/O multiplexing, và giao thức RESP gọn.
  • RESP là giao thức text request-response; RESP3 (từ 6.0) thêm Map/Set/Boolean/Double... và hỗ trợ client-side caching. Wire format lệnh là array các bulk string, ví dụ *1\r\n$4\r\nPING\r\n.
  • Redis thường network/memory-bound, hiếm khi CPU-bound → thêm core không tự tăng throughput command.
  • Lệnh O(N) trên dữ liệu lớn (KEYS *, HGETALL bigHash, SMEMBERS bigSet) block toàn bộ server. Dùng SCAN và đo bằng SLOWLOG / --latency.

Quiz 5 câu

  1. Vì sao mỗi lệnh Redis đơn lẻ là atomic mà không cần lock ở phía Redis? Cơ chế nào đảm bảo điều đó?
  2. Trên Linux, Redis dùng cơ chế I/O multiplexing nào để một thread phục vụ hàng nghìn connection? Trên macOS thì là gì?
  3. Khi bật io-threads (Redis 6.0+), giai đoạn nào của vòng đời request được song song hoá, và giai đoạn nào vẫn chạy trên main thread? Tính atomic có bị phá vỡ không?
  4. Vì sao thêm CPU core cho một instance Redis không tự động tăng throughput command? Muốn scale throughput thì làm gì?
  5. Tại sao KEYS * nguy hiểm trên production, và nên thay bằng lệnh nào? Công cụ nào giúp phát hiện lệnh chạy chậm?

Đáp án gợi ý

  1. Vì command execution chạy trên một main thread duy nhất, tuần tự từng lệnh; không có lệnh nào chen vào giữa chừng nên thao tác đơn lẻ luôn atomic. Cần atomic cho nhóm lệnh thì dùng MULTI/EXEC hoặc Lua script (EVAL).
  2. Trên Linux là epoll; trên BSD/macOS là kqueue (còn có evport trên Solaris và select làm fallback). Redis bọc chúng trong thư viện event ae và tự chọn backend tốt nhất.
  3. I/O threads song song hoá đọc/ghi socket và parse RESP (giai đoạn read & write). Giai đoạn execute command vẫn trên main thread, vẫn tuần tự, nên tính atomic không bị phá vỡ.
  4. Vì execution chỉ dùng một core; core dư không giúp chạy lệnh nhanh hơn. Scale bằng cách sharding với Redis Cluster, thêm replica để chia tải đọc, hoặc chạy nhiều instance; đồng thời tối ưu network bằng pipelining.
  5. KEYS *O(N) theo tổng số key, chạy trên main thread nên block mọi client khác. Thay bằng SCAN (duyệt theo cursor, không block). Phát hiện lệnh chậm bằng SLOWLOGredis-cli --latency.

Bài tiếp theo

Đã hiểu vì sao Redis nhanh và vì sao network thường là nút thắt, Bài 3 đi sâu vào việc khai thác điều đó: Bài 3: Performance Fundamentals — Pipelining, Batching, Connection Pooling — cách gộp lệnh để cắt giảm round-trip và đẩy throughput lên nhiều lần.

Tham khảo