redis中存儲的數(shù)據(jù)在內存中,取內存數(shù)據(jù)速度比去磁盤取數(shù)據(jù)快多了,能夠減小數(shù)據(jù)庫壓力。內存存儲開銷比較大所以redis一般存一些比較常用的數(shù)據(jù)。
redis速度很快,官方數(shù)據(jù)每秒10w+QPS。(采用單線程,多路I/O復用模型)
持久化
redis數(shù)據(jù)存儲在內存中,如果不進行持久化,重啟系統(tǒng)后數(shù)據(jù)就丟失了
1. rdb
每隔一段時間對數(shù)據(jù)進行快照存儲
redis先fork一個子進程,子進程將數(shù)據(jù)集寫入臨時rdb文件中,等全部寫完以后用這個rdb文件替換掉之前的rdb文件并刪除舊的rdb。
2. aof
AOF持久化方式記錄每次對服務器寫的操作,當服務器重啟的時候會重新執(zhí)行這些命令來恢復原始的數(shù)據(jù),AOF命令以redis協(xié)議追加保存每次寫的操作到文件末尾。
兩者比較
RDB 方式可以保存過去一段時間內的數(shù)據(jù),并且保存結果是一個單一的文件,可以將文件備份到其他服務器,并且在回復大量數(shù)據(jù)的時候,RDB 方式的速度會比 AOF 方式的回復速度要快。
AOF 方式默認每秒鐘備份1次,頻率很高,它的操作方式是以追加的方式記錄日志而不是數(shù)據(jù),并且它的重寫過程是按順序進行追加,所以它的文件內容非常容易讀懂。可以在某些需要的時候打開 AOF 文件對其編輯,增加或刪除某些記錄,最后再執(zhí)行恢復操作。保存的數(shù)據(jù)比較完整。
主從復制
單節(jié)點就算進行了持久化但是遇到了磁盤故障也會導致丟失。主從復制可以避免單節(jié)點故障問題(全部節(jié)點都壞概率太小了),又可以Master/Slave讀寫分離,緩解Master讀的壓力。不過由于所有的寫操作都是在 Master 節(jié)點上操作,然后同步到 Slave 節(jié)點,那么同步就會有一定的延時。
原理
同步和命令傳播
- 從服務器發(fā)出SLAVEOF命令后,從服務器會向主節(jié)點發(fā)送SYNC命令。(新的命令叫PSYNC,可以解決斷線重復制的問題)
- 主服務器執(zhí)行 BGSAVE 命令,在后臺生成一個 RDB 文件,并使用一個緩沖區(qū)記錄從開始執(zhí)行的所有寫命令。
- 主服務器的 BGSAVE 命令執(zhí)行完畢后,主服務器會將 BGSAVE 命令生成的 RDB 文件發(fā)送給從服務器,從服務器接收此 RDB 文件,并將服務器狀態(tài)更新為RDB文件記錄的狀態(tài)。
- 主服務器把緩沖區(qū)的寫命令寫發(fā)給從服務器,從服務器執(zhí)行這些命令。
- 同步完成后,主服務器遇到新的寫命令后,把這些寫的命令傳播給從服務器。然后從服務器執(zhí)行后就能保持一致了。
哨兵
如果master服務器掛了那么整個系統(tǒng)就無法用了。哨兵模式監(jiān)控redis系統(tǒng)的運行狀態(tài),當master掛了以后可以主動挑選出一個從節(jié)點擔當主節(jié)點(自動通過投票機制),并建立和其他節(jié)點的關系。
redis集群
可以在多個 Redis 節(jié)點之間進行數(shù)據(jù)共享
集群+主從復制實現(xiàn)了redis高可用,又分布式服務又不容易掛掉。
redis集群不支持事務,rename(因為涉及了多個key)
Redis Cluster | 相關博客
去中心化,每個節(jié)點都是平等的并實現(xiàn)了redis的分布式存儲
Redis 集群沒有并使用傳統(tǒng)的一致性哈希來分配數(shù)據(jù),而是采用另外一種叫做哈希槽 (hash slot)的方式來分配的。redis cluster 默認分配了 16384 個slot,當我們set一個key 時,會用CRC16算法來取模得到所屬的slot,然后將這個key 分到哈希槽區(qū)間的節(jié)點上,具體算法就是:CRC16(key) % 16384。
所以,我們假設現(xiàn)在有3個節(jié)點已經(jīng)組成了集群,分別是:A, B, C 三個節(jié)點,它們可以是一臺機器上的三個端口,也可以是三臺不同的服務器。那么,采用哈希槽 (hash slot)的方式來分配16384個slot 的話,它們三個節(jié)點分別承擔的slot 區(qū)間是:
節(jié)點A覆蓋0-5460;
節(jié)點B覆蓋5461-10922;
節(jié)點C覆蓋10923-16383.
這種哈希槽的分配方式有好也有壞,好處就是很清晰,比如我想新增一個節(jié)點D,redis cluster的這種做法是從各個節(jié)點的前面各拿取一部分slot到D上。
一致性哈希|相關博客
hash(服務器的IP地址) % 2^32
2^32 無符號整形的最大值
通過上述公式算出的結果一定是一個0到2^32-1之間的一個整數(shù),我們就用算出的這個整數(shù),代表服務器A,既然這個整數(shù)肯定處于0到2^32-1之間,那么,上圖中的hash環(huán)上必定有一個點與這個整數(shù)對應,而我們剛才已經(jīng)說明,使用這個整數(shù)代表服務器A,那么,服務器A就可以映射到這個環(huán)上。
接下來使用如下算法定位數(shù)據(jù)訪問到相應服務器: 將數(shù)據(jù)key使用相同的函數(shù)Hash計算出哈希值,并確定此數(shù)據(jù)在環(huán)上的位置,從此位置沿環(huán)順時針“行走”,第一臺遇到的服務器就是其應該定位到的服務器!
redis緩存雪崩/緩存穿透
一致性哈希博客里面如果不使用一致性哈希,使用普通哈希函數(shù),如果其中有一臺服務器宕機了,哈希函數(shù)會大概率定位錯服務器導致大多數(shù)緩存失效,這就是緩存雪崩。
正常使用緩存的流程是拿到key后先去redis查詢,沒有的話再去數(shù)據(jù)庫查詢,如果查到數(shù)據(jù)以后保存key和value在緩存。惡意攻擊就會故意拿一些沒有的key去查詢,這會對數(shù)據(jù)庫造成壓力,這就是緩存穿透。(可以把key查不到的值稍微緩存?zhèn)€一分鐘解決。)
redis的事務
redis事務是通過MULTI,EXEC,DISCARD和WATCH四個原語實現(xiàn)的。
先以 MULTI 開始一個事務, 然后將多個命令入隊到事務中, 最后由 EXEC 命令觸發(fā)事務, 一并執(zhí)行事務中的所有命令
不保證原子性:redis同一個事務中如果有一條命令執(zhí)行失?。▍⒖?a target="_blank">事務中的2類錯誤,執(zhí)行失敗是指第二類錯誤),其后的命令仍然會被執(zhí)行,沒有回滾,這也就是:Redis部分支持事務。
redis的數(shù)據(jù)類型(String、List、Set、Hash、ZSet)
redis的內部整體的存儲結構就是一個大的hashmap
Redis中的一個對象的結構體包含:類型(上面五種),編碼方式(底層實現(xiàn)),指向對象的值(*ptr)等。
typedef struct redisObject {
// 類型
unsigned type:4;
// 編碼
unsigned encoding:4;
// 對象最后一次被訪問的時間
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用計數(shù)
int refcount;
// 指向實際值的指針
void *ptr;
} robj;
底層數(shù)據(jù)結構

