NHT

Redis Bitmaps: Theo Dõi Hàng Triệu User Hoạt Động Chỉ Với 122KB RAM

Hướng dẫn chi tiết về thiết kế hệ thống theo dõi trạng thái online/offline của người dùng bằng Redis Bitmaps. Tính toán dung lượng RAM tối ưu, nắm vững các câu lệnh cốt lõi và xử lý các vấn đề thực tế.

Nguyen Hoang TuanNguyen Hoang Tuan3 thg 6, 20268 phút đọc

Một trong những yêu cầu phổ biến nhất trong các ứng dụng mạng xã hội, dịch vụ chat hoặc trang quản trị cộng tác hiện nay là theo dõi trạng thái online/offline của người dùng. Mới nhìn qua, đây có vẻ là một tính năng đơn giản chỉ cần lưu một giá trị Boolean. Tuy nhiên, khi hệ thống của bạn mở rộng quy mô lên hàng triệu người dùng hoạt động, một cách triển khai ngây thơ có thể nhanh chóng làm sập database của bạn.

Bài viết này sẽ hướng dẫn bạn giải pháp thiết kế hệ thống kinh điển: Redis Bitmaps. Bằng cách biểu diễn trạng thái của người dùng dưới dạng các bit nhị phân, bạn có thể theo dõi hàng triệu người dùng chỉ với chưa đầy một megabyte RAM cùng tốc độ đọc/ghi đạt O(1).


Vấn đề: Chi phí đắt đỏ của cột is_online

Trong một cơ sở dữ liệu quan hệ truyền thống, bạn thường thêm cột is_online vào bảng users hoặc lưu một timestamp dạng last_active_at. Mỗi khi người dùng tải trang, click nút, hoặc thực hiện bắt tay kết nối WebSocket, ứng dụng của bạn sẽ chạy một câu lệnh cập nhật:

UPDATE users SET is_online = 1, last_active_at = NOW() WHERE id = 12345;

Cách làm này hoàn toàn ổn với vài nghìn người dùng. Nhưng khi lên quy mô lớn, phương pháp này sẽ gặp các nút thắt cổ chai nghiêm trọng:

  • Write Amplification (Khuếch đại ghi): Các hệ quản trị cơ sở dữ liệu được tối ưu hóa cho việc đọc có cấu trúc, việc ghi dữ liệu tương đối chậm do phải cập nhật index, ghi vào transaction log (WAL) và đồng bộ xuống ổ đĩa.
  • Exhaustion Connection Pool (Cạn kiệt kết nối): Hàng triệu người dùng hoạt động gửi tín hiệu heartbeat (nhịp tim) mỗi vài giây sẽ nhanh chóng làm quá tải kết nối của database pool.
  • Lock Contention (Tranh chấp khóa): Cập nhật liên tục trên cùng một dòng của bảng người dùng sẽ gây ra chi phí khóa (locking overhead), làm giảm hiệu năng của các tính năng quan trọng khác.

Giải pháp: Redis Bitmap là gì?

Thay vì biểu diễn trạng thái online dưới dạng một dòng dữ liệu hoặc một bản ghi trong bảng, chúng ta có thể biểu diễn nó dưới dạng một chữ số nhị phân (bit) duy nhất:

  • 1 đại diện cho Online
  • 0 đại diện cho Offline

Redis cung cấp một tập hợp các lệnh chuyên biệt cho phép xử lý các chuỗi ký tự (string) như một mảng các bit. Tính năng này được gọi là Bitmaps. Trong một Redis Bitmap, vị trí (offset) của bit trong mảng sẽ tương ứng với ID của người dùng, và giá trị của bit (0 hoặc 1) đại diện cho trạng thái của họ.

Vì Redis hoạt động hoàn toàn trên bộ nhớ RAM (in-memory), các thao tác trên bit diễn ra cực kỳ nhanh chóng—đạt thời gian không đổi O(1) cho cả việc cập nhật và truy vấn trạng thái.


Bài toán bộ nhớ: Tính toán dung lượng RAM cần thiết

Để thấy được sức mạnh của Bitmap, hãy cùng làm một phép tính toán bộ nhớ đơn giản. Trong một Bitmap:

Dung lượng bộ nhớ = 1 bit cho mỗi người dùng

Hãy tính dung lượng RAM cần thiết để lưu trữ trạng thái online của 1.000.000 người dùng:

  1. Tổng số bit: 1.000,000 bits
  2. Quy đổi ra Bytes: 1.000.000 bits / 8 = 125.000 bytes
  3. Quy đổi ra Kilobytes: 125.000 bytes / 1024 ≈ 122 KB

