Chấm xanh online và bài toán hit database: Tại sao một triệu người dùng chỉ cần khoảng 122 KB RAM?

Hiển thị trạng thái trực tuyến bằng một chấm xanh nhỏ cạnh tên người dùng là tính năng xuất hiện ở khắp nơi: mạng xã hội, diễn đàn, hệ thống chat, nền tảng cộng đồng và các ứng dụng cộng tác thời gian thực.

Chấm xanh online và bài toán hit database: Tại sao một triệu người dùng chỉ cần khoảng 122 KB RAM?

Nhìn bề ngoài, đây là một tính năng rất đơn giản. Tuy nhiên phía sau nó lại là một bài toán hiệu năng thú vị. Nếu mỗi lần người dùng truy cập website đều cập nhật trạng thái vào database, hệ thống sẽ nhanh chóng tạo ra hàng nghìn hoặc hàng triệu truy vấn ghi mỗi ngày chỉ để phục vụ một chấm màu nhỏ trên giao diện.

Trong bài viết này, chúng ta sẽ xây dựng một hệ thống User Presence hiệu năng cao bằng Redis Bitmap. Mục tiêu là xác định trạng thái online/offline của hàng triệu người dùng mà không cần cập nhật database liên tục, không cần cron dọn dẹp trạng thái và chỉ tiêu tốn vài trăm KB RAM.

Vấn đề của cách tiếp cận truyền thống

Phần lớn các hệ thống bắt đầu bằng cách lưu thời gian hoạt động cuối cùng của người dùng.

user_id = 123
last_seen = 1749416400

Mỗi request sẽ cập nhật một timestamp mới:

UPDATE usermeta
SET last_seen = NOW()
WHERE user_id = 123

Khi cần hiển thị trạng thái trực tuyến, hệ thống sẽ kiểm tra:

NOW() - last_seen < 300 giây

Cách làm này hoạt động tốt ở quy mô nhỏ, nhưng khi số lượng người dùng tăng lên, lượng truy vấn ghi vào database cũng tăng theo. Trong thực tế, phần lớn dữ liệu này chỉ phục vụ cho việc xác định online/offline và không có giá trị lâu dài.

Nói cách khác, database đang phải xử lý một bài toán mà RAM làm tốt hơn rất nhiều.

Bitmap: Mỗi người dùng chỉ cần một bit

Thay vì lưu timestamp cho từng người dùng, chúng ta có thể biểu diễn trạng thái online bằng một bitmap.

Mỗi User ID tương ứng với một bit:

User ID 1  => Bit 1
User ID 2  => Bit 2
User ID 3  => Bit 3
...
User ID N  => Bit N

Khi người dùng hoạt động:

$redis->setbit(
    'init_html_presence',
    $user_id,
    1
);

Khi cần kiểm tra:

$redis->getbit(
    'init_html_presence',
    $user_id
);

Đây chính là điểm thú vị của Bitmap.

Một triệu người dùng:

1,000,000 bit
= 125,000 byte
≈ 122 KB

Nói cách khác, toàn bộ trạng thái online của một triệu tài khoản có thể nằm gọn trong khoảng hơn một trăm KB RAM.

Bài toán khó hơn: Làm sao biết người dùng đã offline?

Việc đánh dấu online rất đơn giản:

$redis->setbit(
    $key,
    $user_id,
    1
);

Tuy nhiên khi người dùng đóng trình duyệt, mất điện hoặc mất kết nối mạng, hệ thống không nhận được tín hiệu nào để chuyển bit trở lại thành 0.

Đây là lý do nhiều hệ thống quay lại mô hình timestamp.

Thay vì làm vậy, chúng ta có thể sử dụng kỹ thuật Sliding Presence Window.

Sliding Presence Window

Ý tưởng rất đơn giản.

Thay vì chỉ có một bitmap duy nhất, hệ thống tạo bitmap theo từng khung thời gian.

