first commit
This commit is contained in:
391
docs/Redis.md
Normal file
391
docs/Redis.md
Normal file
@@ -0,0 +1,391 @@
|
||||
# Hướng Dẫn Đặt Tên Redis Key Trong Dự Án
|
||||
|
||||
> **Tham khảo:** [Redis Namespace and Other Keys to Developing with Redis](https://redis.io/blog/5-key-takeaways-for-developing-with-redis/)
|
||||
|
||||
---
|
||||
|
||||
## Mục Lục
|
||||
|
||||
1. [Nguyên Tắc Chung](#1-nguyên-tắc-chung)
|
||||
2. [Quy Ước Đặt Tên Key (Naming Convention)](#2-quy-ước-đặt-tên-key-naming-convention)
|
||||
3. [Cấu Trúc Key Theo Namespace](#3-cấu-trúc-key-theo-namespace)
|
||||
4. [Bảng Mẫu Key Theo Chức Năng](#4-bảng-mẫu-key-theo-chức-năng)
|
||||
5. [Quy Tắc Về Độ Dài Key](#5-quy-tắc-về-độ-dài-key)
|
||||
6. [Chọn Đúng Data Structure](#6-chọn-đúng-data-structure)
|
||||
7. [Quản Lý Key: SCAN thay vì KEYS](#7-quản-lý-key-scan-thay-vì-keys)
|
||||
8. [Chiến Lược TTL & Expiration](#8-chiến-lược-ttl--expiration)
|
||||
9. [Cache Invalidation](#9-cache-invalidation)
|
||||
10. [Ví Dụ Thực Tế Trong Dự Án](#10-ví-dụ-thực-tế-trong-dự-án)
|
||||
|
||||
---
|
||||
|
||||
## 1. Nguyên Tắc Chung
|
||||
|
||||
Theo bài viết từ Redis, có **5 điểm quan trọng** khi phát triển với Redis:
|
||||
|
||||
| # | Nguyên tắc | Mô tả |
|
||||
|---|-----------|-------|
|
||||
| 1 | **Namespace cho key** | Sử dụng dấu `:` để phân tách các phần của tên key, giúp dễ quản lý và tìm kiếm |
|
||||
| 2 | **Giữ key ngắn gọn** | Key name cũng chiếm bộ nhớ — key dài 12 ký tự tốn thêm ~15% RAM so với key 6 ký tự (trên 1 triệu key) |
|
||||
| 3 | **Dùng đúng data structure** | Hash, List, Set, Sorted Set — mỗi loại phù hợp với một use case khác nhau |
|
||||
| 4 | **Dùng SCAN, không dùng KEYS** | Lệnh `KEYS` có thể block server, `SCAN` an toàn hơn cho production |
|
||||
| 5 | **Sử dụng Lua Scripts** | Xử lý logic phía server để giảm latency và tối ưu hiệu suất |
|
||||
|
||||
---
|
||||
|
||||
## 2. Quy Ước Đặt Tên Key (Naming Convention)
|
||||
|
||||
### Format chung
|
||||
|
||||
```
|
||||
{project}:{service}:{entity}:{identifier}
|
||||
```
|
||||
|
||||
- **Dấu phân cách:** Luôn dùng dấu hai chấm `:` (colon) — đây là **convention chuẩn** của Redis
|
||||
- **Chữ thường:** Tất cả các phần của key đều viết **lowercase**
|
||||
- **Không dấu cách, không ký tự đặc biệt:** Chỉ dùng chữ cái, số, dấu `:` và dấu `-` hoặc `_`
|
||||
|
||||
### Đúng
|
||||
|
||||
```
|
||||
myapp:user:profile:12345
|
||||
myapp:order:detail:ORD-001
|
||||
myapp:cache:product:list:page:1
|
||||
```
|
||||
|
||||
### Sai
|
||||
|
||||
```
|
||||
MyApp_User_Profile_12345 # Dùng underscore thay vì colon, viết hoa
|
||||
user profile 12345 # Có dấu cách
|
||||
MYAPP:USER:PROFILE:12345 # Viết hoa toàn bộ (lãng phí bộ nhớ)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Cấu Trúc Key Theo Namespace
|
||||
|
||||
Áp dụng cho dự án **Clean Architecture**, cấu trúc key nên phản ánh rõ layer và module:
|
||||
|
||||
```
|
||||
{app}:{layer}:{entity}:{action/scope}:{identifier}
|
||||
```
|
||||
|
||||
### Các prefix theo layer
|
||||
|
||||
| Prefix | Ý nghĩa | Ví dụ |
|
||||
|--------|---------|-------|
|
||||
| `app:cache` | Cache dữ liệu | `app:cache:product:list` |
|
||||
| `app:session` | Quản lý session | `app:session:user:abc123` |
|
||||
| `app:rate` | Rate limiting | `app:rate:api:login:192.168.1.1` |
|
||||
| `app:lock` | Distributed lock | `app:lock:order:process:ORD-001` |
|
||||
| `app:queue` | Message queue | `app:queue:email:pending` |
|
||||
| `app:temp` | Dữ liệu tạm thời | `app:temp:otp:user:12345` |
|
||||
| `app:pub` | Pub/Sub channels | `app:pub:notifications:user:12345` |
|
||||
| `app:counter` | Bộ đếm | `app:counter:visit:page:home` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Bảng Mẫu Key Theo Chức Năng
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
| Key Pattern | Data Type | TTL | Mô tả |
|
||||
|------------|-----------|-----|--------|
|
||||
| `app:session:{sessionId}` | Hash | 30 phút | Thông tin session người dùng |
|
||||
| `app:token:refresh:{userId}` | String | 7 ngày | Refresh token |
|
||||
| `app:token:blacklist:{jti}` | String | Thời gian còn lại của token | JWT bị thu hồi |
|
||||
| `app:temp:otp:{userId}` | String | 5 phút | Mã OTP xác thực |
|
||||
| `app:rate:login:{ip}` | String (counter) | 15 phút | Giới hạn số lần đăng nhập |
|
||||
|
||||
### Cache Dữ Liệu (CRUD)
|
||||
|
||||
| Key Pattern | Data Type | TTL | Mô tả |
|
||||
|------------|-----------|-----|--------|
|
||||
| `app:cache:{entity}:detail:{id}` | Hash/String | 10 phút | Cache chi tiết 1 entity |
|
||||
| `app:cache:{entity}:list:{hash}` | String (JSON) | 5 phút | Cache danh sách có phân trang/filter |
|
||||
| `app:cache:{entity}:count` | String | 5 phút | Cache tổng số record |
|
||||
| `app:cache:{entity}:ids:all` | Set | 10 phút | Tập hợp tất cả ID của entity |
|
||||
|
||||
> **`{hash}`** là hash MD5/SHA256 của query parameters (page, filter, sort) để tạo key unique cho mỗi truy vấn khác nhau.
|
||||
|
||||
### 🔔 Real-time & Pub/Sub
|
||||
|
||||
| Key Pattern | Data Type | TTL | Mô tả |
|
||||
|------------|-----------|-----|--------|
|
||||
| `app:pub:notification:{userId}` | Channel | — | Kênh thông báo realtime |
|
||||
| `app:queue:email:pending` | List | — | Hàng đợi gửi email |
|
||||
| `app:counter:online:users` | String | — | Đếm user đang online |
|
||||
|
||||
### 🔒 Distributed Locking
|
||||
|
||||
| Key Pattern | Data Type | TTL | Mô tả |
|
||||
|------------|-----------|-----|--------|
|
||||
| `app:lock:{resource}:{id}` | String | 30 giây | Lock tài nguyên để tránh race condition |
|
||||
|
||||
---
|
||||
|
||||
## 5. Quy Tắc Về Độ Dài Key
|
||||
|
||||
Theo bài viết từ Redis:
|
||||
|
||||
> *"Storing 1,000,000 keys, each set with a 32-character value, will consume about **96MB** when using 6-character key names, and **111MB** with 12-character names. This overhead of more than **15%** becomes quite significant as your number of keys grows."*
|
||||
|
||||
### Hướng dẫn cân bằng
|
||||
|
||||
| Quy tắc | Chi tiết |
|
||||
|---------|---------|
|
||||
| **Tối đa 50 ký tự** | Giữ tổng chiều dài key không quá 50 ký tự |
|
||||
| **Viết tắt hợp lý** | `usr` thay vì `user`, `prod` thay vì `product` — nhưng phải có **bảng chú giải** |
|
||||
| **Không lạm dụng viết tắt** | Key phải đọc được, tránh như `a:b:c:d:1` |
|
||||
| **Ưu tiên rõ ràng nếu < 10K keys** | Nếu dataset nhỏ, ưu tiên key dễ đọc hơn key ngắn |
|
||||
|
||||
### Bảng viết tắt chuẩn (nếu cần tối ưu)
|
||||
|
||||
| Viết tắt | Đầy đủ |
|
||||
|----------|--------|
|
||||
| `usr` | user |
|
||||
| `prod` | product |
|
||||
| `ord` | order |
|
||||
| `sess` | session |
|
||||
| `notif` | notification |
|
||||
| `cfg` | config |
|
||||
| `inv` | inventory |
|
||||
| `txn` | transaction |
|
||||
|
||||
---
|
||||
|
||||
## 6. Chọn Đúng Data Structure
|
||||
|
||||
Theo bài viết, việc chọn đúng cấu trúc dữ liệu giúp **tối ưu bộ nhớ và hiệu suất**:
|
||||
|
||||
| Data Structure | Khi nào dùng | Ví dụ trong dự án |
|
||||
|---------------|-------------|-------------------|
|
||||
| **String** | Giá trị đơn giản, counter, cache JSON | `app:cache:product:detail:123` → JSON string |
|
||||
| **Hash** | Object có nhiều field, profile user | `app:session:abc123` → `{userId, role, name, exp}` |
|
||||
| **List** | Queue, danh sách có thứ tự, cho phép trùng | `app:queue:email:pending` → FIFO queue |
|
||||
| **Set** | Tập hợp unique, kiểm tra membership | `app:cache:user:ids:all` → tập hợp user IDs |
|
||||
| **Sorted Set** | Leaderboard, ranking, timeline | `app:rank:score:board:game1` → ranking theo điểm |
|
||||
| **Bitmap** | Track true/false cho lượng lớn, analytics | `app:analytics:daily:login:2026-02-23` → bit per user |
|
||||
|
||||
### 💡 Tips quan trọng:
|
||||
- **Hash thay vì nhiều String**: Nhóm dữ liệu liên quan vào 1 Hash thay vì tạo nhiều key String riêng lẻ → tiết kiệm bộ nhớ đáng kể
|
||||
- **List thay vì Set**: Nếu không cần kiểm tra uniqueness, List nhanh hơn và tốn ít RAM hơn
|
||||
- **Tránh Sorted Set nếu không cần ranking**: Sorted Set tốn nhiều bộ nhớ và phức tạp nhất
|
||||
|
||||
---
|
||||
|
||||
## 7. Quản Lý Key: SCAN thay vì KEYS
|
||||
|
||||
### ⚠️ KHÔNG BAO GIỜ dùng `KEYS` trong production!
|
||||
|
||||
Lệnh `KEYS *` sẽ:
|
||||
- **Block toàn bộ Redis server** cho đến khi hoàn thành
|
||||
- **Tiêu tốn RAM** nguy hiểm
|
||||
- Gây **downtime** nếu dataset lớn
|
||||
|
||||
### Dùng `SCAN` để duyệt key an toàn
|
||||
|
||||
```bash
|
||||
# Cú pháp
|
||||
SCAN cursor [MATCH pattern] [COUNT count]
|
||||
|
||||
# Ví dụ: Tìm tất cả cache key của product
|
||||
SCAN 0 MATCH "app:cache:product:*" COUNT 100
|
||||
|
||||
# Dùng HSCAN cho Hash
|
||||
HSCAN app:session:abc123 0 MATCH "*"
|
||||
|
||||
# Dùng SSCAN cho Set
|
||||
SSCAN app:cache:user:ids:all 0 COUNT 50
|
||||
```
|
||||
|
||||
### Trong code C# (StackExchange.Redis):
|
||||
```csharp
|
||||
// Đúng: Dùng SCAN
|
||||
var server = redis.GetServer(endpoint);
|
||||
var keys = server.Keys(pattern: "app:cache:product:*", pageSize: 100);
|
||||
|
||||
// Sai: Dùng KEYS (block server)
|
||||
// var keys = server.Keys(pattern: "app:cache:product:*", pageSize: int.MaxValue);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Chiến Lược TTL & Expiration
|
||||
|
||||
| Loại dữ liệu | TTL khuyến nghị | Lý do |
|
||||
|--------------|----------------|-------|
|
||||
| **Cache API response** | 5 – 15 phút | Đảm bảo data tương đối fresh |
|
||||
| **Session** | 30 phút – 2 giờ | Theo session timeout của app |
|
||||
| **OTP / Verification** | 3 – 10 phút | Bảo mật |
|
||||
| **Refresh Token** | 7 – 30 ngày | Theo chính sách auth |
|
||||
| **Rate Limit Counter** | 1 – 60 phút | Theo window rate limit |
|
||||
| **Distributed Lock** | 10 – 60 giây | Tránh deadlock |
|
||||
| **Analytics / Counter** | Không expire hoặc 24h | Tùy yêu cầu business |
|
||||
|
||||
### ⚡ Luôn đặt TTL cho mọi cache key!
|
||||
|
||||
```csharp
|
||||
// Luôn set expiration khi SET
|
||||
await db.StringSetAsync("app:cache:product:detail:123", jsonData, TimeSpan.FromMinutes(10));
|
||||
|
||||
// Hoặc set TTL riêng
|
||||
await db.KeyExpireAsync("app:cache:product:detail:123", TimeSpan.FromMinutes(10));
|
||||
```
|
||||
|
||||
> ⚠️ **Cảnh báo:** Key không có TTL sẽ tồn tại mãi mãi → nguy cơ memory leak!
|
||||
|
||||
---
|
||||
|
||||
## 9. Cache Invalidation
|
||||
|
||||
Khi dữ liệu thay đổi trong database chính (SQL, MongoDB...), cần **xóa cache Redis tương ứng**:
|
||||
|
||||
### Pattern: Tag-based Invalidation
|
||||
|
||||
```
|
||||
# Khi tạo cache, thêm key vào một Set quản lý
|
||||
SADD app:tags:product app:cache:product:detail:123
|
||||
SADD app:tags:product app:cache:product:list:abc
|
||||
SADD app:tags:product app:cache:product:count
|
||||
|
||||
# Khi invalidate, duyệt Set và xóa tất cả
|
||||
SMEMBERS app:tags:product → lấy tất cả key liên quan
|
||||
DEL app:cache:product:detail:123 app:cache:product:list:abc ...
|
||||
DEL app:tags:product
|
||||
```
|
||||
|
||||
### Trong code C#:
|
||||
|
||||
```csharp
|
||||
public async Task InvalidateCacheByTagAsync(string tag)
|
||||
{
|
||||
var db = _redis.GetDatabase();
|
||||
var tagKey = $"app:tags:{tag}";
|
||||
|
||||
// Lấy tất cả cache key thuộc tag này
|
||||
var members = await db.SetMembersAsync(tagKey);
|
||||
|
||||
if (members.Length > 0)
|
||||
{
|
||||
// Xóa tất cả cache key
|
||||
var keys = members.Select(m => (RedisKey)m.ToString()).ToArray();
|
||||
await db.KeyDeleteAsync(keys);
|
||||
}
|
||||
|
||||
// Xóa luôn tag set
|
||||
await db.KeyDeleteAsync(tagKey);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Ví Dụ Thực Tế Trong Dự Án
|
||||
|
||||
Áp dụng quy ước cho dự án **MyNewProjectName** (Clean Architecture):
|
||||
|
||||
### Entity: `SampleEntity`
|
||||
|
||||
```
|
||||
# Cache chi tiết
|
||||
app:cache:sample:detail:{id}
|
||||
|
||||
# Cache danh sách (hash = MD5 của query params)
|
||||
app:cache:sample:list:{queryHash}
|
||||
|
||||
# Cache count
|
||||
app:cache:sample:count
|
||||
|
||||
# Tag để invalidation
|
||||
app:tags:sample → Set chứa tất cả key cache liên quan
|
||||
|
||||
# Lock khi cập nhật
|
||||
app:lock:sample:update:{id}
|
||||
```
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
```
|
||||
# Session sau khi login
|
||||
app:session:{sessionId} → Hash { userId, role, loginAt, ip }
|
||||
|
||||
# Refresh token
|
||||
app:token:refresh:{userId} → "eyJhbGciOi..."
|
||||
|
||||
# OTP xác thực email
|
||||
app:temp:otp:{userId} → "123456" (TTL: 5 phút)
|
||||
|
||||
# Blacklist JWT đã revoke
|
||||
app:token:blacklist:{jti} → "1" (TTL: thời gian còn lại của token)
|
||||
|
||||
# Rate limit login
|
||||
app:rate:login:{ip} → counter (TTL: 15 phút, max: 5 lần)
|
||||
```
|
||||
|
||||
### Constant class trong C#
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Định nghĩa tất cả Redis key patterns sử dụng trong dự án.
|
||||
/// Sử dụng dấu ':' làm namespace separator theo convention chuẩn Redis.
|
||||
/// </summary>
|
||||
public static class RedisKeyPatterns
|
||||
{
|
||||
private const string Prefix = "app";
|
||||
|
||||
// ── Cache ──────────────────────────────────
|
||||
public static string CacheDetail(string entity, string id)
|
||||
=> $"{Prefix}:cache:{entity}:detail:{id}";
|
||||
|
||||
public static string CacheList(string entity, string queryHash)
|
||||
=> $"{Prefix}:cache:{entity}:list:{queryHash}";
|
||||
|
||||
public static string CacheCount(string entity)
|
||||
=> $"{Prefix}:cache:{entity}:count";
|
||||
|
||||
// ── Tags (cho cache invalidation) ──────────
|
||||
public static string Tag(string entity)
|
||||
=> $"{Prefix}:tags:{entity}";
|
||||
|
||||
// ── Session ────────────────────────────────
|
||||
public static string Session(string sessionId)
|
||||
=> $"{Prefix}:session:{sessionId}";
|
||||
|
||||
// ── Token ──────────────────────────────────
|
||||
public static string RefreshToken(string userId)
|
||||
=> $"{Prefix}:token:refresh:{userId}";
|
||||
|
||||
public static string BlacklistToken(string jti)
|
||||
=> $"{Prefix}:token:blacklist:{jti}";
|
||||
|
||||
// ── Temporary ──────────────────────────────
|
||||
public static string Otp(string userId)
|
||||
=> $"{Prefix}:temp:otp:{userId}";
|
||||
|
||||
// ── Rate Limiting ──────────────────────────
|
||||
public static string RateLimit(string action, string identifier)
|
||||
=> $"{Prefix}:rate:{action}:{identifier}";
|
||||
|
||||
// ── Distributed Lock ───────────────────────
|
||||
public static string Lock(string resource, string id)
|
||||
=> $"{Prefix}:lock:{resource}:{id}";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Checklist Trước Khi Tạo Key Mới
|
||||
|
||||
- [ ] Key có sử dụng namespace với dấu `:` không?
|
||||
- [ ] Key có phản ánh đúng layer/module không?
|
||||
- [ ] Key có ngắn gọn nhưng vẫn dễ hiểu không? (< 50 ký tự)
|
||||
- [ ] Đã chọn đúng data structure (String/Hash/List/Set/Sorted Set)?
|
||||
- [ ] Đã đặt TTL phù hợp cho key?
|
||||
- [ ] Đã có chiến lược invalidation khi data thay đổi?
|
||||
- [ ] Đã thêm key pattern vào class `RedisKeyPatterns`?
|
||||
- [ ] Không dùng `KEYS` command trong code production?
|
||||
|
||||
---
|
||||
|
||||
> **💡 Ghi nhớ:** *"Luôn namespace key bằng dấu `:`, giữ key ngắn gọn, chọn đúng data structure, dùng SCAN thay vì KEYS, và luôn đặt TTL!"*
|
||||
Reference in New Issue
Block a user