Dưới đây là bảng so sánh mức tiêu thụ bộ nhớ theo số lượng người dùng:

| Số lượng người dùng | Tổng số bit | Dung lượng bộ nhớ (KB/MB) | | :--- | :--- | :--- | | 1.000.000 | 1.000.000 bits | ~122 KB | | 5.000.000 | 5.000.000 bits | ~610 KB | | 10.000.000 | 10.000.000 bits | ~1.2 MB | | 50.000.000 | 50.000.000 bits | ~6.0 MB |

Ngay cả khi bạn có 50 triệu người dùng đăng ký, toàn bộ index trạng thái hoạt động của bạn chỉ tốn vỏn vẹn 6 megabytes RAM. Đây là một chi phí cực kỳ nhỏ cho bất kỳ server hiện đại nào.


Triển khai thực tế: Các lệnh Redis cốt lõi

Việc sử dụng Bitmap trong Redis chỉ yêu cầu 3 câu lệnh đơn giản: SETBIT, GETBIT, và BITCOUNT. Giả sử chúng ta sử dụng key user:online:bitmap.

1. Đánh dấu người dùng Online

Khi người dùng 12345 kết nối (ví dụ: tạo kết nối WebSocket thành công), hãy đặt bit tại vị trí 12345 thành 1:

SETBIT user:online:bitmap 12345 1

2. Đánh dấu người dùng Offline

Khi người dùng 12345 ngắt kết nối hoặc đăng xuất, hãy đặt bit về lại 0:

SETBIT user:online:bitmap 12345 0

3. Kiểm tra trạng thái của một người dùng

Để kiểm tra xem người dùng 12345 có đang online hay không:

GETBIT user:online:bitmap 12345
# Trả về 1 (Online) hoặc 0 (Offline)

4. Đếm tổng số người dùng đang Online

Để lấy tổng số người dùng đang trực tuyến tại thời điểm hiện tại (đếm tổng số bit 1 trong bitmap):

BITCOUNT user:online:bitmap
# Trả về tổng số lượng người dùng đang hoạt động

Các vấn đề thực tế trong môi trường Production & Giải pháp

Mặc dù Bitmaps hoạt động cực kỳ hiệu quả, việc đưa chúng vào hệ thống production thực tế đòi hỏi bạn phải giải quyết một số vấn đề quan trọng sau.

Vấn đề 1: ID dạng UUID (String) và ID phân tán (Sparse IDs)

Vị trí bit (offset) trong Redis phải là một số nguyên. Nếu hệ thống của bạn sử dụng ID dạng UUID (ví dụ: e5b02bcf-a8e5-4d7a...), bạn không thể sử dụng chúng trực tiếp làm offset.

Thêm vào đó, Redis cấp phát bộ nhớ động dựa trên bit có offset cao nhất. Nếu bạn chỉ có hai người dùng hoạt động với ID là 199.999.999, Redis vẫn sẽ cấp phát bộ nhớ cho toàn bộ 100 triệu bit ở giữa—tiêu tốn khoảng 12MB RAM chỉ để lưu trữ trạng thái của hai người dùng.

Giải pháp:

  • Auto-Increment IDs: Đảm bảo bạn chỉ sử dụng ID số nguyên tự tăng và liên tục.
  • ID Mapping: Nếu bắt buộc phải dùng UUID, hãy duy trì một Redis Hash (hoặc một dịch vụ nội bộ) để ánh xạ các UUID này thành một chuỗi số nguyên liên tục bắt đầu từ 0.

Vấn đề 2: Ngắt kết nối đột ngột (Bài toán Heartbeat)

Điều gì xảy ra nếu điện thoại của người dùng hết pin, họ mất kết nối mạng đột ngột hoặc ứng dụng bị crash? Server của bạn sẽ không có cơ hội để chạy lệnh SETBIT user:online:bitmap <user_id> 0. Người dùng đó sẽ hiển thị trạng thái online vĩnh viễn.

Hơn nữa, các bit riêng lẻ trong một Redis Bitmap không hỗ trợ cơ chế tự hủy (TTL). Bạn không thể set thời hạn hết hạn cho một bit cụ thể.

Giải pháp:

  • WebSocket Heartbeats: Yêu cầu thiết bị client gửi tín hiệu ping (heartbeat) lên server định kỳ mỗi 1-5 phút. Mỗi khi nhận được heartbeat, server chạy lại lệnh SETBIT <id> 1.
  • Key Rotation (Xoay vòng Key): Để dọn dẹp các user đột ngột mất kết nối mà không gửi tín hiệu offline, hãy xóa hoặc xoay vòng các bitmap định kỳ.

