# 無鎖的原子操作:Redis 如何應對併發訪問? > 目的:在高併發場景下,確保對**同一份資料**的修改具備**互斥性**與**正確性**,避免庫存、計數等資料被錯誤覆寫。 --- ## 1) 背景與問題 - 常見場景:多用戶同時下單、同一商品庫存在 Redis 內被多個客戶端同時更新。 - **RMW(Read-Modify-Write)操作**: 1. 讀取資料(Read)→ 2. 本地修改(Modify)→ 3. 寫回(Write)。 - **臨界區(Critical Section)**:會同時訪問同一份資料的 RMW 代碼段,需保證互斥。 - 風險:沒有互斥時,多客戶端基於**相同初始值**修改,容易**覆蓋彼此結果**(如 10 → A減1=9,B也減1=9,最終仍是 9 而不是 8)。 --- ## 2) 兩大類解法總覽 ### A. 加鎖(Lock) - 思路:把並行變串行。一個客戶端持鎖時,其它客戶端等待。 - 優點:語義直觀、容易理解。 - 缺點:**降低並發性能**;分散式場景需**分散式鎖**(實作與運維較複雜)。 ### B. 原子操作(Atomic) - 思路:以**原子性**保證互斥執行,**無需顯式加鎖**。 - 兩種方式: 1. **單命令原子操作**(如 `INCR` / `DECR`)。 2. **Lua 腳本**(多步操作合併為一次原子執行)。 - 優點:對吞吐影響小於加鎖。 - 注意:Lua 腳本過長會**阻塞 Redis 單執行緒**,也會降低並發。 --- ## 3) 為什麼 Redis 命令具原子性? - Redis 使用**單執行緒**處理命令,單個命令在執行時不會被其他命令打斷,天然互斥。 - 例外:快照、AOF 重寫等在背景執行(讀為主),不修改資料,對並發控制影響小。 --- ## 4) 單命令原子操作:`INCR` / `DECR` - 把「讀 → 改 → 寫」三步合為**一個命令**。 - 典型:庫存扣減 ```bash DECR product_stock:{id} ``` - 適用:**純加減**、**計數器**等簡單 RMW。 --- ## 5) Lua 腳本原子操作 - 當 RMW 涉及**多條命令與判斷邏輯**(如:計數 + 判斷 + 設置過期),用 Lua 把多步**合併為一次**。 - Redis 以**整個腳本**為單位原子執行,不會被打斷。 ### 範例:IP 限流(每分鐘不超過 20 次) 需求: - 每次訪問 `INCR ip_key`; - **第一次**訪問時同時 `EXPIRE ip_key 60`; - 大於門檻則拒絕。 Lua(核心片段): ```lua local current = redis.call("incr", KEYS[1]) if tonumber(current) == 1 then redis.call("expire", KEYS[1], 60) end return current ``` - 說明:把 **INCR + 判斷是否首次 + 設置過期** 合為一次原子執行,避免「先增後設過期被搶跑」導致**永不過期**的問題。 --- ## 6) 解法對比 | 面向 | 加鎖 | 原子命令(INCR/DECR) | Lua 腳本 | |---|---|---|---| | 適用場景 | 任意臨界區 | 單純加減、計數器 | 多步複合邏輯(增減 + 判斷 + 過期…) | | 開發複雜度 | 中等(分散式鎖較高) | 低 | 中 | | 吞吐/延遲 | **降低並發** | 影響最小 | 視腳本長度而定(長腳本會阻塞) | | 正確性保障 | 高(用對即可) | 高(命令原子) | 高(腳本原子) | | 運維成本 | 可能需要外部鎖基礎設施 | 無 | 無 | --- ## 7) 小結 - 目標是讓臨界區**互斥且高效**: - **簡單加減 → 單命令原子**; - **多步一致性 → Lua 腳本**; - **跨系統事務/特定鎖語義 → 分散式鎖**(審慎使用)。 - 原則:**最小化臨界區**、**避免冗長腳本**、**優先使用 Redis 原子命令**。