Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion i18n/zh-TW/GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
- 程式碼區塊內的程式碼不翻譯,只翻譯註解。
- 中文與英文、數字、inline code 之間加一個半形空格;中文句子使用全形標點。
- 下表標示「保留」的詞在中文句子中直接使用英文。
- 章節錨點(`#authenticated-endpoints` 等)由英文標題產生;連到英文文件時沿用英文錨點。
- 章節錨點沿用英文:每個翻譯後的標題都以 `{#id}` 指定與英文頁面相同的錨點,例如 `## 快取鍵 {#cache-keys}`,讓兩種語言的連結可以互換。
- 中文段落不要在句中換行:換行在 HTML 中會變成空格,出現在兩個中文字之間。一個段落或清單項目寫在同一行。
- 尚未翻譯的頁面以英文網址連結(`https://fastapi-cachex.readthedocs.io/en/latest/...`),並在連結後加上「(英文)」。

## 詞彙

Expand All @@ -20,9 +22,14 @@
| cache(動詞) | 快取、存入快取 | |
| cache hit / miss | 快取命中/未命中 | |
| cache key | 快取鍵 | |
| (cache) entry | 項目、快取項目 | |
| key builder | 保留 | |
| backend | 後端 | |
| header | 標頭 | |
| request / response | 請求/回應 | |
| (response) body | 本文 | |
| render(回應) | 產生、重新產生 | |
| replay | 重播 | 重播快取的回應;replay attack:重送攻擊 |
| handler | 保留 | 指路由處理函式 |
| route / endpoint | 路由/端點 | |
| decorator | 裝飾器 | |
Expand All @@ -38,12 +45,20 @@
| atomic | 原子性、原子操作 | |
| counter | 計數器 | |
| lock | 鎖 | |
| one-shot | 一次性 | |
| glob | 萬用字元(glob) | |
| no-op | 保留 | |
| stampede | 保留(cache stampede) | |
| token | 權杖 | |
| signature / sign | 簽章/簽署 | |
| session | 保留(Session) | |
| claim | 保留 | JWT claim |
| state(OAuth) | 保留 | |
| serialize / deserialize | 序列化/反序列化 | |
| process / multi-process | 行程/多行程 | |
| worker | 保留 | 工作執行緒(worker thread)除外 |
| production | 正式環境 | |
| callback(OAuth) | 回呼(callback) | |
| extra | 保留 | 套件的選用依賴,例:`redis` extra |
| deprecated | 已棄用 | |
| breaking change | 破壞性變更 | |
50 changes: 50 additions & 0 deletions i18n/zh-TW/docs/APP_CACHE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 應用層快取 {#application-cache}

除了透過 `@cache` 快取 HTTP 回應,你也可以用 `CacheManager` 在商業邏輯中直接快取任意可 JSON 序列化的 Python 值。它是一層輕量、具命名空間的包裝,底層使用透過 `BackendProxy` 設定的後端。

```python
from fastapi_cachex import AppCache, CacheManager


@app.get("/expensive")
async def expensive_operation(cache: AppCache):
result = await cache.get("expensive:result")
if result is None:
result = perform_expensive_calculation()
await cache.set("expensive:result", result, ttl=300)
return result


# 也可以直接建立實例,例如在請求之外使用:
manager = CacheManager(key_prefix="myapp:", default_ttl=60)
await manager.set("user:42", {"name": "Alice"})
user = await manager.get("user:42") # {"name": "Alice"}
await manager.delete("user:42")
await manager.clear_prefix() # 清除 "myapp:" 底下的所有項目

# 未命中時才計算:只有在鍵不存在、已過期或無法解碼時才會執行 `factory`。
# 它可以是同步或非同步函式。
profile = await manager.get_or_set("user:42", lambda: load_user(42), ttl=300)

# 只在鍵尚未被占用時寫入:同時呼叫的呼叫者中恰好只有一個會得到 True。
if await manager.add(f"webhook:{event_id}", True, ttl=86400):
await deliver_webhook(event_id)

# 在此 manager 的命名空間內做萬用字元(glob)比對,使用後端原生的模式比對
# 支援(Redis SCAN),而不是列舉所有鍵。
await manager.clear_pattern("user:*") # 比對 "myapp:user:*"
```

## 行為 {#behavior}

- `get()` 在快取未命中時回傳 `None`(或你提供的 `default=`),遇到不存在或損毀的項目也絕不會拋出例外。
- `set()` 遇到無法 JSON 序列化的值時,會讓 `TypeError` 直接往外拋出。
- `get_or_set()` 不提供 cache stampede 保護:同一個鍵同時發生多次未命中時,每一次都會執行 `factory`。
- `add()` 只在鍵尚未被占用時寫入值,並回傳是否有寫入。檢查與寫入是同一個後端原子操作(`set_if_absent`),因此適合「每個鍵只做一次」的工作,例如 webhook 或電子郵件的去重。已過期的鍵視為未被占用;存放無法解碼之值的鍵則不算,即使 `get()` 會把它當成未命中。
- 鍵預設位於獨立、以 `cache:` 為前綴的命名空間,與 HTTP 路由快取及 OAuth state 分開,因此 `clear()`/`clear_prefix()` 絕不會動到無關的快取項目。
- `AppCache` 依賴項在第一次使用時會建立並註冊一個預設的 `CacheManager`;`CacheManagerProxy.set()` 則可改為註冊你自己的實例。

> [!NOTE]
> `clear()`/`clear_prefix()` 是以後端的 `get_all_keys()` 與 `delete_many()` 實作(在 Redis 上是一次批次 `DEL`)。由於 Memcached 不支援列舉鍵(見[後端](BACKENDS.md#memcached)),這些方法以及 `clear_pattern()` 在 Memcached 後端上不會有任何作用;`get()`/`set()`/`add()`/`delete()`/`has()` 則照常運作。若需要大量清除,請使用 Redis 或記憶體後端。

完整的方法清單請見 [API 參考](https://fastapi-cachex.readthedocs.io/en/latest/api/cache-manager/)(英文)。
135 changes: 135 additions & 0 deletions i18n/zh-TW/docs/BACKENDS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# 後端 {#backends}

所有快取,包括 HTTP 回應、`CacheManager` 的值、Session 與 OAuth state,都存放在同一個後端,並在啟動時以 `BackendProxy.set()` 註冊一次。

## 選擇後端 {#choosing-a-backend}

| 情境 | 建議後端 | 原因 |
|----------|---------------------|-----|
| 開發與測試 | MemoryBackend | 快速,無外部依賴 |
| 分散式系統 | Redis | 非同步、有效率,支援依模式清除 |
| 簡單快取 | Memcached | 穩定、成熟(但無法列舉鍵,因此不支援依模式/路徑清除,也無法監控) |
| 多行程部署 | Redis | 共用快取、一致性 |

所有後端都會以前綴為鍵建立命名空間(預設為 `fastapi_cachex:`,可用 `key_prefix=` 變更),以避免與其他應用程式衝突。

## 記憶體(預設) {#in-memory-default}

若未指定後端,FastAPI-CacheX 預設會使用記憶體快取。這適合開發與測試用途。此後端會自動執行清理工作,每 60 秒移除一次已過期的項目(`MemoryBackend(cleanup_interval=60)`)。

```python
from fastapi_cachex.backends import MemoryBackend
from fastapi_cachex import BackendProxy

backend = MemoryBackend()
BackendProxy.set(backend)
```

> [!NOTE]
> 記憶體快取不適合用於多行程的正式環境。每個行程都各自維護獨立的快取。

## Redis {#redis}

以 `uv add "fastapi-cachex[redis]"` 安裝此 extra。

```python
from fastapi_cachex.backends import AsyncRedisCacheBackend
from fastapi_cachex import BackendProxy

backend = AsyncRedisCacheBackend(host="127.0.0.1", port=6379, db=0)
BackendProxy.set(backend)
```

- 完全非同步的實作
- 支援依模式清除鍵
- 使用 SCAN 而非 KEYS,可安全用於正式環境(不會阻塞)
- 預設以 `fastapi_cachex:` 前綴建立命名空間;多租戶情境可傳入 `key_prefix="myapp:cache:"`
- 只有傳給 `clear_pattern()` 的模式是萬用字元(glob)模式。鍵前綴與傳給 `clear_path()` 的路徑都以字面值比對,因此其中的 `*`、`?`、`[` 或 `]` 不會觸及前綴以外的鍵,也不會漏掉該路徑

**從模型設定**:`RedisConfig` 是具有相同設定項與驗證的 pydantic 模型,當設定來自環境變數或設定檔時很方便:

```python
from fastapi_cachex.backends import AsyncRedisCacheBackend
from fastapi_cachex.backends.config import RedisConfig

config = RedisConfig(
host="127.0.0.1",
port=6379,
password=None, # SecretStr | None
db=0,
encoding="utf-8", # 用戶端解碼伺服器回應的方式
socket_timeout=1.0, # 秒;適用於讀取/寫入
socket_connect_timeout=1.0,
key_prefix="fastapi_cachex:",
protocol=2, # RESP 版本,2 或 3
)
backend = AsyncRedisCacheBackend.load_from_config(config)
BackendProxy.set(backend)
```

除非你需要 RESP3 的功能,*而且*你的 `hiredis` 建置支援它(RESP3 需要 hiredis >= 3.0),否則請保留 `protocol=2`。Redis 8.0 支援 RESP3,但較舊的 hiredis 會無法協商使用它。

## Memcached {#memcached}

以 `uv add "fastapi-cachex[memcache]"` 安裝此 extra(注意是 `memcache`,不是 `memcached`)。

```python
from fastapi_cachex.backends import MemcachedBackend
from fastapi_cachex import BackendProxy

backend = MemcachedBackend(servers=["localhost:11211"])
BackendProxy.set(backend)
```

**限制**:

- Memcached 協定不支援依模式清除鍵(`clear_pattern`)
- 無法列舉鍵:`get_all_keys()`/`get_cache_data()` 會回傳空結果(並發出 `RuntimeWarning`),因此監控路由不會顯示任何內容
- `clear_path()` 只會刪除完全相符的那個鍵;`include_params` 沒有作用
- `clear()` 會發出 `flush_all`,清空整台 Memcached 伺服器,而不只是這個命名空間
- Memcached 會拒絕的鍵(超過 250 位元組、含空白字元或非 ASCII 字元)會改以其 SHA-256 摘要儲存
- 若需要依模式清除快取,請考慮使用 Redis 後端

同步的 pymemcache 用戶端在工作執行緒中執行,並使用連線池,因此並行的請求絕不會共用同一個 socket。寫入會等待伺服器確認(`default_noreply=False`),因此只要 `set()` 返回,就能從連線池中的任何連線讀到該值。

## 後端的原子操作 {#atomic-backend-primitives}

每個後端都在 `get`/`set`/`delete` 之上提供原子操作,供會被許多並行請求讀寫的值使用:

```python
import secrets

from fastapi_cachex import BackendProxy
from fastapi_cachex.types import CacheEntry

backend = BackendProxy.get()

# 固定時間窗計數器:首次使用時建立,`ttl` 只在那時套用。
hits = await backend.increment(f"resend:{user_id}", ttl=86400)
if hits > 3:
raise TooManyRequests()

# 一次性的值:多個並行呼叫者中恰好只有一個會取得該項目。
grant = await backend.get_and_delete(f"grant:{token}")

# 鎖/名額:只在未被占用時取得,且只在仍屬於你時釋放。
owner = CacheEntry(fingerprint="lock", content=secrets.token_bytes(16))
if await backend.set_if_absent(f"stream:{user_id}", owner, ttl=300):
try:
...
finally:
await backend.delete_if_equals(f"stream:{user_id}", owner)
```

- `increment(key, delta=1, ttl=None) -> int`:記憶體後端在鎖內執行讀取—修改—寫入,Redis 執行 Lua 腳本(`EXISTS` + `INCRBY` + `EXPIRE`),Memcached 則使用 `ADD` + `INCR`/`DECR`(Memcached 的計數器最低停在 0)。計數器可透過 `get()` 讀到,形式為 fingerprint 為 `COUNTER_FINGERPRINT`、內容為十進位數值的 `CacheEntry`,因此 `delete`/`clear*` 與監控路由都會把它當成一般項目處理。對存放快取回應的鍵執行 increment 會拋出 `CacheXError`。
- `get_and_delete(key) -> CacheEntry | None`:記憶體後端在鎖內 pop,Redis 使用 `GETDEL`(伺服器 6.2 以上),Memcached 則只在自己的 `DELETE` 勝出時才回傳該值。`StateManager.consume_state`、`StateManager.delete_state`、`CacheManager.delete` 與 `invalidate()` 都建立在它之上。
- `set_if_absent(key, value, ttl=None) -> bool`:只在 `key` 不存在時儲存 `value`(已過期的鍵視為不存在),並回報是否有寫入。記憶體後端在鎖內檢查,Redis 使用 `SET NX EX`,Memcached 使用 `ADD`。
- `delete_if_equals(key, expected) -> bool`:只在 `key` 仍存放 `expected` 時才移除它,因此項目已過期的持有者無法釋放已被他人取得的鎖。請在你儲存的項目中放入唯一的權杖,並以同一個項目釋放。記憶體後端在鎖內比較,Redis 透過 Lua 腳本刪除,並在腳本中重新檢查先前比較過的值,Memcached 則使用 `GETS` + 一個讓項目立即過期的 `CAS` 寫入(傳統協定的 `DELETE` 不接受 CAS 權杖)。

這四個方法在 `BaseCacheBackend` 上都有非原子性的後備實作,因此只實作抽象方法的第三方後端仍可正常運作;覆寫它們才能得到真正的原子性。

## TTL 值 {#ttl-values}

每個 `ttl` 參數(`set`、`set_if_absent`、`increment`,以及建立在它們之上的 `CacheManager` 與 `StateManager` 方法和預設值)只能是 `None`(表示項目永不過期),或正數秒數。零與負值會拋出 `ValueError`。底層儲存對這些值的解讀各不相同:Memcached 把 exptime `0` 視為「永不過期」,Redis 拒絕 `EX 0`,而行程內的 dict 則會立即讓項目過期。第三方後端應在其 `set` 中呼叫 `fastapi_cachex.backends.base.validate_ttl(ttl)`,以遵循相同規則。(`@cache(ttl=0)` 是另一回事:它會送出 `max-age=0`,且絕不會把 `0` 傳給後端;見 [HTTP 快取](HTTP_CACHING.md)。)

各後端如何儲存項目,請見[快取流程](CACHE_FLOW.md#backend-storage-formats);類別本身請見 [API 參考](https://fastapi-cachex.readthedocs.io/en/latest/api/backends/)(英文)。
Loading
Loading