Bloom Filter: Kiểm tra Email/Username trong 100 triệu record với RAM tối thiểu
Hướng dẫn Bloom Filter cho 100 triệu email/username: cách tính bit array, hash functions, false positives, Node.js implementation, Redis BF.RESERVE và kiến trúc production an toàn.
ShanverseUpdated 12 min read— viewsLoading views
Data Structures
Algorithms
Bài toán 100 triệu record
Giả sử bảng users có 100 triệu email và username. Endpoint kiểm tra tính khả dụng có thể tạo lượng query rất lớn:
B-tree index giúp lookup nhanh, nhưng mỗi request vẫn tiêu thụ connection, CPU, buffer-cache và network. Bot còn có thể biến endpoint thành công cụ dò tài khoản hoặc gây tải database.
Bloom Filter đặt một lớp kiểm tra cực nhỏ trước database:
Mermaid
Loading diagram…
Bloom Filter không lưu email. Nó chỉ lưu dấu vết bit của các hash.
Quy tắc quan trọng nhất
Bloom Filter chuẩn có:
Comments
Không false negative về mặt thuật toán: nếu phần tử đã được thêm đúng cách, filter không trả “không có”.
Có false positive: filter có thể trả “có thể có” cho phần tử chưa từng thêm.
Không trả về record hoặc vị trí record.
Không đếm số phần tử chính xác.
Không hỗ trợ delete an toàn nếu chỉ dùng bit array chuẩn.
Trong production, false negative vẫn có thể xuất hiện do vận hành: event update bị mất, filter chưa backfill đủ, dùng sai version, normalize không nhất quán hoặc dữ liệu bị restore/corrupt. Vì vậy cần quy trình đồng bộ và rebuild chứ không chỉ dựa vào tính chất toán học.
Cấu trúc bên trong
Bloom Filter gồm:
Bit array kích thước m.
k hash functions.
n phần tử dự kiến.
Khi thêm alice@example.com:
Tính k vị trí bit.
Đặt tất cả bit đó thành 1.
Khi kiểm tra:
Chỉ cần một bit bằng 0 → chắc chắn chưa có.
Tất cả bit bằng 1 → có thể đã có.
Nhiều phần tử có thể dùng chung bit, tạo ra false positive.
Công thức sizing
Với false-positive probability mục tiêu p:
Trong đó:
n: capacity dự kiến.
m: số bit.
k: số hash tối ưu, làm tròn về số nguyên phù hợp.
Xấp xỉ false-positive rate khi đã chèn n phần tử:
100 triệu phần tử cần bao nhiêu RAM?
Đây là kích thước lý thuyết của bit array, chưa gồm metadata, allocator, replication, snapshot và overhead của runtime/Redis.
So với việc giữ 100 triệu chuỗi email đầy đủ trong hash set, vài trăm MiB là rất nhỏ. Tuy nhiên, Bloom Filter đổi độ chính xác tuyệt đối lấy tiết kiệm bộ nhớ.
Chọn false-positive rate theo chi phí thật
Không nên mặc định chọn 0,1% chỉ vì nghe nhỏ.
Ví dụ có 10 triệu lookup âm tính mỗi ngày:
FPR 1% có thể tạo khoảng 100.000 database confirmation không cần thiết.
FPR 0,1% tạo khoảng 10.000.
FPR 0,01% tạo khoảng 1.000.
Đây là kỳ vọng thống kê, không phải SLA tuyệt đối. Chọn p dựa trên:
RAM budget.
Số lookup mỗi giây.
Chi phí database fallback.
Latency SLO.
Tỷ lệ key thực sự tồn tại.
Khả năng filter vượt capacity.
Normalize trước khi hash
Nếu database lưu Alice@Example.com nhưng filter nhận alice@example.com, hai chuỗi có thể hash khác nhau.
Một policy minh họa:
Quy tắc email phụ thuộc chính sách sản phẩm. Không tự ý xóa dấu chấm, bỏ +tag hoặc lowercase local-part cho mọi provider. Quan trọng nhất là database unique index, Bloom producer và Bloom consumer dùng cùng một canonicalization version.
Nên thêm namespace/version vào key:
TypeScript implementation từ đầu
Ví dụ dưới đây dùng SHA-256 để tạo hai hash 64-bit rồi áp dụng double hashing:
Đây là code học thuật/minh họa. Production nên dùng thư viện đã benchmark, test vectors ổn định, serialization có version và hash nhanh phù hợp với threat model.
Complexity
Add: O(k).
Lookup: O(k).
Memory: O(m), không phụ thuộc độ dài email/username.
Delete: không an toàn với Bloom Filter chuẩn.
Lookup thường chạm một số byte cố định và không cần random I/O tới database.
Luồng kiểm tra username đúng trong production
Nhưng lúc tạo tài khoản, không dùng kết quả kiểm tra trước đó làm bằng chứng:
Hai request đồng thời đều có thể thấy “available”. Chỉ unique constraint/transaction mới giải quyết race condition chính xác.
Đồng bộ Bloom Filter bằng CDC hoặc Outbox
Nếu database commit thành công nhưng BF.ADD thất bại, filter bị thiếu phần tử. Thuật toán không tạo false negative, nhưng hệ thống đã vi phạm giả định “mọi record đều được add”.
Mẫu an toàn:
Database là source of truth.
Transaction ghi user và outbox event cùng lúc, hoặc dùng CDC từ WAL/binlog.
Consumer normalize theo cùng version.
Consumer add email/username vào filter idempotently.
Theo dõi consumer lag và dead-letter queue.
Unique constraint luôn tồn tại.
Nếu event lag, availability check có thể trả “likely available”, nhưng final insert vẫn an toàn.
Redis Bloom Filter
Redis hỗ trợ các lệnh Bloom Filter như BF.RESERVE, BF.ADD, BF.MADD, BF.EXISTS và BF.MEXISTS.
Tạo filter cho 100 triệu email, FPR 0,1%:
Thêm và kiểm tra:
0 từ BF.EXISTS nghĩa là chưa được thêm vào filter.
1 nghĩa là có xác suất đã được thêm; hãy confirm database nếu cần exactness.
Theo dõi cấu hình và capacity:
Reserve trước tốt hơn để filter auto-create bằng default capacity quá nhỏ.
Một filter 100 triệu phần tử có scale trên Redis Cluster không?
Một Bloom Filter Redis là một key, nên nằm trên một hash slot và một primary. Redis Cluster không tự chia nội dung của key lớn qua nhiều shard.
Nếu một node không đủ RAM/QPS, partition filter:
Chọn partition bằng vài bit ổn định của hash. Mỗi lookup chỉ hỏi đúng một partition, không phải cả 256 filter.
Mỗi partition phải được sizing theo phân phối thực tế và có headroom. Theo dõi skew vì tenant/domain lớn có thể làm một partition tăng nhanh hơn.
Điều gì xảy ra khi vượt capacity?
Bloom Filter không ngừng hoạt động, nhưng nhiều bit chuyển thành 1 và false-positive rate tăng nhanh hơn mục tiêu.
Giải pháp:
Dự báo growth và reserve có headroom.
Theo dõi inserted items/capacity và FPR thực đo.
Dùng scalable Bloom Filter nếu thư viện hỗ trợ.
Tạo filter version mới lớn hơn rồi rebuild.
Không coi p cấu hình là vĩnh viễn nếu n vượt thiết kế.
Rebuild không downtime
Mermaid
Loading diagram…
Quy trình:
Tạo identity:email:v4 với capacity/FPR mới.
Ghi nhận WAL/binlog position.
Scan database theo primary-key ranges và BF.MADD theo batch.
Replay event phát sinh sau snapshot boundary.
So sánh sample record database đều trả positive ở filter mới.
Chuyển config/version atomic.
Giữ filter cũ trong rollback window.
Không switch khi backfill chưa đầy đủ, vì đó tạo operational false negatives.
Delete và rename
Không thể clear một bit khi xóa phần tử, vì bit đó có thể được nhiều phần tử dùng chung.
Các lựa chọn:
Không delete; chấp nhận stale positive và confirm database.
Rebuild định kỳ.
Counting Bloom Filter dùng counter thay bit.
Cuckoo Filter hỗ trợ delete tốt hơn trong nhiều workload.
Với username được giải phóng sau khi xóa account, stale positive chỉ tạo thêm database lookup nếu positive luôn được xác nhận. Nó không được phép chặn username vĩnh viễn.
Khi rename:
Database unique transaction là nguồn sự thật.
Add identity mới sau commit.
Identity cũ vẫn có thể positive đến lần rebuild.
Database confirmation trả chính xác rằng identity cũ đã trống.
Cache penetration
Bloom Filter có thể chặn request tới key chắc chắn không tồn tại trước khi query database/cache backend. Tuy nhiên:
Filter phải chứa toàn bộ key hợp lệ.
Bot có thể chọn key tạo false positive hoặc tấn công endpoint trực tiếp.
Vẫn cần rate limiting, authentication, WAF và negative caching.
Không tiết lộ account existence nhạy cảm qua response/timing.
Với login/password reset, phản hồi nên chung chung để giảm user enumeration.
Bloom Filter không phù hợp khi nào?
Không chấp nhận false positive và không có bước confirm.
Cần lấy value, range query hoặc prefix search.
Cần delete liên tục nhưng không thể rebuild.
Dataset nhỏ đủ để dùng exact set.
Filter không thể đồng bộ đầy đủ với source of truth.
Mọi query gần như đều positive, nên ít tiết kiệm database lookup.
Shard key hoặc normalization thường xuyên thay đổi.
So sánh các lựa chọn
Testing
Unit/property tests
Mọi item đã add đều trả positive.
Item chưa add có FPR gần mục tiêu trên sample đủ lớn.
Serialize/restore giữ nguyên kết quả.
Cùng input tạo cùng index trên mọi runtime.
Unicode/case/whitespace normalize giống database.
Boundary capacity và over-capacity được đo.
Integration tests
Database commit nhưng consumer Bloom bị restart.
Outbox/CDC replay không làm sai filter.
Rebuild v3 → v4 trong lúc có write.
Redis timeout phải fallback database, không đoán.
Filter key mất/expire phải phát hiện và fail safe.
Hai signup đồng thời vẫn bị unique constraint bảo vệ.
Rename/delete không khóa nhầm identity chỉ vì stale positive.
Metrics production
Theo dõi:
Bloom negative và positive ratio.
Database confirmation rate.
False positives thực đo: Bloom positive nhưng DB negative.
FPR theo filter version.
Capacity, size, expansion và fill/saturation signal.
CDC/outbox lag, retry và dead letters.
Rebuild progress.
Redis latency/error/memory/replication.
Unique-constraint conflicts.
Endpoint abuse/rate-limit rejections.
Không đặt raw email/username trong metric label hoặc log không bảo mật.
Anti-patterns
Bloom positive rồi kết luận email chắc chắn tồn tại.
Từ chối username chỉ dựa trên Bloom Filter.
Bỏ unique constraint vì đã có Bloom.
Add vào Bloom trước database commit rồi transaction rollback.
Update filter kiểu best effort nhưng vẫn tin negative tuyệt đối.
Không version normalization/hash function.
Reserve capacity thấp hơn growth thật.
Delete bằng cách clear bit.
Để Bloom key expire ngoài ý muốn.
Dùng một key khổng lồ rồi kỳ vọng Redis Cluster tự chia nội dung.
Trả lỗi khi Redis down thay vì fallback theo policy.
Dùng endpoint availability để lộ account existence.
Production checklist
Database unique index là source of truth.
Bloom negative chỉ tối ưu existence query, không thay thế final insert.
Bloom positive được confirm khi cần exactness.
Email/username normalization được version hóa.
Capacity và FPR được tính theo growth thực.
Có headroom và cảnh báo saturation.
Update đi qua outbox/CDC có retry.
Redis failure fallback về database.
Filter lớn được partition có chủ đích nếu cần.
Rebuild dùng snapshot boundary + event replay.
Version switch atomic và có rollback.
Delete/rename không dựa vào clear bit.
Endpoint có rate limit và chống enumeration.
FPR thực đo và database savings có dashboard.
Kết luận
Với 100 triệu email/username, Bloom Filter có thể dùng khoảng:
114 MiB cho FPR 1%.
171 MiB cho FPR 0,1%.
229 MiB cho FPR 0,01%.
Giá trị lớn nhất của Bloom Filter là loại bỏ nhanh phần lớn query chắc chắn không tồn tại. Nhưng kiến trúc đúng phải giữ database và unique constraint làm nguồn sự thật, xác nhận mọi Bloom positive quan trọng, đồng bộ qua CDC/outbox và rebuild bằng version mới khi capacity hoặc canonicalization thay đổi.
Hướng dẫn Fast & Slow Pointers và Floyd’s Cycle Detection bằng TypeScript: phát hiện cycle, tìm entry, cycle length, functional graph và các giới hạn thực tế.