Mục lục
- Mục Tiêu Bài Học
- Bài Toán & Hai Loại Metric
- Total View Counter — INCR & Time Bucket
- Unique Visitor Với Set & Set-Per-Day
- Unique Visitor Với HyperLogLog
- Unique Visitor Với Bitmap
- So Sánh Bốn Pattern Unique Visitor
- Dedup Window — Cùng Visitor Reload Liên Tục
- Anonymous Visitor — Session ID & IP Hash
- Bot Detection
- Trending Realtime Với Time Bucket
- Flush View Count Sang DB
- Click-Through Funnel & Multi-Dimension
- Memory Budget
- Anti-patterns & Best Practices
- Tổng Kết & Quiz
Mục Tiêu Bài Học
- Phân biệt total view và unique visitor, hiểu khi nào cần cái nào.
- Nắm bốn pattern unique visitor (Set, Set-per-day, HLL, Bitmap) với trade-off memory và độ chính xác.
- Triển khai dedup window 30 phút để không đếm reload của cùng visitor.
- Xử lý anonymous visitor bằng session ID, bot detection bằng UA và rate.
- Xây trending realtime với time bucket và flush view count sang DB định kỳ.
Bài Toán & Hai Loại Metric
Một bài blog cần hiển thị số lượt xem dưới tiêu đề — ví dụ "1.2k views". Đằng sau con số đó ít nhất hai metric khác nhau cần được phân biệt:
- Total view (pageview): mỗi lần trang được load, counter tăng 1. User A đọc bài 3 lần → total view tăng 3.
- Unique visitor (UV): 1 user dù đọc bao nhiêu lần trong kỳ, chỉ tính 1. User A đọc 3 lần vẫn là 1 UV.
Ngoài ra còn các yêu cầu phái sinh:
- Realtime: UI muốn thấy view tăng gần tức thì — không thể đợi batch job hàng giờ.
- Persist long-term: số view cần lưu vào DB cho analytics, không để mất khi Redis restart.
- Chống abuse: bot crawler, refresh spam, cùng user reload liên tục đều có thể thổi phồng số.
Redis phù hợp làm lớp realtime counter vì INCR atomic, sub-millisecond latency, và có sẵn các structure tiết kiệm memory cho unique visitor (HyperLogLog, Bitmap). DB sẽ là nơi lưu trữ dài hạn sau khi flush.
Total View Counter — INCR & Time Bucket
Pattern đơn giản nhất: INCR mỗi lần page load. Kết hợp time bucket theo giờ để vẽ trend chart.
import time
def track_total_view(redis_client, post_id: str):
# Counter tổng — không TTL, persist vĩnh viễn
redis_client.incr(f"views:total:{post_id}")
# Counter theo giờ — dùng để vẽ trend, retention 1 ngày
hour_bucket = int(time.time() // 3600)
hour_key = f"views:hour:{hour_bucket}:{post_id}"
redis_client.incr(hour_key)
redis_client.expire(hour_key, 86400) # 1 ngày
def get_total_view(redis_client, post_id: str) -> int:
val = redis_client.get(f"views:total:{post_id}")
return int(val) if val else 0
def get_hourly_trend(redis_client, post_id: str, hours: int = 24) -> list[int]:
"""Trả về danh sách view count cho từng giờ trong N giờ gần nhất."""
now_bucket = int(time.time() // 3600)
pipe = redis_client.pipeline()
for i in range(hours - 1, -1, -1):
pipe.get(f"views:hour:{now_bucket - i}:{post_id}")
results = pipe.execute()
return [int(v) if v else 0 for v in results]
INCR atomic nên không cần lock dù có nhiều web server ghi đồng thời. Key views:total:{post_id} không có TTL — giá trị tích lũy dài hạn. Key hourly có TTL 1 ngày để tự cleanup sau khi không còn cần trend cũ.
Lưu ý: track_total_view này đếm mỗi page load, kể cả bot và reload spam. Dedup và bot filter sẽ được bổ sung ở các bước sau.
Unique Visitor Với Set & Set-Per-Day
Pattern 2a — Set tích lũy: mỗi post có một Set chứa tất cả user đã xem. SADD trả về 1 nếu phần tử mới, 0 nếu đã tồn tại.
def track_unique_set(redis_client, post_id: str, visitor_id: str) -> bool:
"""Trả về True nếu đây là lần đầu visitor này xem post."""
added = redis_client.sadd(f"views:unique:{post_id}", visitor_id)
return bool(added)
Set tự dedup — không cần logic kiểm tra thêm. Tuy nhiên memory tăng vô hạn theo số visitor. Mỗi user_id string ~50 byte; 10k unique visitor per post = 500KB; 1 triệu post × 10k UV = 500GB — không khả thi.
Pattern 2b — Set per day window: Reset daily, không tích lũy vĩnh viễn. Dùng để đếm unique visitor trong ngày, kết hợp tăng tổng counter khi có visitor mới.
from datetime import date
def track_unique_daily(redis_client, post_id: str, visitor_id: str) -> bool:
today = date.today().isoformat() # "2026-06-01"
day_key = f"views:unique:day:{today}:{post_id}"
added = redis_client.sadd(day_key, visitor_id)
if added:
# Tăng tổng unique count khi có visitor mới trong ngày
redis_client.incr(f"views:unique:count:{post_id}")
# TTL 2 ngày — cleanup tự nhiên, giữ hôm qua để so sánh
redis_client.expire(day_key, 2 * 86400)
return bool(added)
Set per day giải quyết vấn đề memory grow vô hạn: mỗi ngày Set cũ tự xóa sau 2 ngày. Nhược điểm: chỉ biết unique visitor hôm nay, không biết UV tổng từ khi bài được đăng. Nếu cần UV all-time, dùng HLL.
Unique Visitor Với HyperLogLog
HyperLogLog (HLL) dùng ~12KB cố định bất kể có 1 nghìn hay 1 tỷ phần tử, với standard error 0.81%. Bài 26 đã giới thiệu lệnh PFADD/PFCOUNT; bài này áp dụng vào page view tracking cụ thể.
def track_unique_hll(redis_client, post_id: str, visitor_id: str):
"""Thêm visitor vào HLL của post — không biết có phải mới hay không."""
redis_client.pfadd(f"views:hll:{post_id}", visitor_id)
def get_unique_count_hll(redis_client, post_id: str) -> int:
"""Trả về ước lượng unique visitor, error ~0.81%."""
return redis_client.pfcount(f"views:hll:{post_id}")
def get_unique_count_multi(redis_client, post_ids: list[str]) -> int:
"""Unique visitor trên tập nhiều post (dùng PFMERGE hoặc PFCOUNT multi-key)."""
keys = [f"views:hll:{pid}" for pid in post_ids]
return redis_client.pfcount(*keys) # redis-py hỗ trợ multi-key PFCOUNT
Khác biệt quan trọng so với Set và Bitmap: PFADD không trả về "mới hay cũ" một cách đáng tin cậy cho từng element (chỉ trả về 1 nếu internal state thay đổi, 0 nếu không). Vì vậy không thể dùng PFADD để vừa dedup vừa tăng total counter trong một bước — cần tách riêng.
Khi nào dùng HLL: post có hơn 10k unique visitor, cần lưu UV all-time mà không bị memory explode. Với post nhỏ (dưới vài nghìn UV), Set per day cũng ổn và chính xác 100%.
PFMERGE — hợp nhất nhiều HLL:
def merge_weekly_unique(redis_client, post_id: str, days: list[str]) -> int:
"""Tính unique visitor trong tuần bằng cách merge HLL 7 ngày."""
src_keys = [f"views:hll:day:{d}:{post_id}" for d in days]
dest_key = f"views:hll:week_temp:{post_id}"
redis_client.pfmerge(dest_key, *src_keys)
redis_client.expire(dest_key, 3600) # cache kết quả 1 giờ
return redis_client.pfcount(dest_key)
Unique Visitor Với Bitmap
Bitmap dùng 1 bit per user_id integer. SETBIT trả về bit cũ (0 = lần đầu xem, 1 = đã xem). BITCOUNT đếm tổng số bit đang là 1.
def track_unique_bitmap(redis_client, post_id: str, user_id: int) -> bool:
"""Trả về True nếu đây là lần đầu user này xem post."""
old_bit = redis_client.setbit(f"views:bitmap:{post_id}", user_id, 1)
return old_bit == 0 # 0 = bit trước đó là 0, tức là lần đầu
def get_unique_count_bitmap(redis_client, post_id: str) -> int:
return redis_client.bitcount(f"views:bitmap:{post_id}")
Memory: Bitmap có kích thước = (max_user_id + 7) // 8 byte. Với 100 triệu user (user_id 0..99.999.999), mỗi post cần 100M / 8 = ~12.5MB. Nếu có 10.000 post đang active → 125GB chỉ cho Bitmap.
Điều kiện để Bitmap hiệu quả:
- User_id là integer và dense — tức là phân bố liên tục từ 1 đến N, không có khoảng trống lớn.
- Nếu user_id sparse (ví dụ UUID hash thành integer 64-bit), Redis phải cấp phát bitmap ~2 exabyte — không thực tế.
- Dùng cho use case cần biết chính xác "user này đã xem chưa" với O(1) lookup, thay vì chỉ đếm.
Bitmap thắng ở tính chính xác 100% và khả năng trả lời "user X đã xem post Y chưa?" trong O(1). HLL không làm được điều đó.
So Sánh Bốn Pattern Unique Visitor
| Pattern | Memory per post | Chính xác | Biết "đã xem chưa" | Phù hợp khi |
|---|---|---|---|---|
| Set tích lũy | ~50B × UV (vô hạn) | 100% | Có (SISMEMBER) | Post ít, UV nhỏ (< vài nghìn) |
| Set per day | ~50B × UV/ngày | 100% trong ngày | Có (trong ngày) | Cần UV hôm nay, reset daily OK |
| HyperLogLog | ~12KB cố định | ~99.2% (error 0.81%) | Không | Post lớn, UV all-time, memory giới hạn |
| Bitmap | (max_uid)/8 byte | 100% | Có (GETBIT O(1)) | user_id integer dense, cần exact + lookup |
Khuyến nghị thực tế: dùng HLL cho unique visitor all-time (tiết kiệm memory, đủ chính xác cho analytics) kết hợp INCR cho total view. Nếu cần "user này đã xem chưa" để dedup, dùng Set per day hoặc dedup key riêng (xem bước 8).
Dedup Window — Cùng Visitor Reload Liên Tục
Không có dedup: user F5 liên tục 50 lần trong 5 phút → total view tăng 50, cộng thêm việc HLL bị PFADD 50 lần (dù kết quả PFCOUNT không đổi vì HLL dedup internally, total view vẫn sai).
Pattern: ghi một key có TTL cho mỗi cặp (visitor, post). Nếu key chưa tồn tại (SET NX thành công), đây là view hợp lệ trong window. Nếu key đã tồn tại, bỏ qua.
def track_view_dedup(redis_client, post_id: str, visitor_id: str,
window_seconds: int = 1800): # 30 phút
"""
Trả về True nếu view này được tính (lần đầu trong window).
Trả về False nếu visitor đã xem bài này trong 30 phút qua.
"""
dedup_key = f"viewed:{visitor_id}:{post_id}"
# SET NX EX: chỉ ghi nếu key chưa tồn tại, TTL = window_seconds
result = redis_client.set(dedup_key, "1", nx=True, ex=window_seconds)
if result:
# View hợp lệ: tăng total và ghi HLL
redis_client.incr(f"views:total:{post_id}")
redis_client.pfadd(f"views:hll:{post_id}", visitor_id)
# Hourly trend
hour_bucket = int(time.time() // 3600)
hour_key = f"views:hour:{hour_bucket}:{post_id}"
redis_client.incr(hour_key)
redis_client.expire(hour_key, 86400)
return bool(result)
Dedup key có dạng viewed:{visitor_id}:{post_id} với TTL 30 phút. Sau 30 phút key tự xóa, visitor xem lại thì tính là view mới. Đây là cách phổ biến trên các publishing platform — "30 phút = 1 session view".
Memory cho dedup key: mỗi key ~30–60 byte. Với 10k visitor xem 100 post trong 30 phút → 1M key × 60B = 60MB. Chấp nhận được và tự cleanup sau 30 phút.
Anonymous Visitor — Session ID & IP Hash
User chưa đăng nhập vẫn cần tracking. Hai lựa chọn phổ biến: session ID từ cookie, hoặc hash của IP address.
import secrets
import hashlib
def get_visitor_id(request) -> str:
"""
Trả về visitor_id dạng chuỗi — luôn có giá trị dù user chưa login.
Logged-in user: prefix "u:" để phân biệt với anonymous.
"""
# Ưu tiên user đã login
if user_id := get_authenticated_user_id(request):
return f"u:{user_id}"
# Anonymous: dùng session ID từ cookie
if anon_id := request.cookies.get("anon_id"):
return f"s:{anon_id}"
# Chưa có cookie: tạo mới, set trong response
new_id = secrets.token_urlsafe(16)
# Caller phải set cookie: response.set_cookie("anon_id", new_id, max_age=...)
return f"s:{new_id}"
def get_visitor_id_ip_fallback(request) -> str:
"""
Fallback dùng IP hash khi không có session cookie (GDPR-friendly:
không lưu raw IP, chỉ lưu hash không thể reverse).
"""
if user_id := get_authenticated_user_id(request):
return f"u:{user_id}"
ip = request.headers.get("X-Forwarded-For", request.remote_addr)
ip_hash = hashlib.sha256(ip.encode()).hexdigest()[:16]
return f"ip:{ip_hash}"
Session ID cookie bền hơn IP hash vì nhiều user có thể share cùng IP (NAT, VPN). IP hash phù hợp làm fallback khi cookie bị block.
GDPR: không lưu raw IP hay raw user_id vào HLL/Set nếu không cần trace ngược. Hash một chiều (SHA-256 với salt) đủ cho dedup mà không lưu PII. Retention: drop Set/dedup key sau 30 ngày theo retention policy.
Bot Detection
Bot crawler có thể chiếm 30–60% traffic trên một số site. Đếm bot view làm méo số liệu.
import re
BOT_UA_PATTERN = re.compile(
r"bot|crawler|spider|slurp|bingbot|googlebot|yandex|baidu|duckduck",
re.IGNORECASE
)
def is_bot(user_agent: str) -> bool:
if not user_agent:
return True # request không có UA — nghi ngờ bot
return bool(BOT_UA_PATTERN.search(user_agent))
def get_request_rate(redis_client, visitor_id: str,
window_seconds: int = 10) -> int:
"""Đếm số request của visitor trong N giây qua."""
rate_key = f"rate:{visitor_id}"
count = redis_client.incr(rate_key)
if count == 1:
redis_client.expire(rate_key, window_seconds)
return count
def should_track(redis_client, request, visitor_id: str) -> bool:
"""Trả về False nếu request từ bot hoặc có rate quá cao."""
ua = request.headers.get("User-Agent", "")
if is_bot(ua):
return False
# Hơn 5 request trong 10 giây từ cùng visitor → nghi ngờ bot/script
if get_request_rate(redis_client, visitor_id, window_seconds=10) > 5:
return False
return True
UA pattern matching bắt được crawler tuân thủ robots.txt. Headless browser (Puppeteer, Playwright) thường có UA bình thường nhưng bị bắt bởi rate check. Với hệ thống lớn hơn, kết hợp thêm honeypot field hoặc JavaScript challenge.
Rate key TTL: TTL ngắn (10 giây) để key tự xóa sau window. Không dùng key này cho banning — chỉ dùng để bỏ qua view. Banning cần mechanism riêng với TTL dài hơn.
Trending Realtime Với Time Bucket
Trending = những bài nhận nhiều view trong khoảng thời gian gần đây (1 giờ, 24 giờ). Yêu cầu: cập nhật gần realtime, không cần exact — approximate đủ dùng.
Approach: Hash per 1-minute bucket. Mỗi phút là một Hash key. Key lưu mapping post_id → view count trong phút đó. Để lấy trending 1 giờ, aggregate 60 bucket gần nhất.
from collections import Counter
def update_trending(redis_client, post_id: str):
now = int(time.time())
bucket = now // 60 # bucket 1 phút
bucket_key = f"trending:bucket:{bucket}"
redis_client.hincrby(bucket_key, post_id, 1)
redis_client.expire(bucket_key, 3600) # giữ 1 giờ
def get_trending(redis_client, top_n: int = 10) -> list[tuple[str, int]]:
"""
Aggregate 60 bucket gần nhất → top N post có nhiều view nhất.
Dùng pipeline để giảm round-trip.
"""
now_bucket = int(time.time()) // 60
pipe = redis_client.pipeline()
for i in range(60):
pipe.hgetall(f"trending:bucket:{now_bucket - i}")
bucket_data = pipe.execute()
scores: Counter = Counter()
for bucket_hash in bucket_data:
for post_id, count in bucket_hash.items():
# redis-py trả về bytes nếu decode_responses=False
pid = post_id.decode() if isinstance(post_id, bytes) else post_id
scores[pid] += int(count)
return scores.most_common(top_n)
Đặc điểm:
- 60 HGETALL qua pipeline ≈ 1 round-trip. Mỗi bucket Hash nhỏ nên nhanh.
- TTL 1 giờ trên mỗi bucket → tự cleanup.
- Có thể cache kết quả
get_trendingtrong 30 giây bằng một key riêng để tránh 60 HGETALL cho mỗi request.
Anti-pattern cần tránh: gọi get_trending trực tiếp trong mỗi request HTTP — đây là read heavy path. Cache kết quả trending và refresh background mỗi 30–60 giây.
def get_trending_cached(redis_client, top_n: int = 10) -> list[tuple[str, int]]:
cache_key = f"trending:cache:{top_n}"
cached = redis_client.get(cache_key)
if cached:
import json
return json.loads(cached)
result = get_trending(redis_client, top_n)
import json
redis_client.set(cache_key, json.dumps(result), ex=30) # cache 30 giây
return result
Flush View Count Sang DB
Redis là in-memory. Counter trong Redis cần được ghi xuống DB định kỳ để không mất khi Redis restart và để analytics tool đọc được.
Anti-pattern: gọi UPDATE posts SET view_count = view_count + 1 mỗi lần load page → DB nhận spike 10k write/giây với tải cao.
Pattern đúng: accumulate trong Redis, flush batch mỗi giờ (hoặc mỗi 5 phút tùy SLA).
def flush_views_to_db(redis_client, db_connection):
"""
Quét tất cả key views:total:*, đọc giá trị, upsert vào DB.
Chạy trong background job (celery task, cron, etc.) mỗi giờ.
"""
cursor = 0
batch = {}
while True:
cursor, keys = redis_client.scan(
cursor, match="views:total:*", count=100
)
for key in keys:
post_id = key.decode().split(":")[-1] if isinstance(key, bytes) else key.split(":")[-1]
val = redis_client.get(key)
if val is not None:
batch[post_id] = int(val)
if cursor == 0:
break
if not batch:
return
# Bulk upsert — tránh N query riêng lẻ
with db_connection.cursor() as cur:
records = [(count, pid) for pid, count in batch.items()]
cur.executemany(
"UPDATE posts SET view_count = %s WHERE id = %s",
records
)
db_connection.commit()
print(f"Flushed {len(batch)} posts")
Lưu ý:
- SCAN thay vì KEYS để tránh block Redis trên production (KEYS chặn toàn bộ lệnh khác trong thời gian scan).
- Giá trị trong Redis là source of truth cho realtime display. DB là snapshot dùng cho analytics và bền vững sau restart.
- Không
DELkey sau flush — counter tiếp tục tích lũy. DB ghi bằng absolute value, không cộng thêm. - Nếu Redis restart mà chưa flush, view count trong DB là số cũ nhất có. Chấp nhận được với metric view count (không phải billing).
Click-Through Funnel & Multi-Dimension
View chỉ là bước đầu của funnel. Tracking đầy đủ cần biết: bao nhiêu view → click vào CTA → convert (mua hàng, sign up).
class FunnelStage:
VIEW = "view"
CLICK = "click"
CONVERT = "convert"
def track_funnel(redis_client, post_id: str, stage: str, visitor_id: str):
dedup_key = f"funnel:{stage}:{visitor_id}:{post_id}"
if redis_client.set(dedup_key, "1", nx=True, ex=3600):
redis_client.incr(f"funnel:{post_id}:{stage}")
def get_funnel_stats(redis_client, post_id: str) -> dict:
pipe = redis_client.pipeline()
for stage in [FunnelStage.VIEW, FunnelStage.CLICK, FunnelStage.CONVERT]:
pipe.get(f"funnel:{post_id}:{stage}")
views, clicks, converts = pipe.execute()
return {
"views": int(views or 0),
"clicks": int(clicks or 0),
"converts": int(converts or 0),
}
Multi-dimension counter: đếm view theo quốc gia hoặc device type.
def track_view_by_dimension(redis_client, post_id: str,
country: str, device: str):
pipe = redis_client.pipeline()
pipe.incr(f"views:country:{country}:{post_id}")
pipe.incr(f"views:device:{device}:{post_id}")
pipe.execute()
Cảnh báo key explosion: với 10 dimension, mỗi dimension 20 giá trị → 200 key per post, nhân với 1 triệu post = 200 triệu key. Giữ dimension counting chỉ cho những gì thực sự dùng trong dashboard. Đừng track mọi thứ "phòng khi cần sau này".
Memory Budget
Ước tính memory khi có 1 triệu post:
| Cấu phần | Size per post | Tổng 1M post |
|---|---|---|
views:total:{post_id} (string INCR) |
~50 byte | ~50MB |
views:hll:{post_id} (HLL unique) |
~12KB | ~12GB |
Dedup key viewed:{visitor}:{post} |
~60 byte × active visits | Tự cleanup sau 30 phút |
| Hourly trend bucket (60 bucket × active posts) | ~10 byte per entry | Tự cleanup sau 1 ngày |
| Bitmap per post (100M user) | ~12.5MB | ~12.5TB — không khả thi |
Kết luận thực tế:
- INCR total + HLL unique: ~12GB cho 1M post — chấp nhận được trên instance 16–32GB.
- Bitmap per post: chỉ khả thi khi số post nhỏ (< 1000) hoặc user_id space nhỏ.
- Set tích lũy vĩnh viễn: không dùng cho production với post nhiều.
- Nếu 12GB HLL vẫn quá tốn, chỉ giữ HLL cho post có traffic cao (top 5% posts chiếm 80% view).
Anti-patterns & Best Practices
Anti-patterns
- INCR mỗi load không dedup: user reload 100 lần trong 1 phút → total view tăng 100. Số hiển thị trên UI bị thổi phồng.
- Set lưu unique visitor vĩnh viễn: memory grow theo tổng số visitor all-time. 1M post × 10k UV = 500GB+ không thể giảm.
- Đếm bot view: Googlebot crawl liên tục → metric méo, trending bị sai.
- Lưu raw IP vào Redis: vi phạm GDPR nếu không có consent và legal basis. Dùng hash thay thế.
- DB UPDATE mỗi page load: với 10k view/giây, DB nhận 10k write/giây — thường không chịu được trên write path đồng bộ.
- Gọi get_trending trong mỗi request HTTP: 60 HGETALL per request với traffic cao → latency spike.
- SCAN không có TTL cleanup: key tích lũy vô hạn, SCAN ngày càng chậm hơn.
Best Practices
- INCR total + PFADD HLL unique là combo tốt nhất cho hầu hết use case.
- Dedup window 30 phút per (visitor, post) để chống reload spam.
- Bot filter: UA pattern + rate check, bỏ qua trước khi ghi bất kỳ counter nào.
- Anonymous visitor: session cookie có độ bền tốt hơn IP hash.
- Anonymize: hash user_id trước khi đưa vào HLL/Set nếu không cần trace ngược.
- Trending: time bucket Hash + cache kết quả aggregate 30 giây.
- Flush DB hourly bằng background job (Celery, cron), dùng SCAN thay KEYS.
- TTL aggressive: hourly bucket 1 ngày, dedup key 30 phút, Set per day 2 ngày.
Tổng Kết & Quiz
Page view tracking trong Redis kết hợp nhiều primitive: INCR cho total, PFADD cho unique all-time, SET NX EX cho dedup window, HINCRBY cho trending bucket, SCAN cho flush job. Mỗi lựa chọn có trade-off rõ ràng giữa memory, chính xác, và độ phức tạp triển khai.
Quiz
- Tại sao không dùng Set tích lũy vĩnh viễn cho unique visitor trên site có 1 triệu post?
- Dedup window dùng
SET NX EX. Nếu muốn tăng window từ 30 phút lên 2 giờ cho visitor đã được track, có thể dùng EXPIRE để extend TTL không? Tại sao? - HLL PFADD trả về 0 hay 1 không phụ thuộc vào "element đã tồn tại hay chưa" theo nghĩa Set. Tại sao không dùng return value của PFADD để quyết định có tăng total counter không?
- Bot detection dùng User-Agent regex bắt crawler biết cách khai báo. Headless browser với UA bình thường qua được UA check — tại sao rate check vẫn giúp phát hiện?
- Trong
flush_views_to_db, sau khi UPDATE DB thành công, có nên DEL key Redis hay không? Phân tích trade-off.
Đáp án gợi ý
- Set tích lũy lưu toàn bộ visitor_id all-time. Với 1M post × 10k UV all-time × 50 byte/user_id = 500GB — vượt xa memory ngân sách của một Redis instance thông thường. Không có cơ chế cleanup tự nhiên.
- EXPIRE trên dedup key đã tồn tại sẽ reset TTL về mức mới — nhưng điều này thay đổi semantic: visitor đến lần thứ hai "gia hạn" window, có thể không bao giờ được đếm view mới nếu liên tục quay lại trong cửa sổ. Thông thường không dùng EXPIRE để extend dedup key; chỉ để key tự expire rồi tính view mới khi hết window.
- HLL là probabilistic: internal state (hash register) có thể không thay đổi dù element thực sự mới vì hash collision. PFADD trả về 1 khi internal state thay đổi, 0 khi không — không phải "element mới hay cũ" theo nghĩa exact. Dùng return value này để tăng total counter sẽ miss một số view hợp lệ.
- Headless browser với UA bình thường có thể crawl nhiều page rất nhanh (5+ page/giây) để fake traffic. Rate check theo visitor_id trong window ngắn (10 giây) bắt được hành vi navigate bất thường này — human không thể browse 5 trang/giây.
- Không DEL: Redis tiếp tục là source of truth realtime, counter tích lũy đúng. DB ghi absolute value mỗi lần flush — không cần reset. DEL sau flush sẽ làm mất view xảy ra giữa lúc đọc giá trị và lúc DEL, đồng thời làm mất khả năng hiển thị realtime.
Bài tiếp theo
Bài 92 xây dựng leaderboard realtime với Sorted Set — score, rank, range query theo score, và các edge case khi điểm số bằng nhau.
