技術筆記 Redis
技術筆記

Redis 核心知識

In-Memory5 大資料結構快取策略CQRS Projection

傳統資料庫讀寫要到磁碟(毫秒級),Redis 把資料放在記憶體(奈秒~微秒級),快 100 倍以上。但它不是拿來取代資料庫的——它站在資料庫前面當快速通道,讓 90% 的請求根本不需要碰 DB。

100μs
平均讀取延遲
1,000,000+
ops/sec 吞吐量
5
種核心資料結構

為什麼這麼快?記憶體 vs 磁碟

In-Memory 儲存
所有資料存在 RAM。記憶體存取速度是磁碟的 10 萬倍——Redis 讀一個 key 通常只要 100 微秒(0.1ms),MySQL 查詢常常要 1ms~10ms。
單執行緒 + I/O 多路復用
主執行緒處理命令是單線程的(避免鎖競爭),但用作業系統的 epoll 監控數萬個連線的 I/O 狀態,所以單線程也能扛高並發。epoll 是什麼?看專頁 →
簡單的資料結構
命令的時間複雜度多為 O(1)(GET/SET)或 O(log N)(ZSet)。不像 SQL 要解析查詢計畫、做 JOIN,直接按 key 存取。

六大應用場景

資料快取
熱點資料(商品詳情、用戶資料)放 Redis,避免資料庫被反覆查詢。
Session / Token
登入 Token、Session 存 Redis,多服務節點共享,支援水平擴展。
計數器 / 限流
INCR 是原子操作,可做點讚數、訪問量;INCR + TTL 就是 API Rate Limit。
排行榜
ZSet(有序集合)天然按分數排序,幾行指令就能做即時排行榜。
分散式鎖
SET NX EX(或 Redisson)實現跨服務節點的互斥鎖,防止並發衝突。
Pub/Sub 訊息
輕量的發布訂閱。簡單通知類場景可用,複雜的用 Kafka。

快取命中 vs 未命中

同一個請求,命中與否走的路完全不同——這是理解快取價值最直接的一張圖。

Application
→
Redis Cache
→
Database
點上面的按鈕,看兩種情況下請求各走了哪條路。

Redis vs 關聯式資料庫

不是二選一,而是分工。

比較項目RedisMySQL
儲存位置記憶體(In-Memory)磁碟(Disk)
讀取速度100μs 等級1ms~10ms
資料容量受 RAM 限制(昂貴)TB 等級(便宜)
資料持久性可選(RDB/AOF)強(ACID 事務)
關聯查詢不支援(只有 key 查詢)支援(JOIN/WHERE)
資料結構String/Hash/List/Set/ZSet固定表格欄位
適用場景熱點資料快取、Session、計數器業務資料持久化、複雜查詢
黃金原則:Redis 存熱資料(頻繁讀取、不常變),MySQL 存冷資料(長期保存、需要關聯查詢)。兩者配合才能兼顧速度與完整性。

五種核心資料結構

選對資料結構,效能事半功倍。點上面的類型切換指令與使用場景。

String(字串)

最基本的類型。Key-Value 中 Value 就是字串。可以存文字、數字、JSON、甚至二進位(圖片)。數字型 String 支援原子加減。

SET   user:1001:name  "Alice"
GET   user:1001:name          # "Alice"
SET   user:1001:score 100
INCR  user:1001:score          # 101(原子操作)
INCRBY user:1001:score 50      # 151
SETEX session:token:abc 3600 "{...}"  # 3600秒後過期
複雜度:GET/SET = O(1)
常見使用場景
Session / Token
SETEX 儲存登入 Token,TTL 到期自動失效,天然實現 Session 過期機制。
計數器 / 限流
INCR 是原子操作,天然線程安全。每秒訪問次數 > 100 就拒絕請求(Rate Limiting)。
物件快取(JSON)
將商品詳情序列化為 JSON 字串存入,比 Hash 操作更簡單,適合整體讀取的物件。
Hash(雜湊)

每個 Key 對應一個包含多個欄位的 Map。適合儲存結構化物件,可以只讀取/更新特定欄位,不像 String 要整個 JSON 反序列化。

