一、Redis 協議
1.1 RESP
RESP 是 Redis 客戶端與服務器之間的通信協議,采用文本格式(基于 ASCII 字符),支持多種數據類型的序列化和反序列化
RESP 通過首字符區分數據類型,主要支持 5 種類型:
類型 | 首字符 | 格式示例 | 說明 |
---|---|---|---|
簡單字符串 | + | +OK\r\n | 以 \r\n 結尾,用于返回狀態信息(如 OK) |
錯誤信息 | - | -ERR wrong type\r\n | 格式同簡單字符串,但表示錯誤 |
整數 | : | :10086\r\n | 用于返回計數、自增結果等整數 |
批量字符串 | $ | $5\r\nhello\r\n | 用于存儲二進制安全的字符串(長度 + 內容) |
數組(列表) | * | *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n | 用于表示多個元素的集合(如命令參數) |
示例解析:
客戶端發送命令 SET name redis
時,協議格式為:
`*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nredis\r\n`
*3
表示數組包含 3 個元素(命令 + 2 個參數)$3\r\nSET
表示第一個元素是長度為 3 的字符串 “SET”$4\r\nname
表示第二個元素是長度為4的字符串name
$5\r\nredis
表示第三個元素是長度為5的字符串redis
\r\n
是最后的結束分隔符
1.2 Redis Pipeline
Redis Pipeline(管道)是 Redis 客戶端提供的一種優化網絡通信的機制,允許客戶端一次性發送多個命令到服務器,再批量接收所有命令的響應,從而大幅減少網絡往返次數,提升通信效率
- 傳統模式:客戶端發送一個命令 → 等待服務器響應 → 再發送下一個命令(每命令 1 次網絡往返)。
- Pipeline 模式:客戶端一次性發送多個命令 → 服務器批量執行 → 一次性返回所有結果(N 個命令僅 1 次網絡往返)。
Pipeline 特點
-
非原子性:Pipeline 不保證事務性,命令按順序執行,但中間若某命令失敗,后續命令仍會繼續執行(與
MULTI/EXEC
事務不同)。 -
順序性:服務器按接收順序執行 Pipeline 中的命令,響應結果也與命令順序一一對應。
-
適用場景:
- 批量讀寫操作(如批量設置多個鍵值對)。
- 非依賴型命令(命令之間無因果關系,不需要前一個命令的結果作為后一個的參數)。
1.3 Redis 事務
Redis 事務是一組命令的集合,通過 MULTI
、EXEC
等命令將多個操作封裝為一個不可分割的工作單元,要么全部執行,要么全部不執行(特殊情況除外,見后文說明)。它主要用于保證一系列操作的原子性,避免中間被其他命令干擾
Redis 事務通過以下命令實現完整流程:
命令 | 作用 |
---|---|
MULTI | 開啟事務,后續命令進入 “隊列” 等待執行,而非立即執行 |
EXEC | 執行事務隊列中的所有命令,返回各命令的結果(按入隊順序) |
DISCARD | 取消事務,清空隊列,放棄執行 |
WATCH | 監控一個或多個鍵,若事務執行前被監控的鍵發生變動,則事務被打斷(樂觀鎖) |
Redis 事務的特點
-
原子性限制:
- 若事務中命令存在語法錯誤(如命令不存在),
EXEC
會直接放棄所有命令(全部不執行)。 - 若命令語法正確但運行時錯誤(如對字符串執行
LPOP
),錯誤命令會失敗,其他命令仍會執行,不回滾 ,這與傳統數據庫事務的 “完全回滾” 不同,Redis 不支持部分失敗后的回滾,需業務層處理。
- 若事務中命令存在語法錯誤(如命令不存在),
-
順序性:事務中的命令按入隊順序執行,不會被其他客戶端的命令插入。
-
樂觀鎖機制:通過
WATCH
實現,適用于 “讀 - 改 - 寫” 場景,防止并發修改導致的數據不一致。
基礎命令
MULTI
+ EXEC
執行事務
# 開啟事務
127.0.0.1:6379> MULTI
OK# 命令入隊(此時僅排隊,不執行)
127.0.0.1:6379(TX)> SET name "redis"
QUEUED
127.0.0.1:6379(TX)> GET name
QUEUED
127.0.0.1:6379(TX)> INCR counter
QUEUED# 執行事務(返回所有命令結果,按入隊順序)
127.0.0.1:6379(TX)> EXEC
1) OK
2) "redis"
3) (integer) 1
DISCARD
取消事務
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET a 10
QUEUED
127.0.0.1:6379(TX)> SET b 20
QUEUED# 取消事務,隊列清空
127.0.0.1:6379(TX)> DISCARD
OK# 驗證命令未執行
127.0.0.1:6379> GET a
(nil)
WATCH
監控鍵
兩個客戶端同時更新同一個鍵,確保只有先獲取到原始值的客戶端能成功更新
客戶端A:
# 監控鍵 balance
127.0.0.1:6379> WATCH balance
OK# 讀取當前值
127.0.0.1:6379> GET balance
"100"# 開啟事務,準備更新(此時客戶端 B 還未操作)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET balance 200
QUEUED
客戶端B:
# 修改被監控的鍵 balance
127.0.0.1:6379> SET balance 150
OK
客戶端A繼續執行:
# 由于 balance 被 B 修改,事務被打斷(返回 nil)
127.0.0.1:6379(TX)> EXEC
(nil)# 驗證結果(A 的修改未生效)
127.0.0.1:6379> GET balance
"150"
WATCH
會在 EXEC
前檢查監控的鍵是否被修改,若被修改則事務失敗(返回 nil
),需業務層重試
應用場景
1. 實現 ZPOP
(原子性移除有序集合首個元素)
Redis 沒有內置 ZPOP
命令,可用事務實現 “獲取首個元素并刪除” 的原子操作:
# 監控有序集合 zset,防止被其他客戶端修改
127.0.0.1:6379> WATCH zset
OK# 獲取首個元素(分數最低的)
127.0.0.1:6379> ZRANGE zset 0 0
1) "member1"# 開啟事務,刪除該元素
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> ZREM zset "member1"
QUEUED# 執行事務(若 zset 未被修改,返回 1 表示刪除成功)
127.0.0.1:6379(TX)> EXEC
1) (integer) 1
2. 實現值的原子加倍操作
對一個鍵的值進行加倍,確保操作過程中不被其他客戶端干擾:
# 監控鍵 score:10001
127.0.0.1:6379> WATCH score:10001
OK# 讀取當前值
127.0.0.1:6379> GET score:10001
"5"# 開啟事務,設置新值(5*2=10)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET score:10001 10
QUEUED# 執行事務(成功返回 OK)
127.0.0.1:6379(TX)> EXEC
1) OK
Lua腳本
Redis 中,Lua 腳本的執行也是原子性的(執行期間不會被其他命令打斷),且功能更強大,兩者的區別如下:
特性 | 事務(MULTI/EXEC ) | Lua 腳本(EVAL /EVALSHA ) |
---|---|---|
原子性 | 保證命令按順序執行,部分錯誤不回滾 | 腳本內所有操作作為整體原子執行,支持復雜邏輯 |
靈活性 | 僅支持簡單命令隊列,不支持條件判斷 | 支持分支、循環等邏輯,可實現復雜原子操作 |
網絡開銷 | 需多次交互(MULTI →入隊→EXEC ) | 一次請求即可,減少網絡往返 |
適用場景 | 簡單批量操作,依賴樂觀鎖(WATCH )的場景 | 復雜原子操作(如帶條件的更新、多鍵聯動) |
示例:用 Lua 腳本實現原子加倍操作 |
EVAL "local val = tonumber(redis.call('GET', KEYS[1])); redis.call('SET', KEYS[1], val*2); return val*2" 1 score:10001
事務與Pipeline 對比
特性 | Pipeline | MULTI/EXEC 事務 |
---|---|---|
網絡優化 | 減少網絡往返(核心目的) | 無(仍需多次往返) |
原子性 | 無(命令逐個執行) | 有(所有命令要么全執行,要么全不執行) |
命令依賴 | 不支持(命令無因果關系) | 支持(可基于前序命令結果) |
適用場景 | 批量非依賴型命令 | 需保證原子性的操作 |
1.4 Redis ACID
ACID 是數據庫事務的四大特性(原子性、一致性、隔離性、持久性),Redis 作為內存數據庫,對這些特性的支持與傳統關系型數據庫有顯著差異
1. 原子性(Atomicity)
定義:事務中的操作要么全部成功,要么全部失敗,不允許部分執行。
Redis 的支持情況:
- 不完整支持:Redis 事務通過
MULTI/EXEC
將命令入隊,EXEC
時批量執行,但不支持回滾。- 若事務中存在語法錯誤(如命令不存在),
EXEC
會直接放棄所有命令(全部不執行)。 - 若命令語法正確但運行時錯誤(如對字符串執行
LPOP
),錯誤命令會失敗,其他命令仍會繼續執行(不會回滾)
- 若事務中存在語法錯誤(如命令不存在),
示例:
key1
被成功設置為 “hello”,錯誤命令不影響其他命令執行,違反原子性。
# 開啟事務
127.0.0.1:6379> MULTI
OK# 正確命令:設置 key1
127.0.0.1:6379(TX)> SET key1 "hello"
QUEUED# 運行時錯誤:對字符串執行 LPOP(列表操作)
127.0.0.1:6379(TX)> LPOP key1
QUEUED# 執行事務
127.0.0.1:6379(TX)> EXEC
1) OK # 第一個命令成功
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value # 第二個命令失敗
2. 一致性(Consistency)
定義:事務執行前后,數據需滿足預設的約束(如業務規則),保持邏輯一致。
Redis 的支持情況:
- 有限支持:僅保證數據結構層面的一致性(如字符串不會被改造成列表),但不保證業務邏輯一致性。
- 若事務中部分命令失敗,可能導致業務數據不一致(如轉賬時 “扣錢失敗但加錢成功”)。
示例:模擬轉賬場景(A 向 B 轉 100 元)
# 初始狀態
127.0.0.1:6379> SET A 500
OK
127.0.0.1:6379> SET B 300
OK# 開啟事務(假設 A 扣錢命令出錯)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY A 100 # 正確命令:A 扣 100
QUEUED
127.0.0.1:6379(TX)> INCRBY B 100 # 正確命令:B 加 100
QUEUED
127.0.0.1:6379(TX)> INCRBY A abc # 運行時錯誤:參數不是數字
QUEUED# 執行事務
127.0.0.1:6379(TX)> EXEC
1) (integer) 400 # A 扣錢成功
2) (integer) 400 # B 加錢成功
3) (error) ERR value is not an integer or out of range # 第三個命令失敗# 最終狀態:A=400,B=400(總金額 800,初始總金額 800,數據結構一致)
# 但業務上:A 扣了 100,B 加了 100,看似正確?若第一個命令是錯誤(如 DECRBY A abc):
# 則 A 不變,B 加 100,總金額增加 100,業務邏輯不一致。
3. 隔離性(Isolation)
定義:多個事務并發執行時,彼此的操作互不干擾,結果等同于串行執行。
Redis 的支持情況:
- 完全支持:Redis 是單線程模型,所有命令(包括事務)按順序執行,不存在并發沖突,天然滿足隔離性。
示例:兩個客戶端并發執行事務
-
客戶端 1 執行事務:
SET x 10; INCR x
-
客戶端 2 執行事務:
SET x 20; INCR x
-
結果:無論執行順序如何,最終
x
要么是 11(客戶端 1 先執行),要么是 21(客戶端 2 先執行),不會出現中間狀態。
4. 持久性(Durability)
定義:事務一旦提交,結果需永久保存(即使服務器崩潰)。
Redis 的支持情況:
- 條件支持:依賴持久化配置,默認不保證持久性。
- 若使用 AOF 持久化 且配置
appendfsync=always
,事務執行后會立即寫入磁盤,保證持久性(但性能極差)。 - 若使用 RDB 或默認 AOF 配置(
everysec
或no
),事務結果可能因崩潰丟失。
- 若使用 AOF 持久化 且配置
實際場景:生產環境極少使用 appendfsync=always
,因此 Redis 事務通常不滿足持久性。
總結:Redis 事務與 ACID
特性 | 支持情況 |
---|---|
原子性 | 不支持(無回滾,部分命令失敗不影響其他命令) |
一致性 | 僅保證數據結構一致,不保證業務邏輯一致 |
隔離性 | 完全支持(單線程執行) |
持久性 | 僅在特定 AOF 配置下支持,實際場景中幾乎不滿足 |
補充:Lua 腳本的 ACID 表現
- Lua 腳本執行是原子性的(全程無中斷),且滿足隔離性(單線程),但一致性和持久性仍與上述相同。
1.5 Redis 發布訂閱
Redis 發布訂閱是一種消息通信模式,支持 “一對多” 消息分發(多播),適用于簡單的消息通知場景。其核心是 “頻道(Channel)”:發布者向頻道發送消息,訂閱者從頻道接收消息。
基礎命令
命令 | 作用 | 示例 |
---|---|---|
SUBSCRIBE | 訂閱一個或多個頻道 | SUBSCRIBE news sport |
PSUBSCRIBE | 訂閱符合模式的頻道(支持 * 通配符) | PSUBSCRIBE news.* (匹配 news.tech 等) |
PUBLISH | 向頻道發布消息 | PUBLISH news "Redis 發布訂閱示例" |
UNSUBSCRIBE | 取消訂閱頻道 | UNSUBSCRIBE news |
PUNSUBSCRIBE | 取消訂閱模式頻道 | PUNSUBSCRIBE news.* |
應用場景
發布訂閱
場景:客戶端 A 訂閱 news
頻道,客戶端 B 向 news
發布消息。
客戶端 A(訂閱者):
# 訂閱 news 頻道
127.0.0.1:6379> SUBSCRIBE news
Reading messages... (press Ctrl-C to quit)
1) "subscribe" # 訂閱成功的反饋
2) "news"
3) (integer) 1# 收到客戶端 B 發布的消息
1) "message" # 消息類型
2) "news" # 頻道
3) "Redis 發布訂閱示例" # 消息內容
客戶端 B(發布者):
# 向 news 頻道發布消息
127.0.0.1:6379> PUBLISH news "Redis 發布訂閱示例"
(integer) 1 # 表示有 1 個訂閱者接收成功s
模式訂閱
場景:客戶端 C 訂閱 news.*
模式(匹配 news.tech
、news.sport
等頻道)。
客戶端 A(訂閱者):
127.0.0.1:6379> PSUBSCRIBE news.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "news.*"
3) (integer) 1# 收到向 news.tech 發布的消息
1) "pmessage" # 模式消息類型
2) "news.*" # 訂閱的模式
3) "news.tech" # 實際頻道
4) "AI 技術新突破" # 消息內容
客戶端 B(發布者):
127.0.0.1:6379> PUBLISH news.tech "AI 技術新突破"
(integer) 1 # 客戶端 A 收到
總結
缺點與局限性
- 無消息持久化:Redis 不會存儲發布的消息,若訂閱者離線,期間的消息會永久丟失。
- 無確認機制:發布者無法知道消息是否被訂閱者接收。
- 服務器重啟丟失:Redis 重啟后,所有訂閱關系和未傳遞的消息會被清空。
- 單獨連接:訂閱操作會阻塞連接(等待消息推送),需與普通命令連接分離(單獨開連接處理訂閱)。
適用場景
適用于實時通知、日志廣播等對消息可靠性要求不高的場景(如聊天室、實時監控告警)。若需保證消息可達性,建議使用 Redis Stream 或專業消息隊列(如 Kafka、RabbitMQ)。
1.6 Redis IO多線程
Redis 在 6.0 版本中引入了 IO 多線程 特性,主要用于優化網絡 IO 操作的性能,解決傳統單線程模型在高并發場景下的網絡瓶頸。但需要注意的是,Redis 的核心命令執行仍然是單線程的,IO 多線程僅負責 網絡數據的讀寫(接收客戶端請求和發送響應結果)。
可以修改配置文件開啟IO
多線程
# 開啟 IO 多線程(默認 no)
io-threads-do-reads yes# 設置 IO 線程數量(建議為 CPU 核心數的 1/2 或 1/4,避免線程切換開銷)
# 注意:總線程數 = 配置數 + 1(主線程),如配置 4 則共 5 個線程
io-threads 4
為什么要使用IO多線程
Redis IO 多線程的核心設計約束是:僅將 “網絡數據讀寫” 和 “協議解析” 拆分到多線程,而命令的執行、內存操作等核心邏輯仍由主線程單線程處理。這一原則確保了:
- 避免多線程競爭數據(無需復雜鎖機制),保留 Redis 單線程的簡單性和安全性;
- 僅優化最耗時的網絡 IO 環節(在高并發場景下,網絡讀寫可能占總耗時的 60% 以上)。
IO多線程流程概述
Redis 的 IO 多線程采用 “主線程 + 多 IO 線程” 的混合模型,核心流程如下:
-
接收請求階段:
- 主線程監聽客戶端連接,當有新請求到達時,將連接分配給 IO 線程。
- 多個 IO 線程并行讀取客戶端發送的命令數據(解析成 Redis 協議格式),并暫存到隊列中。
-
命令執行階段:
- 主線程從隊列中取出所有解析好的命令,按順序執行(保持單線程特性,保證命令的原子性和隔離性)。
-
發送響應階段:
- 主線程將命令執行結果分發給 IO 線程。
- 多個 IO 線程并行將結果發送回客戶端。
IO多線程的實現
Redis 通過以下關鍵結構實現 IO 多線程的管理和協作:
1. IO 線程結構體(io_thread_data
)
每個 IO 線程對應一個結構體,存儲線程狀態、任務隊列等信息
typedef struct {pthread_t thread; // 線程 IDint fd; // 用于線程間通知的管道(pipe)寫端redisAtomic size_t pending; // 待處理的任務數(原子變量,避免鎖)list *clients; // 分配給該線程的客戶端連接列表redisAtomic int state; // 線程狀態:IO_THREAD_STATE_IDLE(空閑)/ RUNNING
} io_thread_data;
fd
:主線程通過管道向 IO 線程發送 “有任務待處理” 的通知;clients
:該線程負責處理的客戶端連接隊列;state
:標記線程是否在工作,用于主線程判斷是否可以分配新任務。
2. 全局 IO 線程管理器
Redis 用全局變量管理所有 IO 線程:
// 全局 IO 線程數組
static io_thread_data *io_threads;
// IO 線程數量(配置文件中的 io-threads 值)
static int io_threads_num;
// 是否開啟 IO 多線程讀(配置 io-threads-do-reads yes)
static int io_threads_do_reads = 0;
IO多線程詳細過程
IO 多線程的工作流程可分為初始化、接收請求、命令執行、發送響應四個階段,主線程與 IO 線程通過 “任務分配 - 通知 - 處理 - 同步” 的方式協作。
1. 初始化階段(服務器啟動時)
-
步驟 1:讀取配置文件的
io-threads
和io-threads-do-reads
參數,確定是否開啟 IO 多線程及線程數量(io_threads_num
)。 -
步驟 2:創建
io_threads_num
個 IO 線程,初始化每個線程的管道(用于主線程通知)和狀態(IO_THREAD_STATE_IDLE
)。 -
步驟 3:為每個 IO 線程啟動工作函數(
IOThreadMain
),線程進入循環等待狀態(通過管道監聽主線程的任務通知)。
IO 線程的主循環邏輯(IOThreadMain
):
void *IOThreadMain(void *myid) {int id = *(int*)myid;while(1) {// 等待主線程通過管道發送通知(阻塞)if (aeWait(io_threads[id].fd, AE_READABLE, -1) <= 0)continue;// 讀取管道數據(僅用于喚醒,數據無實際意義)char buf[1];read(io_threads[id].fd, buf, 1);// 處理分配給自己的客戶端任務(讀/寫數據)if (io_threads_do_reads) {processPendingReads(id); // 處理讀任務(解析請求)} else {processPendingWrites(id); // 處理寫任務(發送響應)}// 標記線程為空閑狀態io_threads[id].state = IO_THREAD_STATE_IDLE;}
}
2. 接收請求階段(客戶端發送命令)
當客戶端發送命令時,主線程與 IO 線程協作完成 “讀取數據 + 解析協議”:
- 步驟 1:主線程通過事件循環(
aeMain
)檢測到客戶端套接字可讀,收集所有待讀取的客戶端連接。 - 步驟 2:主線程將客戶端連接平均分配給各個 IO 線程(避免某一線程負載過高),并將連接添加到對應線程的
clients
列表。 - 步驟 3:主線程通過管道向每個 IO 線程發送一個字節的通知(喚醒線程),并標記線程狀態為 “運行中”。
- 步驟 4:IO 線程被喚醒后,執行
processPendingReads
函數:- 循環讀取
clients
列表中每個客戶端的網絡數據; - 解析數據為 Redis 協議格式(如將
*3\r\n$3\r\nSET...
解析為命令和參數); - 解析完成后,將客戶端標記為 “待執行” 狀態,等待主線程處理。
- 循環讀取
- 步驟 5:主線程等待所有 IO 線程完成讀任務(通過輪詢線程狀態,直到所有線程回到
IDLE
),然后進入命令執行階段。
3. 命令執行階段(主線程單線程處理)
IO 線程完成請求解析后,主線程接管后續流程:
- 主線程遍歷所有 “待執行” 的客戶端,按順序執行解析后的命令(如
GET
、SET
等); - 命令執行過程中,主線程獨占數據訪問權(無多線程競爭),保證原子性和隔離性;
- 執行結果暫存在客戶端的響應緩沖區中,等待發送。
4. 發送響應階段(IO 線程并行發送)
命令執行完成后,主線程與 IO 線程協作將結果返回給客戶端:
- 步驟 1:主線程收集所有待發送響應的客戶端連接,再次平均分配給各個 IO 線程。
- 步驟 2:主線程通過管道通知 IO 線程處理寫任務,標記線程狀態為 “運行中”。
- 步驟 3:IO 線程被喚醒后,執行
processPendingWrites
函數:- 循環將客戶端響應緩沖區中的數據寫入套接字(發送給客戶端);
- 若數據發送完畢,清理客戶端狀態;若未發送完畢(如數據量大),則下次繼續發送。
- 步驟 4:主線程等待所有 IO 線程完成寫任務,然后進入下一輪事件循環。處理)
更多資料:https://github.com/0voice