init_html_pres_bm:100
init_html_pres_bm:101
init_html_pres_bm:102
...

Mỗi block đại diện cho 5 phút:

$window_size = 300;

$current_block =
    floor(time() / $window_size);

Khi người dùng hoạt động:

$cache_key =
    'init_html_pres_bm:' .
    $current_block;

$redis->setbit(
    $cache_key,
    $user_id,
    1
);

Đồng thời key được thiết lập thời gian sống:

$redis->expire(
    $cache_key,
    $window_size * 2
);

Khi kiểm tra trạng thái online:

$current =
    $redis->getbit(
        $key_current,
        $user_id
    );

$previous =
    $redis->getbit(
        $key_previous,
        $user_id
    );

$is_online =
    $current || $previous;

Nếu người dùng xuất hiện trong block hiện tại hoặc block trước đó, họ được xem là đang online.

Không cần timestamp. Không cần cleanup job. Không cần ghi database.

Khi block hết hạn, Redis tự động giải phóng toàn bộ dữ liệu.

Giải quyết N+1 Query bằng Redis Pipeline

Một vấn đề khác xuất hiện khi hiển thị danh sách người dùng.

Giả sử một trang có 200 bình luận từ 200 tài khoản khác nhau.

Nếu mỗi người dùng cần:

GETBIT current
GETBIT previous

thì tổng cộng sẽ phát sinh 400 lần giao tiếp với Redis.

Giải pháp là Redis Pipeline.

$pipe = $redis->multi(
    Redis::PIPELINE
);

foreach ($user_ids as $id) {
    $pipe->getbit(
        $key_current,
        $id
    );

    $pipe->getbit(
        $key_previous,
        $id
    );
}

$results = $pipe->exec();

Toàn bộ dữ liệu được gửi và nhận trong một lần trao đổi duy nhất, loại bỏ hoàn toàn hiện tượng N+1 Query ở tầng cache.

L1 Runtime Cache

Ngay cả khi đã sử dụng Pipeline, một trang vẫn có thể kiểm tra trạng thái của cùng một người dùng nhiều lần.

Ví dụ:

  • Tên tác giả bài viết
  • Danh sách bình luận
  • Widget thành viên
  • Hộp chat

Một Runtime Cache đơn giản trong RAM PHP sẽ loại bỏ các truy vấn lặp lại:

static $runtime_cache = [];

if (
    isset(
        $runtime_cache[$user_id]
    )
) {
    return
        $runtime_cache[$user_id];
}

Từ thời điểm đó trở đi, việc kiểm tra trạng thái online trở thành thao tác đọc bộ nhớ trong cùng request.

Kết quả cuối cùng

Sau khi kết hợp:

  • Redis Bitmap
  • Sliding Presence Window
  • Redis Pipeline
  • Runtime Memoization Cache

hệ thống đạt được các đặc tính sau:

  • Không ghi database trong quá trình theo dõi trạng thái online.
  • Không cần cron dọn dẹp trạng thái người dùng.
  • Không cần lưu timestamp cho từng tài khoản.
  • Chi phí RAM cực thấp.
  • Khả năng mở rộng tới hàng triệu người dùng.
  • Độ phức tạp kiểm tra trạng thái đạt O(1).

Một tính năng tưởng như rất nhỏ trên giao diện thực tế lại là ví dụ điển hình cho việc lựa chọn đúng cấu trúc dữ liệu. Khi bài toán được chuyển từ database sang bitmap trong RAM, hiệu năng thay đổi hoàn toàn. Một triệu trạng thái người dùng chỉ cần khoảng 122 KB bộ nhớ, trong khi lượng truy vấn ghi vào database gần như bằng không.

Bình luận


  • Không có bình luận.

Công cụ trực tuyến

Nhấn Ctrl + \ trên máy tính, hoặc vuốt sang trái ở bất kỳ đâu trên mobile.

Đăng nhập





Đang tải...