HSET user:1001 name "Alice" age 30 city "Taipei"
HGET user:1001 name          # "Alice"
HMGET user:1001 name city    # ["Alice","Taipei"]
HGETALL user:1001            # 所有欄位
HINCRBY user:1001 age 1      # age: 31
HDEL user:1001 city          # 刪除 city 欄位
複雜度:HGET = O(1),HGETALL = O(N) (N=欄位數)
常見使用場景
用戶資料快取
每個欄位獨立更新。只想更新 age 就用 HINCRBY,不用讀出整個 JSON 再寫回去。
購物車
用商品 ID 當欄位名,數量當 Value。HINCRBY 一行指令就能加購。
設定快取
把 application.yml 的設定存入 Hash,讓服務動態讀取,不需重啟就能改設定。
List(列表)

雙向鏈結串列。頭尾插入/移除都是 O(1)。天然適合用來實作 Queue(佇列)或是時間軸類的資料。

LPUSH timeline:user:1001 "post-3"   # 從左插入
RPUSH timeline:user:1001 "post-4"   # 從右插入
LRANGE timeline:user:1001 0 9       # 取前10筆
LPOP  jobs:email                    # 取出並刪除最左邊
RPOP  jobs:email                    # 取出並刪除最右邊
LLEN  timeline:user:1001            # 列表長度
BRPOP jobs:email 30                 # 阻塞等待,最多30秒
複雜度:LPUSH/RPUSH/LPOP = O(1),LRANGE = O(N)
常見使用場景
動態時間軸
LPUSH 把新貼文 ID 放到最前面,LRANGE 0 19 取前 20 筆,實現分頁 Feed。
簡易任務佇列
Producer RPUSH 加入,Consumer BRPOP 阻塞取出。比 Kafka 輕量,適合內部異步任務。
最近瀏覽紀錄
LPUSH 插入最新,LTRIM 只保留前 N 筆,自動維護固定長度的歷史記錄。
Set(無序集合)

無重複、無序的集合。最強大的功能是集合運算:交集(SINTER)、聯集(SUNION)、差集(SDIFF),可用來分析用戶關係。

SADD   tags:post:1  "java" "spring" "redis"
SMEMBERS tags:post:1          # {"java","spring","redis"}
SISMEMBER tags:post:1 "java"  # 1(存在)
SCARD  tags:post:1            # 3(元素數量)

# 兩個用戶的共同追蹤者
SINTER following:alice following:bob

# 所有追蹤者(去重)
SUNION following:alice following:bob
複雜度:SADD/SISMEMBER = O(1),SINTER = O(N*M)
常見使用場景
共同好友 / 共同追蹤
SINTER(following:A, following:B) → 兩人共同追蹤的帳號,一行指令實現「你可能認識的人」。
標籤去重
SADD 天然去重,不需要查詢後再判斷。SMEMBERS 取得文章所有標籤。
抽獎 / 白名單
SRANDMEMBER 隨機取出 N 個不重複成員做抽獎;SISMEMBER 毫秒級白名單驗證。
ZSet(有序集合)

Set 的升級版,每個成員多了一個 score(浮點數分數)。Redis 自動按 score 排序,天然適合排行榜、延遲佇列、時間軸等需要排序的場景。

ZADD  leaderboard 9800 "alice"
ZADD  leaderboard 8500 "bob"
ZADD  leaderboard 9999 "carol"

ZREVRANK leaderboard "alice"    # 排名(從0算,carol=0)
ZREVRANGE leaderboard 0 9 WITHSCORES  # Top 10
ZINCRBY  leaderboard 200 "bob"  # bob分數+200
ZRANGEBYSCORE leaderboard 9000 10000  # 取9000~10000分的人
複雜度:ZADD = O(log N),ZRANGE = O(log N + M)
常見使用場景
即時排行榜
ZINCRBY 加分,ZREVRANGE 取前 N 名,自動維護排序。不需要每次都 ORDER BY 查 DB。
延遲任務佇列
用「執行時間戳」當 score,ZRANGEBYSCORE 取出已到期的任務,實現定時觸發。
熱度排序
文章按瀏覽量、點贊數當 score。ZINCRBY 實時更新分數,ZREVRANGE 取熱門文章列表。
怎麼選?
存一個值(Token、計數)→ String;存一個物件的多個欄位(用戶資料、購物車)→ Hash;需要按順序插入取出(時間軸、任務佇列)→ List;需要去重或集合運算(共同好友、標籤)→ Set;需要自動排序(排行榜、延遲佇列)→ ZSet。