1.String 相關博客
string針對不同的字符串會相應轉換成三種不同的底層實現(xiàn):
- 整數(shù),存儲字符串長度小于21且能夠轉化為整數(shù)的字符串。(int)
- EMBSTR,存儲字符串長度小于等于44(以前是39)的字符串(REDIS_ENCODING_EMBSTR_SIZE_LIMIT)。
- 剩余情況使用sds進行存儲。(raw)
為什么以44為界限分配:上面redisObject對象頭占據(jù)了16字節(jié),然后sds對象頭capacity,len,flags各占一個字節(jié),字符串結尾\0還要占一個字節(jié),redis的內存分配器jemalloc給embstr最多64字節(jié),減去上面的就變成最長44字節(jié)了。
embstr和sds區(qū)別:embstr的創(chuàng)建只需要分配一次內存,而sds需要兩次。同樣釋放內存也一樣
不過embstr是只讀的,如果需要修改他的值需要轉換成raw后才能進行修改
embstr 存儲形式
是將 RedisObject 對象頭和 SDS 對象連續(xù)存在一起,使用 malloc 方法一次分配。而 raw 存儲形式不一樣,它需要兩次 malloc,兩個對象頭在內存地址上一般是不連續(xù)的
SDS
struct sdshdr {
// buf 中已占用空間的長度
int len;
// buf 中剩余可用空間的長度
int free;
// 數(shù)據(jù)空間
char buf[];
};
SDS本質上就是char *,不過多了sdshdr結構的存在,獲取字符串長度的時間復雜度為O(1),直接讀取len就可以了。有free的存在,可以修改字符串長度時候杜絕緩沖區(qū)溢出,并減少修改字符串時帶來的內存重分配次數(shù)(C 字符串每次增長或者縮短, 都要對保存這個 C 字符串的數(shù)組進行一次內存重分配操作)。
2. List
壓縮鏈表
相關博客1
相關博客2
列表元素少而且里面元素長度也短的時候用壓縮鏈表比較好
壓縮鏈表將數(shù)據(jù)按照一定規(guī)則編碼在一塊連續(xù)的內存區(qū)域,目的是節(jié)省內存。
其實redis底層存儲不是簡單的鏈表而是快速鏈表quicksort,是由雙鏈表+壓縮鏈表組合而成。將多個壓縮鏈表用雙指針串起來。既滿足了快讀插入刪除的性能,又不會出現(xiàn)太大的空間冗余
雙向鏈表
// list 節(jié)點
typedef struct listNode {
// 前驅節(jié)點
struct listNode *prev;
// 后繼節(jié)點
struct listNode *next;
// 節(jié)點值
void *value;
} listNode;
// redis 雙鏈表實現(xiàn)
typedef struct list {
listNode *head; // 表頭指針
listNode *tail; // 表尾指針
void *(*dup)(void *ptr); // 節(jié)點值復制函數(shù)
void (*free)(void *ptr); // 節(jié)點值釋放函數(shù)(函數(shù)指針)
int (*match)(void *ptr, void *key); // 節(jié)點值對比函數(shù)
unsigned long len; // 鏈表包含的節(jié)點數(shù)量
} list;
當每增加一個listNode的時候,就需要重新malloc一塊內存
節(jié)點包含前驅指針和后繼指針,可以使用迭代輕松遍歷
header和tail,快速定位頭部和尾部。對于在鏈表的頭部或尾部進行插入節(jié)點的時間復雜度全部為O(1)
len:獲取鏈表長度的時間復雜度為O(1)。
3.Hash
當對象數(shù)目不多且內容不大的時候也可以用壓縮鏈表,哈希對象是按照key1,value1,key2,value2這樣的順序存放來存儲的。
redis hashtable 解決哈希沖突使用鏈地址法(python的dict用的是開放尋址法)
typedef struct dictht {
dictEntry **table; // 哈希表數(shù)組
unsigned long size; // 哈希表數(shù)組的大小
unsigned long sizemask; // 用于映射位置的掩碼,值永遠等于(size-1)
unsigned long used; // 哈希表已有節(jié)點的數(shù)量
}dictht;
Redis的字典的值只能是字符串,另外它rehash用的是漸進式rehash策略(一次性rehash過于耗時)。
漸進式rehash在rehash的同時,保存新舊兩個hash結構,查詢時會同時查詢兩個hash結構,然后在后續(xù)的定時任務以及hash操作指令中,循序漸進地將舊hash內容遷移到新hash結構中。遷移完成后,就會使用新的hash結構和取而代之。
4.Set
集合對象的編碼可以是intset或者hashtable。
intset是一個整數(shù)集合,里面存的為某種同一類型的整數(shù)
5.ZSet
類似于java中sortedset和hashmap的結合體,一方面它是一個set保證了value的唯一性,另一方面它可以為每個value賦值一個score,代表value的排序權重。它的內部實現(xiàn):跳躍鏈表。
普通鏈表按照score進行排序。意味著如果新元素要進來時需要遍歷鏈表才能找到特定位置的插入點。所以跳躍鏈表使用的是層級索引(最多32層)

