你知道 Redis 使用的什么協議嗎?
有個小伙伴面試回來說面試官問了他一些 Redis 問題,但是他好像沒有回答上來。
我說,你 Redis 不是用的很溜嗎,什么問題難住你了。
他說,事情是這樣的,剛開始,問了一些基礎的問題,比如 Redis 的幾種基本數據類型和使用場景,以及主從復制和集群的一些問題,這些都還好。
然后問 Redis 的兩種持久化方式,他說與 RDB 和 AOF 兩種方式,RDB 數據文件小,恢復速度快,但是對性能有影響,而且不適合實時存儲。而 AOF 是現在最常用的持久化方式,它的一大優(yōu)點就是實時性,并且對 Redis 半身性能影響最小。
那面試又問了,你知道 AOF 持久化之后的文件是什么格式嗎?
答:好像就是文本文件吧?
好,文本文件,那你知道它有什么規(guī)則嗎?或者說,它和 Redis 的協議有什么關系嗎?
答:啊,這個,恩,不太清楚呢。
現在就來看一下 AOF 和 RESP 協議的關系
- 從兩種持久化方式說起。
- RESP 協議是什么
- 動手實現一個簡單的協議解析命令行工具
先從持久化說起,雖然一提到 Redis,首先想到的就是緩存,但是 Redis 不僅僅是緩存這么簡單,它的定位是內存型數據庫,可以存儲多種類型的數據結構,還可以當做簡單消息隊列使用。既然是數據庫,持久化功能是必不可少的。
Redis 的兩種持久化方式
Redis 提供了兩種持久化方式,一種是 RDB 方式,另外一種是 AOF 方式,AOF 是目前比較流行的持久化方案。
RDB 方式
RDB持久化是通過快照的方式,在指定的時間間隔內將內存中的數據集快照寫入磁盤。它以一種緊湊壓縮的二進制文件的形式出現??梢詫⒖煺諒椭频狡渌掌饕詣?chuàng)建相同數據的服務器副本,或者在重啟服務器后恢復數據。RDB是Redis默認的持久化方式,也是早期版本的必須方案。
RDB 由下面幾個參數控制。
- # 設置 dump 的文件名
- dbfilename dump.rdb
- # 持久化文件的存儲目錄
- dir ./
- # 900秒內,如果至少有1個key發(fā)生變化,就會自動觸發(fā)bgsave命令創(chuàng)建快照
- save 900 1
- # 300秒內,如果至少有10個key發(fā)生變化,就會自動觸發(fā)bgsave命令創(chuàng)建快照
- save 300 10
- # 60秒內,如果至少有10000個key發(fā)生變化,就會自動觸發(fā)bgsave命令創(chuàng)建快照
- save 60 10000
持久化流程
上面說到了配置文件中的幾個觸發(fā)持久化的機制,比如 900 秒、300秒、60秒,當然也可以手動執(zhí)行命令 save或bgsave進行觸發(fā)。bgsave是非阻塞版本,通過 fork 出子進程的方式來進行快照生成,而 save會阻塞主進程,不建議使用。
1、首先 bgsave命令觸發(fā);
2、父進程 fork 出一個子進程,這一步是比較重量級的操作,也是 RDB 方式性能不及 AOF 的一個重要原因;
3、父進程 fork 出子進程后就可以正常的相應客戶端發(fā)來的其他命令了;
4、子進程開始進行持久化工作,對現有數據進行完整的快照存儲;
5、子進程完成操作后,通知父進程;
RDB的優(yōu)點:
- RDB是一個緊湊壓縮的二進制文件,代表Redis在某個時間點上的數據 快照。非常適用于備份,全量復制等場景。比如每6小時執(zhí)行bgsave備份, 并把RDB文件拷貝到遠程機器或者文件系統中(如hdfs),用于災難恢復。
- Redis加載RDB恢復數據遠遠快于AOF的方式。
RDB的缺點:
- RDB方式數據沒辦法做到實時持久化/秒級持久化。因為bgsave每次運 行都要執(zhí)行fork操作創(chuàng)建子進程,屬于重量級操作,頻繁執(zhí)行成本過高。
- RDB文件使用特定二進制格式保存,Redis版本演進過程中有多個格式 的RDB版本,存在老版本Redis服務無法兼容新版RDB格式的問題。
AOF 方式
AOF 由下面幾個參數控制。
- # appendonly參數開啟AOF持久化
- appendonly yes
- # AOF持久化的文件名,默認是appendonly.aof
- appendfilename "appendonly.aof"
- # AOF文件的保存位置和RDB文件的位置相同,都是通過dir參數設置的
- dir ./
- # 同步策略
- # appendfsync always
- appendfsync everysec
- # appendfsync no
- # aof重寫期間是否同步
- no-appendfsync-on-rewrite no
- # 重寫觸發(fā)配置
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
- # 加載aof出錯如何處理
- aof-load-truncated yes
- # 文件重寫策略
- aof-rewrite-incremental-fsync yes
針對RDB不適合實時持久化的問題,Redis提供了AOF 持久化方式來解決,AOF 也是目前最流程的持久化方式。
AOF(append only file),以獨立日志的方式記錄每次寫命令, 重啟時再重新執(zhí)行AOF文件中的命令達到恢復數據的目的。
1、所有的寫入命令會追加到aof_buf(緩沖區(qū))中;
2、AOF緩沖區(qū)根據對應的策略向硬盤做同步操作;
3、隨著AOF文件越來越大,需要定期對AOF文件進行重寫,達到壓縮的目的;
4、當Redis服務器重啟時,可以加載AOF文件進行數據恢復;
AOF 文件里存的是什么
我在本地的測試 redis 環(huán)境中隨便刷了幾條命令,然后打開 appendonly.aof 文件查看,發(fā)現里面的內容像下面這樣子。
RESP 協議Redis客戶端與服務端通信,使用 RESP 協議通信,該協議是專門為 Redis 設計的通信協議,但也可以用于其它客戶端-服務器通信的場景。
RESP 協議
有如下幾個特點:
- 實現簡單;
- 快速解析;
- 可閱讀;
客戶端發(fā)送命令給服務端,服務端拿到命令后進行解析,然后執(zhí)行對應的邏輯,之后返回給客戶端,當然了,這一發(fā)一回復都是用的 RESP 協議特點的格式。
一般情況下我們會使用 redis-cli或者一些客戶端工具連接 Redis 服務端。
- ./redis-cli
然后整個交互過程的命令發(fā)送和返回結果像下面這樣,綠色部分為發(fā)送的命令,紅色部分為返回的結果。
這就是我們再熟悉不過的部分了。但是,這并不能看出 RESP 協議的真實面貌。
用 telnet 試試
RESP 是基于 TCP 協議實現的,所以除了用各種客戶端工具以及 Redis 提供的 redis-cli工具,還可以用 telnet 查看,用 telnet 就可以看出 RESP 返回的原始數據格式了。
我本地的 Redis 是用的默認 6379 端口,并且沒有設置 requirepass ,我們來試一下用 telnet 連接。
- telnet 127.0.0.1 6379
然后執(zhí)行與前面相同的幾條命令,發(fā)送和返回的結果如下,綠色部分為發(fā)送的命令,紅色為返回的結果。
怎么樣,有些命令的返回還好,但是像get str:hello這條,返回的結果除了 world值本身,上面還多了一行 $5,是不是有點迷糊了。
協議規(guī)則
請求命令
一條客戶端發(fā)往服務器的命令的規(guī)則如下:
- *<參數數量> CR LF
- $<參數 1 的字節(jié)數量> CR LF
- <參數 1 的數據> CR LF
- ...
- $<參數 N 的字節(jié)數量> CR LF
- <參數 N 的數據> CR LF
RESP 用\r\n作為分隔符,會表明此條命令的具體參數個數,在命令上看來,空格分隔的都表示一個參數,例如 set str:hello world 這條命令就是3個參數,會表明每個參數的字符數和具體內容。
用這條命令舉例,對應到 RESP 協議規(guī)則上就會變成下面這個樣子:
- *3\r\n$3\r\nset\r\n$9str:hello\r\n$5world\r\n
服務端回復
Redis 命令會返回多種不同類型的回復。
通過檢查服務器發(fā)回數據的第一個字節(jié), 可以確定這個回復是什么類型:
1、狀態(tài)回復(status reply)的第一個字節(jié)是 "+"
比如 ping命令的回復,+PONG\r\n
2、錯誤回復(error reply)的第一個字節(jié)是 "-"
比如輸入一個 redis 中不存在的命令,或者給某些命令設置錯誤的參數,例如輸入auth,auth 命令后面需要有一個密碼參數的,如果不輸入就會返回錯誤回復類型。
-ERR wrong number of arguments for 'auth' command\r\n
3、整數回復(integer reply)的第一個字節(jié)是 ":"
例如 INCR、DECR 自增自減命令,返回的結果是這樣的 :2\r\n
4、批量回復(bulk reply)的第一個字節(jié)是 "$"
例如對 string 類型執(zhí)行 get 操作,$5\r\nworld\r\n,$后面的數字 5 表示返回的結果有 5 個字符,后面是返回結果的實際內容。
5、多條批量回復(multi bulk reply)的第一個字節(jié)是 "*"
例如 LRANGE key start stop或者 hgetall等返回多條結果的命令,比如 lrange命令返回的結果:
- *2\r\n$6\r\nnews-2\r\n$6\r\nnews-1\r\n
多條批量回復和前面說的客戶端發(fā)送命令的格式是一致的。
實現一個簡單的 Redis 交互工具
了解了 Redis 的協議規(guī)則,我們就可以自己寫一個簡單的客戶端了。當然,通過官網我們可以看到已經有各種語言,而且每種語言有不止一個客戶端工具了。
比如 Java 語言的客戶端就有這么多種,其中 Jedis 應該是用的最多了,既然已經有這么好用的輪子了,當然沒必要重復造輪子,主要還是為了加深印象。
RESP 協議基于 TCP 協議,可以使用 socket 方式進行連接。
- public Socket createSocket() throws IOException {
- Socket socket = null;
- try {
- socket = new Socket();
- socket.setReuseAddress(true);
- socket.setKeepAlive(true);
- socket.setTcpNoDelay(true);
- socket.setSoLinger(true, 0);
- socket.connect(new InetSocketAddress(host, port), DEFAULT_TIMEOUT);
- socket.setSoTimeout(DEFAULT_TIMEOUT);
- outputStream = socket.getOutputStream();
- inputStream = socket.getInputStream();
- return socket;
- } catch (Exception ex) {
- if (socket != null) {
- socket.close();
- }
- throw ex;
- }
- }
然后剩下的就是對返回的結果進行字符串的解析了,我做的工具就到簡陋的到這一步了,下面是一些簡單命令的返回輸出。
代碼已放到 github 上,有興趣的可以 clone 下來看一下。
https://github.com/huzhicheng/medis
本文轉載自微信公眾號「 古時的風箏」,可以通過以下二維碼關注。轉載本文請聯系 古時的風箏公眾號。