快取策略

快取不是「存進去就完事」,策略決定了資料一致性與系統穩定性。

由應用程式自己負責讀寫 Redis。讀時先查 Redis,Miss 才查 DB,然後把結果存進 Redis。寫時直接寫 DB,同時刪除(而非更新)Redis 中的舊快取。

📖 讀取流程
1
查 Redis(快速)
✗
Cache Miss → 查 MySQL
3
存入 Redis(設 TTL)
4
回傳給用戶
✏️ 寫入流程
1
寫入 MySQL(持久化)
2
DEL Redis 舊快取
⚠️ 寫入時刪除快取(不是更新)!避免「先更新快取、DB 再失敗」導致不一致。下次讀取時 Miss 再重建快取。
✅ 優點:簡單,DB 永遠是真相來源;缺點:第一次 Miss 有延遲(冷啟動);高並發下可能出現「快取擊穿」問題(見下方)。
Write-Through(直寫)
每次寫資料時,同步更新 Redis 和 DB。兩者保持強一致性,但每次寫都有額外 Redis 開銷。

適合:對資料一致性要求高、寫操作不頻繁的場景。
Write-Behind / Write-Back(異步回寫)
寫資料時只更新 Redis,異步批次回寫 DB。速度極快,但宕機時可能遺失未寫入 DB 的資料。

適合:日誌、計數器等可容忍少量遺失的高頻寫入場景。

TTL(Time-To-Live):每個 Key 可以設定過期時間,到期後自動刪除。設定方法:

SET product:1001 "{...}" EX 3600   # 3600 秒後過期
EXPIRE user:session:abc 1800       # 已存在的 key 設定過期
TTL user:session:abc               # 查剩餘秒數,-1=永不過期,-2=已不存在

記憶體滿了怎麼辦?淘汰策略(maxmemory-policy):

策略說明建議場景
noeviction(預設)記憶體滿了直接報錯作為 DB 使用,不允許資料遺失
allkeys-lru淘汰最久未使用的 key快取場景(最推薦)
volatile-lru只淘汰設了 TTL 的舊 key快取 + 持久 key 混用
allkeys-random隨機淘汰任意 key不在乎哪些被刪

三大快取問題

雪崩、穿透、擊穿——名字很像,成因與解法完全不同。

問題:一次性設置大量快取,且它們的 TTL 完全相同。某一刻 TTL 同時到期,大量請求湧入資料庫,可能壓垮 DB。

🔧 解決方案:

  • TTL 加隨機抖動:設定 TTL 時加入隨機值,如 baseTime + random(0, 300),讓過期時間分散
  • 熱點資料永不過期:設 TTL = -1,由後台異步刷新,不讓快取真的過期
  • 多級快取:本地 Caffeine + 分散式 Redis 雙層,Redis 掛了還有 L1 快取兜底
  • Redis 高可用:Sentinel / Cluster 避免 Redis 本身宕機引發雪崩

問題:查詢一個根本不存在的資料(如惡意攻擊者用 id=-1 查詢),Redis 查不到就問 DB,DB 也查不到,但每次請求都會穿透到 DB。大量這樣的請求可以讓 DB 崩潰。

🔧 解決方案:

  • 快取空值:DB 查不到時,也在 Redis 存入 "NULL"(TTL 設短一點,如 60 秒),讓後續請求不再打到 DB
  • Bloom Filter(布隆過濾器):一種超省空間的「這個值可能存在嗎?」判斷器。原理是一個很長的位元陣列(一堆 0),每個 key 用多個雜湊函式算出幾個位置、把那幾格設成 1;查詢時同樣算位置,只要有任一格是 0 就代表這個 key 一定不存在(直接拒絕,不查 Redis 也不查 DB)。它的特性是「說不存在就絕對不存在,說存在則可能是誤判」——用極小的記憶體,就能擋掉絕大多數查不存在 key 的攻擊。
  • 業務層驗證:id 必須為正整數、格式驗證,非法請求直接 400

