Redis
Vai trò Redis trong V-Nexus
Redis là state store tạm thời + rate-limit backbone cho backend V-Nexus. Đặt cùng Minipc Server với Postgres và App (single-node Lettuce, không cluster) — footprint nhỏ vì mọi key đều có TTL.
Nguyên tắc bất biến
- Mọi key đều có TTL — không có key vĩnh viễn. Redis không phải DB.
- Source of truth vẫn là Postgres — Redis chỉ là cache/counter/lock. Cache miss luôn rebuild được từ DB.
- Fail-open trên Redis outage — khi Redis chết, request vẫn đi (không fail business logic). Trade-off: rate-limit + cache hit-rate suy giảm tạm thời.
- Key prefix theo domain (
otp:*,fav:*,bucket:*, …) — dễ scan, dễ TTL chính sách. - Không lưu PII raw — OTP lưu BCrypt hash, phone đã được normalize E.164.
Kiến trúc
flowchart LR
subgraph App[BE App]
OTP[OtpServiceImpl]
RL[RateLimitGuard]
FAV[FavoriteCacheService]
FUT[Future use cases]
end
subgraph Redis[Redis - Minipc Server local]
K1[otp:*]
K2[bucket:*]
K3[fav:count:*]
K4[Future keys]
end
OTP -->|StringRedisTemplate| K1
RL -->|Bucket4j Lettuce| K2
FAV -->|StringRedisTemplate| K3
FUT -->|TBD| K4
Redis -. fail-open .-> AppThư viện sử dụng
| Cấp | Thư viện | Mục đích |
|---|---|---|
| Driver | Lettuce (qua Spring LettuceConnectionFactory) | Async, single client reuse cho cả app |
| API thường | StringRedisTemplate | OTP, favorites — operations đơn giản |
| Rate limit | Bucket4j + Bucket4jLettuce | Token bucket distributed (CAS-based) |
| Caching | @Cacheable (Spring Cache) | Chưa dùng — kế hoạch dùng cho permissions/entitlements |
3 use case đang dùng Redis trong production
1. OTP lifecycle (OtpServiceImpl)
Quản lý vòng đời OTP cho 3 luồng: đăng ký, đổi mật khẩu, resend. Đây là use case Redis-heavy nhất — 1 luồng OTP đụng ≥ 4 key.
| Key | Mục đích | TTL |
|---|---|---|
otp:code:{phone} | BCrypt hash của OTP đang chờ verify | 5 phút (config app.otp.ttl-seconds) |
otp:cooldown:{phone} | Chống spam resend (1 OTP / 60s) | 60 giây |
otp:daily:{phone} | Counter request/ngày (max 3) | 24 giờ |
otp:resend:{phone} | Counter resend/giờ (max 5) | 1 giờ |
otp:attempts:{phone} | Đếm số lần verify sai | 15 phút |
otp:lock:{phone} | Khoá 30 phút sau 5 lần sai | 30 phút (app.otp.lock-duration-seconds) |
otp:reg-token:{phone} | UUID registration token sau verify thành công | 10 phút |
otp:reset-token:{phone} | UUID reset-password token | 10 phút |
Note: OTP code lưu BCrypt hash — Redis bị compromise vẫn không lộ mã gốc.
2. HTTP Rate Limit (RateLimitConfig + Bucket4j)
Distributed token bucket cho mọi HTTP endpoint. Cấu hình chi tiết xem trang Rate Limit.
| Key | Mục đích | TTL |
|---|---|---|
bucket:{name}:{key} | Token bucket Bucket4j (CAS-based) | refillPeriod + 1 phút (Bucket4j auto-expire idle bucket) |
{name} ∈ auth-otp, auth-login, payment, ai, upload, admin, authenticated, public-read.
{key} = userId (nếu đã đăng nhập) hoặc IP (anonymous).
Fail-open behaviour: RateLimitConfig set Lettuce command timeout = app.ratelimit.redis-timeout (default 100ms). Redis chết → tryConsume throw nhanh → guard cho request đi tiếp.
3. Favorite count cache (FavoriteCacheService)
Counter realtime cho số lượt favorite mỗi listing. Membership (user X có favorite listing Y không) không dùng Redis — đọc thẳng từ junction table user_favorites (source of truth, tránh stale).
| Key | Mục đích | TTL |
|---|---|---|
fav:count:{listingId} | Counter INCR/DECR số lượt favorite của listing | 24 giờ |
Cache miss / Redis outage: rebuild từ userFavoriteRepository.countByIdListingId(). Đọc không bao giờ fail.
Guard: DECR chỉ chạy khi cached > 0 → không bao giờ counter âm dù event redelivery / drift.
Naming convention
| Quy tắc | Ví dụ |
|---|---|
{domain}:{type}:{id} (3 cấp) | otp:code:+84901234567 · fav:count:abc-123-uuid |
| Domain prefix bắt buộc | otp:, fav:, bucket:, … |
id là raw (không hash) trừ khi PII | phone đã normalize E.164, UUID raw |
| Counter dùng singular | otp:daily không phải otp:dailies |
Lock dùng từ lock | otp:lock:*, crawl:lock:* (tương lai) |
TTL strategy
| Loại | TTL điển hình | Lý do |
|---|---|---|
| Auth challenge (OTP code, registration token) | 5–10 phút | Đủ thời gian user thao tác, đủ ngắn để giảm window tấn công |
| Lockout (sau abuse) | 15–30 phút | Đủ răn đe nhưng không gây bực cho user nhầm |
| Counter daily/hourly | 24h / 1h | Window quota tự reset |
| Cache (favorite count, …) | 24h | Cân bằng giữa staleness và memory |
| Distributed bucket (Bucket4j) | refill + 1 phút | Idle bucket auto-cleanup |
| Distributed lock (tương lai) | 30s–2 phút | Đủ chạy job + auto-release nếu worker chết |
Snapshot toàn bộ keyspace hiện tại
| Prefix | Số key pattern | TTL range | Owner module |
|---|---|---|---|
otp:* | 8 | 60s – 24h | modules/auth |
bucket:* | 1 (động theo bucket name) | refill + 1m | common/ratelimit |
fav:* | 1 | 24h | modules/favorites |
Use cases dự kiến — phân loại theo độ ưu tiên
Mapping vào feature trong Danh sách Feature và module BE chưa dùng Redis.
Ưu tiên cao (sẵn sàng triển khai, có feature owner)
| Use case | Liên quan feature / module | Mô tả ngắn | Loại Redis op |
|---|---|---|---|
| Refresh token denylist | auth-logout | Cache token đã revoke để bypass DB query mỗi lần refresh — TTL = thời gian còn lại của token | SET token:revoked:{jti} + EXISTS check |
| Payment idempotency | payment-order-detail-sync, wallet-topup-* | Lưu idempotencyKey → orderId để retry an toàn webhook + tránh double-charge | SETNX với TTL 24h |
| Crawl coordination lock | listing-crawl-external-sources | Distributed lock per-source (Muaban / Nhà Tốt / Batdongsan) tránh nhiều worker crawl trùng | SET lock:crawl:{source} NX EX 120 |
| Daily distribution lock | broker-daily-listing-distribution | Lock khi job 5 phút chạy — đảm bảo chỉ 1 instance phân bổ | SET lock:distribution:{date} NX EX 60 |
Ưu tiên trung bình (đợi traffic / requirement chín)
| Use case | Liên quan feature / module | Mô tả ngắn | Loại Redis op |
|---|---|---|---|
| Permission/entitlement cache | modules/iam + modules/subscriptions | Cache user roles + subscription tier để authorize không hit DB mỗi request | HSET user:perm:{userId} TTL 15 phút, invalidate khi update |
| Unlock state cache | masking-unlock | User X đã unlock listing Y? — fast check thay vì JOIN | SET unlock:{userId}:{listingId} TTL = thời hạn unlock |
| Hot listing cache | modules/listings, modules/marketplace | Top/featured/popular listings (top 100) — read-through cache, invalidate on state change | GET listing:hot:{slot} |
| Crawl dedup hash | listing-crawl-external-sources | Hash nội dung (sha256 title+price+address) để skip listing đã crawl trong N giờ | SET crawl:hash:{sha} TTL 24h |
| Phone contact dedup | contacts | Cùng phone + listing trong window 1h chỉ ghi 1 contact event | SETNX contact:{phoneHash}:{listingId} EX 3600 |
Ưu tiên thấp / spike
| Use case | Liên quan feature / module | Mô tả ngắn | Loại Redis op |
|---|---|---|---|
| WebSocket presence | /system/websocket/ | Track online connections để fan-out notification chỉ tới user đang connect | SADD ws:online:{userId} |
| Notification fan-out via Streams | modules/notifications | Redis Streams thay queue khi cần in-order delivery cho realtime push | XADD notif:stream |
| Trust score cache | modules/users (broker), listing | Tính một lần / 5 phút, cache để dashboard / search không recompute | SET trust:user:{userId} TTL 5m |
| Geo / heatmap aggregation | modules/marketplace | Cache aggregation theo quận/huyện cho map view | GET geo:agg:{districtCode} TTL 1h |
| Search query cache | modules/marketplace | Top 100 query phổ biến, cache kết quả 30s | GET search:{queryHash} TTL 30s |
| AVM result cache | modules/ai (AVM) | AVM tính khá nặng (findComparables query DB) — cache result theo địa chỉ | SET avm:{addressHash} TTL 1h |
Cấu hình Spring
# Connectionspring.data.redis.host=${REDIS_HOST:localhost}spring.data.redis.port=${REDIS_PORT:6379}spring.data.redis.username=${REDIS_USERNAME:}spring.data.redis.password=${REDIS_PASSWORD:}
# Rate limit timeout — Redis chết → fail-open nhanhapp.ratelimit.redis-timeout=${RATELIMIT_REDIS_TIMEOUT:100ms}Quy tắc runtime
| Hành vi | Mô tả |
|---|---|
| Client | Lettuce single instance, shared cho cả app + Bucket4j (không tạo client riêng) |
| Codec | UTF-8 String cho key, byte[] cho value của Bucket4j |
| Lazy init | ProxyManager của Bucket4j dùng @Lazy → app vẫn boot khi Redis chưa sẵn sàng |
| Outage = fail-open | Rate-limit + favorite count đều cho request đi tiếp khi Redis chết |
| Test profile | app.ratelimit.enabled=false ở application-test.properties → unit test không cần Redis |
| Shutdown | @PreDestroy đóng dedicated Bucket4j connection |
Mức độ chi phí
Xem trang Chi phí tab “Đánh giá dịch vụ”:
| Khoản | Mức |
|---|---|
| Redis instance | Chạy chung trên Minipc Server tự vận hành (single-node, share Minipc Server với Postgres + App) |
| Memory footprint | THẤP nhờ TTL nhỏ (OTP/lock < 30 phút, counter ≤ 24h) |
| Network egress | 0 (local socket trong Minipc Server) |
Cảnh báo cần setup
| Alert | Threshold | Lý do |
|---|---|---|
| Redis down / connect timeout > 100ms | > 1 phút liên tục | Rate-limit không còn hiệu lực, abuse có thể tăng |
INFO memory used_memory | > 200 MB | Có key không TTL hoặc TTL quá dài đang ăn memory |
INFO clients connected_clients | > 50 | App có connection leak |
INFO stats keyspace_misses / keyspace_hits ratio | miss/hit > 50% | Cache strategy chưa hiệu quả, cần review TTL hoặc warm-up |
slowlog len | > 10 trong 5 phút | Lua script / pipeline đang chậm — review query |
Vận hành / migration
- Tăng tier: Redis vẫn ở Minipc Server local cho đến khi traffic đủ lớn để cần tách Redis riêng. Plan tách:
- Setup Redis cluster trên máy chủ riêng (single Redis, không cluster trừ khi > 100k user)
- Migrate connection string qua
REDIS_HOSTenv var (1 lần restart, không downtime nếu connection pool reconnect tốt) - Cold start có thể mất hit rate vài giờ đầu — không impact correctness
- Backup: Redis không backup. Mọi key có TTL ngắn — nếu Redis mất hết key (restart, di chuyển Minipc Server) thì:
- OTP / token: user phải request lại
- Rate-limit bucket: reset về full → tạm thời cho request đi tự do (acceptable)
- Favorite count: rebuild on-demand từ DB ✓
- Theo dõi keyspace: chạy
redis-cli --scan --pattern '*:*' | head -100định kỳ để spot key lạ (không theo prefix convention).