python中redis的基本操作
import redis
# StrictRedis默認參數(shù)host='localhost', port=6379, db=0
# python3 redis 默認get字符串的時候 返回的是byte,需要設置decode_responses為True
r = redis.StrictRedis(password='your password',decode_responses=True)
1.string
r.set('name', 'alan', ex=20) #ex為過期時間單位秒
r.mset(name1='peter', name2='ben') #批量設置
r.get('name1') #獲取值,沒有的話會返回None
2.hash
r.hset('student', 'alan', 12) # student = {'alan':12}
r.hget('student', 'alan') # 返回12 字符串類型
r.hgetall('student') # 返回{'alan': '12'} dict類型
dic={"ben":10, "peter":22}
r.hmset("student",dic) #批量設置
r.hlen('student') # 獲取鍵值對個數(shù)
r.hkeys('student') # 獲取所有keys 返回一個數(shù)組
r.hvals('student') # 獲取所有vals
r.hexists('student', 'alan') #返回True
r.hdel('student_age', 'alan') #刪除指定key的鍵值對
3.list
blpop/brpop阻塞讀 消息延遲幾乎為0。阻塞讀在隊列沒有數(shù)據(jù)的時候會進入休眠狀態(tài),一旦數(shù)據(jù)來臨就會醒過來。
r.lpush('l_name', 1) #添加元素至列表最左邊
r.lpush('l_name', 2, 3) # [3, 2, 1]
r.rpush('l_name', 4) # 添加元素至列表最右邊
r.llen('l_name') # 查看列表元素個數(shù)
r.lpop('l_name') # 彈出列表最左邊元素
r.lrange('l',0, -1) # 獲取范圍內所有元素都是閉區(qū)間,-1代表最后一個元素
4.set
r.sadd('s_name', 'alan')
r.smembers('s_name') # 獲取所有元素
r.scard('s_name') # 元素個數(shù)
r.sadd('s2_name','peter')
r.sdiff('s_name','s2_name') #獲取在s_name但不在s2_name中的元素
r.sismember('s_name', '12') #檢查元素是否在set中
r.spop('s_name') # 移除最右邊的元素
5.zset
ZADD key score member [score member ...]
redis分布式鎖
分布式鎖的實現(xiàn)目標就是要在redis里面占坑,當別的進程也要來占坑時,發(fā)現(xiàn)坑已經(jīng)被人占了,只能放棄或稍后再來占。
占坑一般使用setnx(set if not exists)指令,只允許一個客戶端來占坑。先來先占,在調用del指令釋放坑。
為了防止死鎖我們要拿到坑位以后先設置個過期時間,保證鎖自動釋放。
redis2.8以后加入了set指令的擴展參數(shù),使set和expire可以一起執(zhí)行。
> set lock true ex 5 nx
...
> del lock