問題:某個超熱門的 key(如促銷商品詳情)TTL 到期的瞬間,大量並發請求全部 Cache Miss,同時湧入 DB 重建快取,形成瞬間的 DB 高負載(比雪崩更集中)。

🔧 解決方案:

  • 分散式鎖:Cache Miss 時,只讓第一個請求拿到鎖去查 DB 重建快取,其他請求等待或短暫使用舊資料
  • 熱點 key 永不過期:不設 TTL,由背景任務定期刷新。是解決擊穿最簡單有效的方法
  • 邏輯過期:TTL 只是邏輯標記(存在 Value 裡),物理上 key 不過期,過期時非同步觸發更新

1. 引入依賴

build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.apache.commons:commons-pool2'   // 連線池(必要)
// 若使用分散式鎖
implementation 'org.redisson:redisson-spring-boot-starter:3.25.0'

2. 核心配置

yaml
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: your-password   # 如有設定
      timeout: 2000             # 連線超時 2s
      lettuce:
        pool:
          max-active: 20        # 最大連線數
          max-idle: 10
          min-idle: 2
RedisTemplate 序列化配置
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);
    // Key 用字串序列化
    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    // Value 用 JSON 序列化(人類可讀)
    Jackson2JsonRedisSerializer<Object> json = new Jackson2JsonRedisSerializer<>(Object.class);
    template.setValueSerializer(json);
    template.setHashValueSerializer(json);
    return template;
}

3. RedisTemplate 基本操作

@Service
@RequiredArgsConstructor
public class ProductCacheService {

    private final RedisTemplate<String, Object> redis;

    // String 操作
    public void cacheProduct(Long id, Product p) {
        redis.opsForValue().set("product:" + id, p, Duration.ofHours(1));
    }
    public Product getProduct(Long id) {
        return (Product) redis.opsForValue().get("product:" + id);
    }

    // Hash 操作
    public void updateUserField(Long uid, String field, Object val) {
        redis.opsForHash().put("user:" + uid, field, val);
    }

    // ZSet 排行榜
    public void addScore(String uid, double score) {
        redis.opsForZSet().incrementScore("leaderboard", uid, score);
    }
    public Set<ZSetOperations.TypedTuple<Object>> getTopN(int n) {
        return redis.opsForZSet().reverseRangeWithScores("leaderboard", 0, n - 1);
    }

    // 刪除快取
    public void evict(Long id) {
        redis.delete("product:" + id);
    }
}

4. 宣告式快取:@Cacheable / @CacheEvict

Spring Cache 抽象層。在方法上加 Annotation,框架自動處理快取邏輯,不必手寫 Redis 操作。

@Service
public class ProductService {

    // 首次呼叫查 DB 並存入快取;之後直接回傳快取,不執行方法體
    // key = "product::1001",TTL 由 CacheManager 統一設定
    @Cacheable(value = "product", key = "#id", unless = "#result == null")
    public Product findById(Long id) {
        return productRepo.findById(id).orElse(null);  // 只有 Cache Miss 才執行
    }

    // 更新資料後,刪除快取(讓下次讀取重建)
    @CacheEvict(value = "product", key = "#product.id")
    public Product update(Product product) {
        return productRepo.save(product);
    }

    // 更新後主動更新快取(而非刪除)
    @CachePut(value = "product", key = "#result.id")
    public Product save(Product product) {
        return productRepo.save(product);
    }
}

// 在 application.yml 設定各快取的 TTL
// spring.cache.redis.time-to-live=3600000   # 全域 1 小時
// 或用 RedisCacheConfiguration 精細控制每個 cacheName 的 TTL
@Cacheable 適合讀操作;@CacheEvict 適合更新後清除;@CachePut 適合更新後主動刷新。三個可以搭配使用。

5. CQRS Projection:事件驅動的快取更新

Projection 本身不是 Redis 的功能,而是 CQRS + Event Sourcing 架構裡的概念。Domain Event 發生時,事件處理器會主動更新 Read Model,而這個讀模型通常就存在 Redis。

