背景
公司一年的部分業務數據放在redis服務器上,但數據量比較大,單純的string類型數據一年就將近32G,而且是經過壓縮后的。
所以我在想能否通過獲取string數據的時間改為保存list數據類型,或者將數據持久化到硬盤上,或者放在不同庫上,解決未來數據過大導致down機的問題。
相關知識點
- string數據類型
- 數據持久化
- 數據加載
Redis的字符串(string)的實現原理
Redis是由C語言編寫的,以高效和輕量著稱。
比如一個簡單的字符串”hello world”,其實是一個如下的字符的數組:
[‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘ ‘, ‘w’, ‘o’, ‘r’, ‘l’, ‘d’, ‘\0’]
最后的一個’\0’是空字符,表示字符串的結尾。
Redis由於各種原因,並沒有直接使用了C語言的字符串結構,而是對其做了一些封裝,得到了自己的簡單動態字符串(simple dynamic string, SDS)的抽象類型。
Redis中,默認以SDS作為自己的字符串表示。只有在一些字符串不可能出現變化的地方使用C字符串。
SDS定義中有三個參數:
buf是一塊可用的內存空間,通常大小會大於等於需要存儲的字符串的大小
len表示字符串的長度,也表示buf中已經被使用的空間的大小
free表示buf中沒有被使用的空間的大小。
要注意的是,buf的大小等於len+free+1,其中多余的1個字節是用來存儲’\0’的
那么這么封裝到底有什么好處呢?
1.常數復雜度獲取字符串長度
在C語言中的字符串只是簡單的字符的數組,當使用strlen獲取字符串長度的時候,C語言內部其實是直接順序遍歷數組的內容,找到對應的’\0’對應的字符,從而計算出字符串的長度。顯然這個算法復雜度和字符串的長度成正比,即O(N)。而對於SDS來說,只需要訪問SDS的len屬性就能得到字符串的長度,復雜度為O(1)。這樣,獲取字符串長度的操作就不會成為Redis的瓶頸。
2.杜絕緩沖區溢出
C++里面的字符串使用了STL的string類型,我們開發者不太需要關注內存的分配和釋放的過程。但是Redis是C語言編寫的,並沒有這么方便的數據類型。對於字符串的拼接、復制等操作,C語言開發者必須確保目標字符串的空間足夠大,不然就會出現溢出的情況。
char a[10]="hello";
strcar(a,"world");
strcpy(a,"hello world");
上面的三句代碼,就是C語言的字符串拼接和復制的使用,但是明顯出現了緩沖區溢出的問題。字符數組a的長度是10,而”hello world”字符串的長度為11,則需要12個字節的空間來存儲(不要忘記了’\0’)。
然而當使用SDS的API對字符串進行修改的時候,API內部第一步會檢測字符串的大小是否滿足。
如果空間已經滿足要求,那么就像C語言一樣操作即可。
如果不滿足,則拓展buf的空間,使得滿足操作的需求,之后再進行操作。
每次操作之后,len和free的值會做相應的修改。
這就是SDS的全部的高明之處了嗎?當然不!
當API發現SDS的buf的容量不夠的時候,並不是簡單申請正好適合的大小,而是額外申請了一倍的空間!我們以sds的API sdscat函數為例,該函數實現了sds的拼接的功能。
3.減少修改字符串時帶來的內存重新分配次數
c語言底層是一個N+1長的字符數組,長度的變化都會引起內存的重新分配。而對於SDS則通過一些策略去解決這些問題:
空間預分配
這種方式用於處理字符串長度增加的問題。
如果對字符串的修改使得字符串的長度增加,API首先會判斷buf的空間大小是否滿足,如果滿足則直接操作,如果不滿足,則進行如下操作:
如果對SDS進行修改之后的,SDS的長度(即len的值)小於1MB。程序將額外分配和len一樣大小的未使用空間。以上面的”hello” + ” world”的操作為例。
在這個例子中”hello”的len是5(不考慮’\0′),修改之后的字符串”hello world”長度為11,那么新的SDS的buf的容量就是11*2+1。其中len和free都是11,多余的1字節用來存儲’\0’。
惰性空間釋放
當執行字符串長度縮短的操作的時候,SDS並不直接重新分配多出來的字節,而是修改len和free的值(len相應減小,free相應增大,buf的空間大小不變化)。通過惰性空間釋放,可以很好的避免縮短字符串需要的內存重分配的情況。而且多余的空間也可以為將來可能有的字符串增長的操作做優化。
當然,SDS也提供直接釋放未使用空間的API,在需要的時候,也能真正的釋放掉多余的空間。
數據持久化
- filesnapshotting(快照)
- Append-only(aof)
filesnapshotting
默認redis是會以快照的形式將數據持久化到磁盤的(一個二進 制文件,xx.rdb)
在配置文件中的格式是:save N M表示在N秒之內,redis至少發生M次修改則redis抓快照到磁盤。
當然我們也可以手動執行save或者bgsave(異步)做快照。
工作原理簡單介紹:當redis需要做持久化時,redis會fork一個子進程;子進程將數據寫到磁盤上一個臨時RDB文件中;當子進程完成寫臨時文件后,將原來的RDB替換掉,這樣的好處就是可以copy-on-write
缺點:filesnapshotting方法在redis異常死掉時, 最近的數據會丟失(丟失數據的多少視你save策略的配置),所以這是它最大的缺點,當業務量很大時,丟失的數據是很多的
Appened-only
可以做到全部數據不丟失,但redis的性能就要差些。AOF就可以做到全程持久化,只需要在配置文件中開啟(默認是no),appendonly yes開啟AOF之后,可以選擇三種不同的策略,都會把它添加到aof文件中,當redis重啟時,將會讀取AOF文件進行“重放”以恢復到 redis關閉前的最后時刻。
AOF的三種策略
appendfsync :appendfsync always每提交一個修改命令都調用fsync刷新到AOF文件,非常非常慢,但也非常安全;
appendfsync everysec每秒鍾都調用fsync刷新到AOF文件,很快,但可能會丟失一秒以內的數據;
appendfsync no依靠OS進行刷新,redis不主動刷新AOF,這樣最快,但安全性就差。默認並推薦每秒刷新,這樣在速度和安全上都做到了兼顧。
LOG Rewriting隨着修改數據的執行AOF文件會越來越大,其中很多內容記錄某一個key的變化情況。
因此redis有了一種比較有意思的特性:在后台重建AOF文件,而不會影響client端操作。在任何時候執行BGREWRITEAOF命令,都會把當前內存中最短序列的命令寫到磁盤,這些命令可以完全構建當前的數據情況,而不會存在多余的變化情況(比如狀態變化,計數器變化等),縮小的AOF文件的大小。
所以當使用AOF時,redis推薦同時使用BGREWRITEAOF。
LOG Rewrite的工作原理:同樣用到了copy-on-write:首先redis會fork一個子進程;子進程將最新的AOF寫入一個臨時文件;父進程 增量的把內存中的最新執行的修改寫入(這時仍寫入舊的AOF,rewrite如果失敗也是安全的);當子進程完成rewrite臨時文件后,父進程會收到 一個信號,並把之前內存中增量的修改寫入臨時文件末尾;這時redis將舊AOF文件重命名,臨時文件重命名,開始向新的AOF中寫入。
Redis啟動加載過程
1. 初始化全局服務器配置
2. 加載配置文件(如果指定了配置文件,否則使用默認配置)
3. 初始化服務器
4. 加載數據庫
5. 網絡監聽
下面對上面這些步驟進行介紹
初始化全局服務器配置
初始化全局服務器配置通過initServerConfig()函數完成,主要是初始化server變量
初始化的內容包括下面幾個方面:
1. 網絡監聽相關,如綁定地址,TCP端口等
2. 虛擬內存相關,如swap文件、page大小等
3. 保存機制,多長時間內有多少次更新才進行保存
4. 復制相關,如是否是slave,master地址、端口
5. Hash相關設置
6. 初始化命令表
如其中的保存機制中,服務器初始化策略為:

// 1小時內1次更新 appendServerSaveParams(60*60,1); // 5分鍾內100次更新 appendServerSaveParams(300,100); // 1分鍾內10000次更新 appendServerSaveParams(60,10000);
如果在啟動服務器時,指定了配置文件,則會在下面的“加載配置文件”步驟中,根據配置文件內容,更改其中的某些服務器配置。
加載配置文件
如果指定了配置文件,Redis使用loadServerConfig()函數加載配置文件,使用標准I/O庫打開配置文件,循環讀取每一行然后覆蓋上一步進行的默認配置。
需要注意的是,下載Redis后代碼包中有一個默認配置文件,如果啟動Redis服務器時,不指定配置文件,Redis不會使用這個默認文件的配置,而是使用上一步“初始化全局服務器配置”中的配置。在默認配置文件中提供的配置項與上一步默認初始化的配置有些事不一樣的,所以如果沒有指定配置文件,千萬不能認為Redis的行為會按照默認配置文件進行,最典型的一個例子,在默認配置文件中的數據保存策略是:

# 15分鍾內1次更新 save 900 1 # 5分鍾內100次更新 save 300 10 # 1分鍾內10000次更新 save 60 10000
而默認初始化的全局配置中數據保存策略:appendServerSaveParams的配置項
初始化服務器
初始化服務器的工作在initServer()函數中,主要是完成前面未完成的工作,繼續對server變量初始化,如設置信號處理、創建clients、slaves列表,創建Pub/Sub通道列表,同時還會創建共享對象:
shared.crlf = createObject(REDIS_STRING,sdsnew("\r\n")); shared.ok = createObject(REDIS_STRING,sdsnew("+OK\r\n")); shared.err = createObject(REDIS_STRING,sdsnew("-ERR\r\n")); shared.emptybulk = createObject(REDIS_STRING,sdsnew("$0\r\n\r\n"));
最后,如果啟用了虛擬內存機制,還需要初始化虛擬內存相關,如Thread I/O等。
加載數據庫
在完成了上面的所有的初始化工作之后,Redis開始加載數據到內存中,如果啟用了appendonly了,則Redis從appendfile加載數據,否則就從dbfile加載數據。
1. 從appendfile中加載數據:loadAppendOnlyFile()函數
在此之前,我們先來看一下appendfile里面保存了什么,如我執行了下面兩條命令(記得在配置文件中開啟appendonly):
redis> SET mykey001 myvalue001 OK redis> GET mykey001 "myvalue001"
使用cat命令查看appendonly.aof的內容:

$ cat appendonly.aof *2 $6 SELECT $1 0 *3 $3 SET $8 mykey001 $10 myvalue001
在appendonly.aof文件中保存的正是從客戶端發過來的請求命令,還可以看到對於GET命令,並沒有保存。
既然appendonly.aof中保存了所有寫入數據的請求命令,那么在加載數據的時候只要重新執行一遍這些命令即可。
事實上Redis也正是這么做的,在開始加載之前暫時關閉appendonly,然后Redis創建一個假的Redis客戶端。
然后讀取appendonly.aof文件中的命令,在假的Redis客戶端上下文中執行,同時服務器也不對該客戶端做任何應答。
如果加載過程中物理內存不夠用,並且Redis開啟了VM,則還需要處理swap操作,最后加載完成后重新設置appendonly標志。
2. 從dbfile中加載數據:rdbLoad()函數
如果Redis沒有開啟appendonly,就需要從數據庫文件中加載數據到內存,基本步驟如下:
a. 處理SELECT命令,即選擇數據庫
b. 讀取key
c. 讀取value
d. 檢測key是否過期
e. 添加新的對象到哈希表
f. 設置過期時間(如果需要)
g. 如果開啟了VM,處理swap操作
網絡監聽
在完成了初始化配置和數據加載后,Redis啟動監聽。Redis的網絡庫沒有使用libevent或者libev,而是作者自己實現的一個非常輕量級的庫(主要實現在ae.c文件中)
然而回到我的問題上,如果數據庫中數據已經放入了32G的數據,在啟動redis時加載數據庫這部分必定會相當相當慢,而且涉及到宕機的問題(物理內存到達峰值)
這時可能單台redis很難解決這個問題,而應該考慮集群。