Mở rộng quy mô: Sử dụng Time-Bucketed Bitmaps

Để giải quyết vấn đề tự động dọn dẹp dữ liệu heartbeat và thu thập thêm dữ liệu phân tích sâu hơn, một mô hình phổ biến là sử dụng Time-Bucketed Bitmaps (lưu trữ theo các khung thời gian như giờ hoặc ngày).

Thay vì sử dụng một key chung duy nhất, chúng ta lưu trữ hoạt động theo các key chứa ngày hoặc giờ:

  • user:online:2026-06-03
  • user:online:2026-06-03:10h

Mỗi khi người dùng tương tác, hãy cập nhật bit của họ trên key bitmap của giờ/ngày hiện tại:

SETBIT user:online:2026-06-03:10h 12345 1
EXPIRE user:online:2026-06-03:10h 86400  # Set key TTL là 24 giờ

Bằng cách thêm TTL cho toàn bộ key, dữ liệu của người dùng không hoạt động sẽ tự động được Redis giải phóng khi key hết hạn, giúp tránh rò rỉ bộ nhớ.

Ứng dụng thực tế: Tính toán chỉ số DAU và MAU cực nhanh

Việc lưu trữ người dùng hoạt động hàng ngày trong các bitmap riêng biệt cho phép bạn thực hiện các câu lệnh phân tích thống kê ngay lập tức bằng các phép toán trên bit. Ví dụ, để tính toán lượng người dùng hoạt động trong vòng 3 ngày liên tiếp Daily Active Users (DAU), bạn chỉ cần dùng lệnh BITOP OR để gộp các key và đếm:

# Thực hiện phép toán bit OR trên bitmap của 3 ngày và lưu kết quả vào key tạm thời
BITOP OR dau:three_days user:online:2026-06-01 user:online:2026-06-02 user:online:2026-06-03

# Đếm tổng số lượng người dùng duy nhất đã đăng nhập trong 3 ngày đó
BITCOUNT dau:three_days

Thao tác này chạy trực tiếp trên bộ nhớ của Redis server và chỉ mất vài mili giây, giúp bạn tránh được các câu lệnh SQL truy vấn cực kỳ chậm trên bảng lưu log sự kiện thô.


Tổng kết & Khuyến nghị

Redis Bitmaps là sự lựa chọn tuyệt vời khi:

  • Bạn cần theo dõi trạng thái nhị phân tốc độ cao, chi phí cực thấp (ví dụ: trạng thái online/offline, bật/tắt tính năng, đăng ký nhận tin).
  • ID người dùng của bạn là chuỗi số nguyên liên tục và có mật độ dày.
  • Bạn cần thực hiện các thống kê nhanh như DAU/MAU.

Tuy nhiên, hãy tránh sử dụng Bitmap nếu:

  • Bạn cần theo dõi các trạng thái phức tạp hơn (ví dụ: "Bận", "Đang họp", "Vui lòng không làm phiền").
  • Bạn cần lưu trữ thông tin đi kèm, chẳng hạn như địa chỉ IP của người dùng, loại thiết bị hoặc mốc thời gian chính xác họ offline.
  • Tập hợp ID người dùng của bạn quá rời rạc hoặc phụ thuộc hoàn toàn vào UUID mà không có lớp ánh xạ trung gian.

Nếu bạn đang trong quá trình mở rộng hệ thống backend và database, việc xây dựng một cấu trúc nền móng vững chắc là vô cùng quan trọng. Tìm hiểu thêm về cách thiết kế tại bài viết Kiến trúc Modular trong NestJS: Xây dựng API Production Quy Mô Lớn, hoặc đảm bảo các trang giao diện công khai được tối ưu hóa SEO với Checklist SEO cho Next.js App Router.

Hệ thống của bạn đang gặp vấn đề hiệu năng hay mở rộng tải?

Tôi chuyên xây dựng hạ tầng bản đồ độ trễ thấp, streaming pipeline thời gian thực (Kafka, ClickHouse) và các hệ thống backend tối ưu. Hãy cùng hợp tác để nâng cấp sản phẩm của bạn.

Hợp tác ngay

Tác giả

Nguyen Hoang Tuan

Nguyen Hoang Tuan

Full-stack developer focused on practical backend architecture, web performance, and production delivery.

Bài viết liên quan