先搞懂兩個前提術語
  • CQRS(命令查詢職責分離):把「寫資料的模型」和「讀資料的模型」拆成兩套。傳統做法同一張表既寫又讀;CQRS 讓寫用正規化的 DB,讀用預先算好、攤平的資料(例如存在 Redis),讀取時就不必臨時 JOIN。
  • Event Sourcing(事件溯源):不存「資料現在的狀態」,而是存「一連串發生過的事件」(OrderCreated → OrderPaid → OrderShipped…)。要知道現在的狀態,就把事件依序重播算出來。
  • 兩者怎麼接:每當有事件發生就拿它去更新 Read Model——這個「用事件更新讀模型」的動作就叫 Projection(投影)。
OrderCreatedDomain Event
→
OrderProjectionEventHandler
→
Redisorder:summary:{id}
需要 Projection
資料變更頻繁且需要即時一致性,例如訂單狀態、庫存快取。事件觸發後立刻更新 Redis。
不需要 Projection
不常更新的資料用 Cache-Aside + TTL 就好,到期後 lazy reload。靜態資料(商品分類、城市列表)適合這種。
// 1. 定義 Domain Event
public record OrderCreatedEvent(Long orderId, String userId, BigDecimal amount) {}

// 2. 發布事件(在 Service 或 Aggregate 中)
@Service
public class OrderService {
    private final ApplicationEventPublisher events;

    public Order createOrder(CreateOrderCmd cmd) {
        Order order = orderRepo.save(new Order(cmd));
        events.publishEvent(new OrderCreatedEvent(order.getId(), cmd.userId(), order.total()));
        return order;
    }
}

// 3. Projection:監聽事件,主動更新 Redis Read Model
@Component
@RequiredArgsConstructor
public class OrderSummaryProjection {

    private final RedisTemplate<String, Object> redis;

    @EventListener  // 同步;用 @TransactionalEventListener 可等 DB commit 後再執行
    public void on(OrderCreatedEvent event) {
        OrderSummary summary = OrderSummary.builder()
            .orderId(event.orderId())
            .status("CREATED")
            .amount(event.amount())
            .build();

        // 主動更新 Redis,下次讀取直接命中,不需要去查 DB
        redis.opsForValue().set(
            "order:summary:" + event.orderId(),
            summary,
            Duration.ofHours(24)
        );
    }
}
一句話總結:Projection = 事件主動推送更新快取。需要即時性的用 Projection;讀多寫少的靜態資料用 Cache-Aside + TTL 就夠了。

6. 分散式鎖(Redisson)

多個服務節點同時操作同一資源時(扣庫存、初始化快取),需要分散式鎖確保互斥。

@Service
@RequiredArgsConstructor
public class InventoryService {

    private final RedissonClient redisson;
    private final InventoryRepo repo;

    public boolean deductStock(Long productId, int qty) {
        RLock lock = redisson.getLock("lock:inventory:" + productId);
        try {
            // 等待最多 3 秒拿鎖;拿到鎖後 10 秒自動釋放(防止死鎖)
            boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (!acquired) return false;  // 拿不到鎖,直接回傳失敗

            Inventory inv = repo.findById(productId).orElseThrow();
            if (inv.getStock() < qty) return false;
            inv.setStock(inv.getStock() - qty);
            repo.save(inv);
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) lock.unlock();  // 一定要在 finally 釋放
        }
    }
}
不要自己用 SET NX 實作鎖——釋放鎖時要判斷鎖是否還是自己的,邏輯繁瑣又容易出 bug。直接用 Redisson,它處理了鎖續期(Watch Dog)與重入鎖這些細節。

持久化:RDB vs AOF

Redis 是記憶體資料庫,但可以把資料持久化到磁碟,重啟後恢復。

特性RDB(快照)AOF(日誌)
原理定時對記憶體做全量快照(fork 子進程)記錄每一條寫入指令到追加文件
資料安全性中(快照間隔期的資料遺失)高(appendfsync=always 近乎零遺失)
恢復速度快(直接載入二進位快照)慢(重播所有指令)
檔案大小小(壓縮二進位)大(所有指令文字,需定期 REWRITE)
效能影響低(fork 子進程,不影響主執行緒)中(每次寫都有 I/O)
推薦場景快速恢復、允許少量資料遺失金融資料、不能遺失任何記錄
生產環境建議:RDB + AOF 同時開啟。AOF 保資料安全,RDB 用於快速重啟恢復。Redis 重啟時會優先用 AOF 恢復(更完整)。

