Bài 6: Bitmaps & HyperLogLog - Kỹ thuật đếm số lượng user active (DAU) siêu tối ưu bộ nhớ.
Mục tiêu bài học:
- Nhận diện giới hạn bộ nhớ khi dùng Set để đếm số lượng người dùng hoạt động (Daily Active Users - DAU) ở quy mô hàng triệu.
- Ứng dụng Bitmaps để đếm DAU chính xác tuyệt đối và phân tích tỷ lệ giữ chân (Retention) với User ID là số nguyên.
- Ứng dụng HyperLogLog để đếm DAU ước lượng với chi phí RAM cố định (tối đa 12KB) cho bất kỳ kiểu User ID nào (UUID, String).
1. Đặt Vấn Đề: Tại Sao Không Dùng Set?
Ở Bài 5, chúng ta biết Set chỉ lưu các phần tử duy nhất. Nếu muốn đếm DAU của ngày hôm nay, bạn có thể tạo một Set dau:2026-09-30 và SADD User ID vào đó. Cuối ngày, dùng SCARD để đếm số phần tử.
- Vấn đề: Mỗi chuỗi ID (ví dụ:
"1000001") tốn khoảng 8 bytes. Nếu bạn có 10 triệu DAU, một Set sẽ ngốn khoảng ~80MB RAM. Kéo theo đó là MAU (Monthly Active Users) tốn vài GB. Với RAM đắt đỏ của Redis, đây là một sự lãng phí.
2. Bitmaps: Đếm Chính Xác Tuyệt Đối (Dành Cho Integer ID)
Bitmap thực chất không phải là một kiểu dữ liệu riêng biệt trong Redis mà là các thao tác mức bit (bit-level) trên kiểu String.
- Hãy tưởng tượng một chuỗi bit:
00000000. Mỗi vị trí (offset) đại diện cho một User ID. - Nếu User ID 5 đăng nhập, ta bật bit ở vị trí thứ 5 lên thành
1. - Kích thước siêu nhỏ: 1 Byte = 8 bits = lưu được trạng thái của 8 users. 10 triệu users chỉ tốn ~1.2MB RAM.
Code mẫu thao tác với Bitmaps:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
var ctx = context.Background()
func workWithBitmaps(rdb *redis.Client) {
todayKey := "dau:2026-09-30"
// 1. Ghi nhận User đăng nhập (SETBIT)
// User ID bắt buộc phải là số nguyên (integer). Ví dụ: ID 105, 106
rdb.SetBit(ctx, todayKey, 105, 1)
rdb.SetBit(ctx, todayKey, 106, 1)
// 2. Kiểm tra 1 User có active hôm nay không (GETBIT) - O(1)
isActive, _ := rdb.GetBit(ctx, todayKey, 105).Result()
fmt.Printf("User 105 có active không? %v\n", isActive == 1) // true
// 3. Đếm tổng số lượng User Active hôm nay (BITCOUNT)
// Thuật toán đếm số bit 1 siêu tốc của Redis
totalDAU, _ := rdb.BitCount(ctx, todayKey, nil).Result()
fmt.Printf("Tổng số DAU hôm nay: %d\n", totalDAU) // 2
}
Lưu ý với Bitmap: Bitmap chỉ thực sự tối ưu nếu các User ID của bạn là các con số liên tiếp nhau. Nếu bạn có User ID là 1 và một User ID khác là 1,000,000,000, Redis vẫn phải cấp phát đủ bộ nhớ (khoảng 120MB) để vươn tới offset 1 tỷ, dù các bit ở giữa đều là 0.
3. HyperLogLog: Đếm Ước Lượng Siêu Việt (Dành Cho Mọi Loại ID)
Nếu hệ thống của bạn dùng UUID, chuỗi định danh, hoặc email làm User ID, bạn không thể dùng Bitmap. Giải pháp lúc này là HyperLogLog (HLL).
- HLL là một cấu trúc dữ liệu xác suất. Nó không lưu trữ chính xác giá trị bạn đưa vào, mà dùng thuật toán băm (hashing) để ước lượng số lượng phần tử duy nhất (Cardinality).
- Ưu điểm tuyệt đối: Dù bạn có đếm 1 ngàn, 1 triệu hay 1 tỷ users, một key HLL luôn chiếm tối đa 12KB RAM.
- Đánh đổi: Có sai số thống kê khoảng 0.81%. Với 1 triệu users, sai số khoảng vài ngàn người. Trong bài toán đếm View/DAU tổng quan, sai số này hoàn toàn chấp nhận được.
Code mẫu thao tác với HyperLogLog:
func workWithHyperLogLog(rdb *redis.Client) {
hllKey := "dau_hll:2026-09-30"
// 1. Thêm phần tử vào HLL (PFADD)
// Chấp nhận mọi kiểu chuỗi (String, UUID, Email...)
rdb.PFAdd(ctx, hllKey, "user-uuid-abc", "user-uuid-xyz")
rdb.PFAdd(ctx, hllKey, "user-email-test@gmail.com")
// Thêm trùng lặp không làm tăng kết quả đếm
rdb.PFAdd(ctx, hllKey, "user-uuid-abc")
// 2. Lấy số lượng ước lượng (PFCOUNT)
count, _ := rdb.PFCount(ctx, hllKey).Result()
fmt.Printf("Tổng DAU (ước lượng) hôm nay: %d\n", count) // 3
}
4. Tổng Kết: Khi Nào Dùng Công Cụ Nào?
| Yêu cầu | Set | Bitmap | HyperLogLog |
|---|---|---|---|
| Bản chất ID | Bắt kỳ | Bắt buộc là số nguyên (Int) | Bắt kỳ |
| Độ chính xác | 100% | 100% | Ước lượng (Sai số ~0.81%) |
| Kích thước bộ nhớ | Rất lớn (Tuyến tính theo N) | Rất nhỏ (~1.2MB cho 10M user) | Cố định tối đa 12KB |
| Lấy lại danh sách ID đã lưu | Được (SMEMBERS) |
Không (chỉ biết offset) | Không (Dữ liệu bị băm) |
All rights reserved