Files
2026-02-26 14:04:18 +07:00

392 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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!"*