高可用架構

Redis Sentinel(哨兵)
1 主 N 從 + 多個哨兵進程。哨兵監控 Master,掛掉時自動投票選出新 Master(Failover)。適合垂直擴展——資料量不大、但需要高可用。
AWS ElastiCache 的「啟用多可用區」本質上就是這個模式。
Redis Cluster(叢集)
資料按 Hash Slot(共 16384 個)分散在多個 Master,每個 Master 各有 Slave。適合海量資料的水平擴展,最少 3 主 3 從。
Cluster 下跨 slot 的 MGET、Pipeline 要注意 key 的分槽策略。

必監控的關鍵指標

快取命中率 INFO stats → keyspace_hits / (hits + misses)
命中率低於 80% 代表快取設計有問題或 TTL 太短。生產環境應該維持在 90% 以上。
記憶體使用量 INFO memory → used_memory_human
超過 maxmemory 的 80% 就要告警,避免觸發淘汰策略把重要快取意外刪掉。
連線數 INFO clients → connected_clients
連線數異常增長通常是連線池配置不當或連線洩漏,會直接影響 Redis 回應速度。
慢查詢日誌 SLOWLOG GET 10
O(N) 的指令(KEYS *、超大 Hash 的 HGETALL)會阻塞單執行緒,用 SCAN 代替。

高頻面試題

四個核心原因:① In-Memory——資料在 RAM,比磁碟快 10 萬倍;② 單執行緒——避免鎖競爭和上下文切換;③ I/O 多路復用——用 epoll 監控數萬連接,單線程處理高並發;④ 精簡資料結構——String/Hash/List/Set/ZSet 操作大多是 O(1),不做複雜查詢。
單執行緒指的是命令處理是單線程,但 I/O 是用 epoll 多路復用處理的。Redis 用一個執行緒監控所有連接的 I/O 事件(哪個連接有資料可讀/寫),然後順序執行這些命令。由於每個命令都非常快(微秒級),單線程每秒也能處理 10 萬+ 請求。Redis 6.0 後,I/O 讀寫本身改用多線程,進一步提升吞吐量。
雪崩:大量 key 同時過期 → 流量全打 DB。解法:TTL 加隨機抖動、熱點 key 永不過期、多級快取。
穿透:查詢根本不存在的 key,每次都 Miss 打 DB。解法:快取空值(存 "NULL")、Bloom Filter 前置過濾。
擊穿:熱點 key 剛好過期瞬間大量並發湧入 DB。解法:熱點 key 永不過期、分散式鎖(只讓一個請求查 DB 重建快取)。
基本實作:SET lock:key uuid NX EX 30(NX=不存在才設,EX=自動過期防死鎖)。釋放時要用 Lua 腳本原子性地「比對 value 再 DEL」,確保只刪自己的鎖。
常見坑:① 鎖過期後業務還沒執行完(需要 Watch Dog 自動續期);② 主從切換時鎖資料來不及同步(RedLock 演算法解決,但有爭議);③ 忘記在 finally 釋放鎖。
建議直接用 Redisson,它處理了以上所有問題。
最常用的 Cache-Aside 模式下:更新 DB 後刪除快取(不是更新快取)。原因:① 更新快取可能失敗導致不一致;② 刪除後下次讀取 Miss 再重建,流程清晰。
更新順序:先更新 DB,再刪快取(而非先刪快取再更新 DB)。先刪後更新有一個窗口期:刪完快取到更新 DB 之間,如果有請求進來,會把舊資料重新存入快取,造成長期不一致。
終極方案:Canal 監聽 MySQL binlog,異步刪除快取,即使應用層更新失敗也能兜底。
① 冒號分層命名:業務:實體:id:欄位,如 order:summary:1001,方便管理和 SCAN 過濾;
② 不要用 KEYS * 掃描:O(N) 阻塞單執行緒,用 SCAN 0 MATCH order:* COUNT 100 代替;
③ Key 不要太長:Key 本身也佔記憶體,建議不超過 100 bytes;
④ 設定合理 TTL:沒有業務意義的快取不要永久保存,避免記憶體無限增長。