傳統資料庫讀寫要到磁碟(毫秒級),Redis 把資料放在記憶體(奈秒~微秒級),快 100 倍以上。但它不是拿來取代資料庫的——它站在資料庫前面當快速通道,讓 90% 的請求根本不需要碰 DB。
epoll 監控數萬個連線的 I/O 狀態,所以單線程也能扛高並發。epoll 是什麼?看專頁 →INCR 是原子操作,可做點讚數、訪問量;INCR + TTL 就是 API Rate Limit。SET NX EX(或 Redisson)實現跨服務節點的互斥鎖,防止並發衝突。同一個請求,命中與否走的路完全不同——這是理解快取價值最直接的一張圖。
不是二選一,而是分工。
| 比較項目 | Redis | MySQL |
|---|---|---|
| 儲存位置 | 記憶體(In-Memory) | 磁碟(Disk) |
| 讀取速度 | 100μs 等級 | 1ms~10ms |
| 資料容量 | 受 RAM 限制(昂貴) | TB 等級(便宜) |
| 資料持久性 | 可選(RDB/AOF) | 強(ACID 事務) |
| 關聯查詢 | 不支援(只有 key 查詢) | 支援(JOIN/WHERE) |
| 資料結構 | String/Hash/List/Set/ZSet | 固定表格欄位 |
| 適用場景 | 熱點資料快取、Session、計數器 | 業務資料持久化、複雜查詢 |
選對資料結構,效能事半功倍。點上面的類型切換指令與使用場景。
最基本的類型。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秒後過期 每個 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 欄位
雙向鏈結串列。頭尾插入/移除都是 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秒
無重複、無序的集合。最強大的功能是集合運算:交集(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 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分的人
快取不是「存進去就完事」,策略決定了資料一致性與系統穩定性。
由應用程式自己負責讀寫 Redis。讀時先查 Redis,Miss 才查 DB,然後把結果存進 Redis。寫時直接寫 DB,同時刪除(而非更新)Redis 中的舊快取。
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。
🔧 解決方案:
baseTime + random(0, 300),讓過期時間分散TTL = -1,由後台異步刷新,不讓快取真的過期問題:查詢一個根本不存在的資料(如惡意攻擊者用 id=-1 查詢),Redis 查不到就問 DB,DB 也查不到,但每次請求都會穿透到 DB。大量這樣的請求可以讓 DB 崩潰。
🔧 解決方案:
"NULL"(TTL 設短一點,如 60 秒),讓後續請求不再打到 DB問題:某個超熱門的 key(如促銷商品詳情)TTL 到期的瞬間,大量並發請求全部 Cache Miss,同時湧入 DB 重建快取,形成瞬間的 DB 高負載(比雪崩更集中)。
🔧 解決方案:
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'
spring:
data:
redis:
host: localhost
port: 6379
password: your-password # 如有設定
timeout: 2000 # 連線超時 2s
lettuce:
pool:
max-active: 20 # 最大連線數
max-idle: 10
min-idle: 2 @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;
} @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);
}
} 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 適合更新後主動刷新。三個可以搭配使用。
Projection 本身不是 Redis 的功能,而是 CQRS + Event Sourcing 架構裡的概念。Domain Event 發生時,事件處理器會主動更新 Read Model,而這個讀模型通常就存在 Redis。
OrderCreated → OrderPaid → OrderShipped…)。要知道現在的狀態,就把事件依序重播算出來。
// 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)
);
}
} 多個服務節點同時操作同一資源時(扣庫存、初始化快取),需要分散式鎖確保互斥。
@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 釋放
}
}
} Redis 是記憶體資料庫,但可以把資料持久化到磁碟,重啟後恢復。
| 特性 | RDB(快照) | AOF(日誌) |
|---|---|---|
| 原理 | 定時對記憶體做全量快照(fork 子進程) | 記錄每一條寫入指令到追加文件 |
| 資料安全性 | 中(快照間隔期的資料遺失) | 高(appendfsync=always 近乎零遺失) |
| 恢復速度 | 快(直接載入二進位快照) | 慢(重播所有指令) |
| 檔案大小 | 小(壓縮二進位) | 大(所有指令文字,需定期 REWRITE) |
| 效能影響 | 低(fork 子進程,不影響主執行緒) | 中(每次寫都有 I/O) |
| 推薦場景 | 快速恢復、允許少量資料遺失 | 金融資料、不能遺失任何記錄 |
KEYS *、超大 Hash 的 HGETALL)會阻塞單執行緒,用 SCAN 代替。SET lock:key uuid NX EX 30(NX=不存在才設,EX=自動過期防死鎖)。釋放時要用 Lua 腳本原子性地「比對 value 再 DEL」,確保只刪自己的鎖。業務:實體:id:欄位,如 order:summary:1001,方便管理和 SCAN 過濾;SCAN 0 MATCH order:* COUNT 100 代替;