---
# System prepended metadata

title: 無鎖的原子操作：Redis 如何應對併發訪問？

---

# 無鎖的原子操作：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 原子命令**。