一文讀懂Redis

目錄結構如下:

簡介

Redis是一個高性能的key-value資料庫。Redis對數據的操作都是原子性的。

優缺點

優點:

  1. 基於記憶體操作,記憶體讀寫速度快。
  2. Redis是單執行緒的,避免執行緒切換開銷及多執行緒的競爭問題。單執行緒是指在處理網路請求(一個或多個redis客戶端連接)的時候只有一個執行緒來處理,redis運行時不止有一個執行緒,數據持久化或者向slave同步aof時會另起執行緒。
  3. 支援多種數據類型,包括String、Hash、List、Set、ZSet等
  4. 支援持久化。Redis支援RDB和AOF兩種持久化機制,持久化功能有效地避免數據丟失問題。
  5. redis 採用IO多路復用技術。多路指的是多個socket連接,復用指的是復用一個執行緒。redis使用單執行緒來輪詢描述符,將資料庫的開、關、讀、寫都轉換成了事件。多路復用主要有三種技術:select,poll,epoll。epoll是最新的也是目前最好的多路復用技術。

缺點:對join或其他結構化查詢的支援就比較差。

io多路復用

將用戶socket對應的文件描述符(file description)註冊進epoll,然後epoll幫你監聽哪些socket上有消息到達。當某個socket可讀或者可寫的時候,它可以給你一個通知。只有當系統通知哪個描述符可讀了,才去執行read操作,可以保證每次read都能讀到有效數據。這樣,多個描述符的I/O操作都能在一個執行緒內並發交替地順序完成,這就叫I/O多路復用,這裡的復用指的是復用同一個執行緒。

應用場景

  1. 快取熱點數據,緩解資料庫的壓力。
  2. 利用Redis中原子性的自增操作,可以用使用實現計算器的功能,比如統計用戶點贊數、用戶訪問數等,這類操作如果用MySQL,頻繁的讀寫會帶來相當大的壓力。
  3. 簡單消息隊列,不要求高可靠的情況下,可以使用Redis自身的發布/訂閱模式或者List來實現一個隊列,實現非同步操作。
  4. 好友關係,利用集合的一些命令,比如求交集、並集、差集等。可以方便搞定一些共同好友、共同愛好之類的功能。
  5. 限速器,比較典型的使用場景是限制某個用戶訪問某個API的頻率,常用的有搶購時,防止用戶瘋狂點擊帶來不必要的壓力。

給大家分享一個github倉庫,上面放了200多本經典的電腦書籍,包括C語言、C++、Java、Python、前端、資料庫、作業系統、電腦網路、數據結構和演算法、機器學習、編程人生等,可以star一下,下次找書直接在上面搜索,倉庫持續更新中~
github地址://github.com/Tyson0314/java-books
如果github訪問不了,可以訪問gitee倉庫。
gitee地址://gitee.com/tysondai/java-books

Memcached和Redis的區別

  1. Redis只使用單核,而Memcached可以使用多核。
  2. MemCached數據結構單一,僅用來快取數據,而Redis支援更加豐富的數據類型,也可以在伺服器端直接對數據進行豐富的操作,這樣可以減少網路IO次數和數據體積。
  3. MemCached不支援數據持久化,斷電或重啟後數據消失。Redis支援數據持久化和數據恢復,允許單點故障。

數據類型

Redis支援五種數據類型:

  • string(字元串)
  • hash(哈希)
  • list(列表)
  • set(集合)
  • zset(sorted set)

字元串類型

字元串類型的值可以是字元串、數字或者二進位,但值最大不能超過512MB。

常用命令:set, get, incr, incrby, desr, keys, append, strlen

  • 賦值和取值
SET name tyson
GET name
  • 遞增數字
INCR num       //若鍵值不是整數時,則會提示錯誤。
INCRBY num 2   //增加指定整數
DESR num       //遞減數字
INCRBY num 2.7 //增加指定浮點數
  • 其他

keys list* 列出匹配的key

APPEND name " dai" 追加值

STRLEN name 獲取字元串長度

MSET name tyson gender male 同時設置多個值

MGET name gender 同時獲取多個值

GETBIT name 0 獲取0索引處二進位位的值

FLUSHDB 刪除當前資料庫所有的key

FLUSHALL 刪除所有資料庫中的key

SETNX和SETEX

SETNX key value:當key不存在時,將key的值設為value。若給定的key已經存在,則SETNX不做任何操作。

SETEX key seconds value:比SET多了seconds參數,相當於SET KEY value + EXPIRE KEY seconds,而且SETEX是原子性操作。

keys和scan

redis的單執行緒的。keys指令會導致執行緒阻塞一段時間,直到執行完畢,服務才能恢復。scan採用漸進式遍歷的方式來解決keys命令可能帶來的阻塞問題,每次scan命令的時間複雜度是O(1),但是要真正實現keys的功能,需要執行多次scan。

scan的缺點:在scan的過程中如果有鍵的變化(增加、刪除、修改),遍歷過程可能會有以下問題:新增的鍵可能沒有遍歷到,遍歷出了重複的鍵等情況,也就是說scan並不能保證完整的遍歷出來所有的鍵。

scan命令用於迭代當前資料庫中的資料庫鍵:SCAN cursor [MATCH pattern] [COUNT count]

scan 0 match * count 10 //返回10個元素

SCAN相關命令包括SSCAN 命令、HSCAN 命令和 ZSCAN 命令,分別用於集合、哈希鍵及有序集合。

expire

SET password 666
EXPIRE password 5
TTL password //查看鍵的剩餘生存時間,-1為永不過期
SETEX password 60 123abc //SETEX可以在設置鍵的同時設置它的生存時間

EXPIRE時間單位是秒,PEXPIRE時間單位是毫秒。在鍵未過期前可以重新設置過期時間,過期之後則鍵被銷毀。

在Redis 2.6和之前版本,如果key不存在或者已過期時返回-1。

從Redis2.8開始,錯誤返回值的結果有如下改變:

  • 如果key不存在或者已過期,返回 -2
  • 如果key存在並且沒有設置過期時間(永久有效),返回 -1 。

type

TYPE 命令用於返回 key 所儲存的值的類型。

127.0.0.1:6379> type NEWBLOG
list

散列類型

常用命令:hset, hget, hmset, hmget, hgetall, hdel, hkeys, hvals

  • 賦值和取值
HSET car price 500 //HSET key field value
HGET car price

同時設置獲取多個欄位的值

HMSET car price 500 name BMW
HMGET car price name
HGETALL car

使用 HGETALL 命令時,如果哈希元素個數比較多,會存在阻塞Redis的可能。如果只需要獲取部分field,可以使用hmget,如果一定要獲取全部field-value,可以使用hscan命令,該命令會漸進式遍歷哈希類型。

HSETNX car price 400 //當欄位不存在時賦值,HSETNX是原子操作,不存在競態條件

  • 增加數字
    HINCRBY person score 60
  • 刪除欄位
    HDEL car price
  • 其他
HKEYS car //獲取key
HVALS car //獲取value
HLEN car  //長度

列表類型

常用命令:lpush, rpush, lpop, rpop, lrange, lrem

添加和刪除元素

LPUSH numbers 1
RPUSH numbers 2 3
LPOP numbers
RPOP numbers

獲取列表片段

LRANGE numbers 0 2
LRANGE numbers -2 -1 //支援負索引 -1是最右邊第一個元素
LRANGE numbers 0 -1

向列表插入值

首先從左到右尋找值為pivot的值,向列表插入value

LINSERT numbers AFTER 5 8 //往5後面插入8
LINSERT numbers BEFORE 6 9 //往6前面插入9

刪除元素

LTRIM numbers 1 2 刪除索引1到2以外的所有元素

LPUSH常和LTRIM一起使用來限制列表的元素個數,如保留最近的100條日誌

LPUSH logs $newLog
LTRIM logs 0 99

刪除列表指定的值

LREM key count value

  1. count < 0, 則從右邊開始刪除前count個值為value的元素
  2. count > 0, 則從左邊開始刪除前count個值為value的元素
  3. count = 0, 則刪除所有值為value的元素 `LREM numbers 0 2`

其他

LLEN numbers       //獲取列表元素個數LINDEX numbers -1  //返回指定索引的元素,index是負數則從右邊開始計算LSET numbers 1 7   //把索引為1的元素的值賦值成7

集合類型

常用命令:sadd, srem, smembers, scard, sismember, sdiff

集合中不能有相同的元素。

增加/刪除元素

SADD letters a b cSREM letters c d

獲取元素

SMEMBERS lettersSCARD letters   //獲取集合元素個數

判斷元素是否在集合中
SISMEMBER letters a

集合間的運算

SDIFF setA setB  //差集運算SINTER setA setB //交集運算SUNION setA setB //並集運算

三個命令都可以傳進多個鍵 SDIFF setA setB setC

其他

SDIFFSTORE result setA setB 進行集合運算並將結果存儲

SRANDMEMBER key count

隨機獲取集合里的一個元素,count大於0,則從集合隨機獲取count個不重複的元素,count小於0,則隨機獲取的count個元素有些可能相同。

SPOP letters

有序集合類型

常用命令:zadd, zrem, zscore, zrange

zadd zsetkey 50 e1 60 e2 30 e3

Zset(sorted set)是string類型的有序集合。zset 和 set 一樣也是string類型元素的集合,且不允許重複的成員。不同的是Zset每個元素都會關聯一個double(超過17位使用科學計演算法表示,可能丟失精度)類型的分數,通過分數來為集合中的成員進行排序。zset的成員是唯一的,但分數(score)可以重複。

有序集合和列表相同點:

  1. 都是有序的;
  2. 都可以獲得某個範圍內的元素。

有序集合和列表不同點:

  1. 列表基於鏈表實現,獲取兩端元素速度快,訪問中間元素速度慢;
  2. 有序集合基於散列表和跳躍表實現,訪問中間元素時間複雜度是OlogN;
  3. 列表不能簡單的調整某個元素的位置,有序列表可以(更改元素的分數);
  4. 有序集合更耗記憶體。

增加/刪除元素

時間複雜度OlogN。

ZADD scoreboard 89 Tom 78 Sophia
ZADD scoreboard 85.5 Tyson      //支援雙精度浮點數
ZREM scoreboard Tyson
ZREMRANGEBYRANK scoreboard 0 2  //按照排名範圍刪除元素
ZREMRANGEBYSCORE scoreboard (80 100 //按照分數範圍刪除元素,"("代表不包含

獲取元素分數

時間複雜度O1。

ZSCORE scoreboard Tyson

獲取排名在某個範圍的元素列表

ZRANGE命令時間複雜度是O(log(n)+m), n是有序集合元素個數,m是返回元素個數。

ZRANGE scoreboard 0 2
ZRANGE scoreboard 1 -1  //-1表示最後一個元素
ZRANGE scoreboard 0 -1 WITHSCORES  //同時獲得分數

獲取指定分數範圍的元素

ZRANGEBYSCORE命令時間複雜度是O(log(n)+m), n是有序集合元素個數,m是返回元素個數。

ZRANGEBYSCORE scoreboard 80 100
ZRANGEBYSCORE scoreboard 80 (100  //不包含100
ZRANGEBYSCORE scoreboard (60 +inf LIMIT 1 3 //獲取分數高於60的從第二個人開始的3個人

增加某個元素的分數

時間複雜度OlogN。

ZINCRBY scoreboard 10 Tyson

其他

ZCARD scoreboard          //獲取集合元素個數,時間複雜度O1
ZCOUNT scoreboard 80 100  //指定分數範圍的元素個數
ZRANK scoreboard Tyson    //按從小到大的順序獲取元素排名
ZREVRANK scoreboard Tyson //按從大到小的順序獲取元素排名

Bitmaps

Bitmaps本身不是一種數據結構,實際上它就是字元串,但是它可以對字元串的位進行操作,可以把Bitmaps想像成一個以位為單位的數組,數組的每個單元只能存儲0和1。

bitmap的長度與集合中元素個數無關,而是與基數的上限有關。假如要計算上限為1億的基數,則需要12.5M位元組的bitmap。就算集合中只有10個元素也需要12.5M。

HyperLogLog

HyperLogLog 是用來做基數統計的演算法,其優點是,在輸入元素的數量或者體積非常非常大時,計算基數所需的空間總是固定的、並且是很小的。

基數:比如數據集 {1, 3, 5, 7, 5, 7, 8}, 那麼這個數據集的基數集為 {1, 3, 5 ,7, 8},基數即不重複元素為5。

應用場景:獨立訪客(unique visitor,uv)統計。

數據結構

動態字元串

SDS定義:

struct sdshdr {

    // 記錄 buf 數組中已使用位元組的數量
    // 等於 SDS 所保存字元串的長度
    int len;

    // 記錄 buf 數組中未使用位元組的數量
    int free;

    // 位元組數組,用於保存字元串
    char buf[];

};
C 字元串 SDS
獲取字元串長度的複雜度為 O(N) 。 獲取字元串長度的複雜度為 O(1) 。
API 是不安全的,可能會造成緩衝區溢出。 API 是安全的,不會造成緩衝區溢出。
修改字元串長度 N 次必然需要執行 N 次記憶體重分配。 修改字元串長度 N 次最多需要執行 N 次記憶體重分配。
只能保存文本數據。 可以保存文本或者二進位數據。
可以使用所有 <string.h> 庫中的函數。 可以使用一部分 <string.h> 庫中的函數。

字典

字典使用hashtable作為底層實現。鍵值對的值可以是一個指針, 或者是一個 uint64_t 整數, 又或者是一個 int64_t 整數。

typedef struct dictEntry {

    // 鍵
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下個哈希表節點,形成鏈表
    struct dictEntry *next;

} dictEntry;

整數集合

整數集合(intset)是 Redis 用於保存整數值的集合抽象數據結構, 它可以保存類型為 int16_t 、 int32_t 或者 int64_t 的整數值, 並且保證集合中不會出現重複元素。

壓縮列表

ziplist是 Redis 為了節約記憶體而開發的, 由一系列特殊編碼的連續記憶體塊組成的順序型(sequential)數據結構。每個壓縮列表節點都由 previous_entry_length 、 encoding 、 content 三個部分組成。

節點的 previous_entry_length 屬性以位元組為單位, 記錄了壓縮列表中前一個節點的長度。
節點的 encoding 屬性記錄了節點的 content 屬性所保存數據的類型以及長度。有兩種編碼方式,位元組數組編碼和整數編碼。

壓縮列表的從表尾向表頭遍歷操作就是使用這一原理實現的: 只要我們擁有了一個指向某個節點起始地址的指針, 那麼通過這個指針以及這個節點的 previous_entry_length 屬性, 程式就可以一直向前一個節點回溯, 最終到達壓縮列表的表頭節點。

跳錶

跳錶可以看成多層鏈表,它有如下的性質:

  • 多層的結構組成,每層是一個有序的鏈表
  • 最底層的鏈表包含所有的元素
  • 跳躍表的查找次數近似於層數,時間複雜度為O(logn),插入、刪除也為 O(logn)

對象

Redis 的對象系統還實現了基於引用計數技術的記憶體回收機制: 當程式不再使用某個對象的時候, 這個對象所佔用的記憶體就會被自動釋放; 另外, Redis 還通過引用計數技術實現了對象共享機制, 這一機制可以在適當的條件下, 通過讓多個資料庫鍵共享同一個對象來節約記憶體。

底層實現

string

字元串對象的編碼可以是 int 、 raw 或者 embstr 。

  1. 如果一個字元串對象保存的是整數值, 並且這個整數值可以用 long 類型來表示, 那麼會將編碼設置為 int 。
  2. 如果字元串對象保存的是一個字元串值, 並且這個字元串值的長度大於 39 位元組, 那麼字元串對象將使用一個簡單動態字元串(SDS)來保存這個字元串值, 並將對象的編碼設置為 raw 。
  3. 如果字元串對象保存的是一個字元串值, 並且這個字元串值的長度小於等於 39 位元組, 那麼字元串對象將使用 embstr 編碼的方式來保存這個字元串值。
值 編碼
可以用 long 類型保存的整數。 int
可以用 long double 類型保存的浮點數。 embstr 或者 raw
字元串值, 或者因為長度太大而沒辦法用 long 類型表示的整數, 又或者因為長度太大而沒辦法用 long double 類型表示的浮點數。 embstr 或者 raw

hash

hash類型內部編碼有兩種:

  1. ziplist,壓縮列表。當哈希類型元素個數小於512個,並且所有值都小於64位元組時,Redis會使用ziplist作為哈希的內部實現。ziplist使用更加緊湊的結構實現多個元素的連續存儲,更加節省記憶體。
  2. hashtable。當哈希類型無法滿足ziplist的條件時,Redis會使用hashtable作為哈希的內部實現,因為此時ziplist的讀寫效率會下降,而hashtable的讀寫時間複雜度為O(1)。

使用 ziplist 作為 hash 的底層實現時,添加元素的時候,同一鍵值對的兩個節點總是緊挨在一起, 保存鍵的節點在前, 保存值的節點在後。

使用場景:記錄部落格點贊數量。hset MAP_BLOG_LIKE_COUNT blogId likeCount,key為MAP_BLOG_LIKE_COUNT,field為部落格id,value為點贊數量。

list

列表list類型內部編碼有兩種:

  1. ziplist,壓縮列表。當列表中的元素個數小於512個,同時列表中每個元素的值都小於64位元組時,Redis會選用ziplist來作為列表的內部實現來減少記憶體的使用。
  2. 當列表類型無法滿足ziplist的條件時,Redis會使用linkedlist作為列表的內部實現。

Redis3.2版本提供了quicklist內部編碼,簡單地說它是以一個ziplist為節點的linkedlist,它結合了ziplist和linkedlist兩者的優勢,為列表類型提供了一種更為優秀的內部編碼實現。

使用場景:

  1. 消息隊列。Redis的lpush+brpop命令組合即可實現阻塞隊列。

set

集合對象的編碼可以是 intset 或者 hashtable 。

  1. intset 編碼的集合對象使用整數集合作為底層實現, 集合對象包含的所有元素都被保存在整數集合(數組)裡面。
  2. hashtable 編碼的集合對象使用字典作為底層實現, 字典的每個鍵都是一個字元串對象, 而字典的值則全部被設置為 NULL 。

zset

有序集合的編碼可以是 ziplist 或者 skiplist 。當有序集合的元素個數小於128,同時每個元素的值都小於64位元組時,Redis會用ziplist來作為有序集合的內部實現,ziplist可以有效減少記憶體的使用。否則,使用skiplist作為有序集合的內部實現。

  1. ziplist 編碼的有序集合對象使用壓縮列表作為底層實現, 每個集合元素使用兩個緊挨在一起的壓縮列表節點來保存, 第一個節點保存元素的成員(member), 而第二個元素則保存元素的分值(score)。壓縮列表內的集合元素按分值從小到大進行排序。
  2. skiplist 編碼的有序集合對象使用字典和跳躍表實現。使用字典查找給定成員的分值,時間複雜度為O(1) (跳躍表查找時間複雜度為O(logN))。使用跳躍表可以對有序集合進行範圍型操作。

使用場景

string:1、常規key-value快取應用。常規計數如微博數、粉絲數。2、分散式鎖。

hash:存放結構化數據,如用戶資訊(昵稱、年齡、性別、積分等)。

list:熱門部落格列表、消息隊列系統。使用list可以構建隊列系統,比如:將Redis用作日誌收集器,多個端點將日誌資訊寫入Redis,然後一個worker統一將所有日誌寫到磁碟。

set:1、好友關係,微博粉絲的共同關注、共同喜好、共同好友等;2、利用唯一性,統計訪問網站的所有獨立ip 。

zset:1、排行榜;2、優先順序隊列。

資料庫管理

切換資料庫:select 1。Redis默認配置中是有16個資料庫。0號資料庫和15號資料庫之間的數據沒有任何關聯,可以存在相同的鍵。不建議使用Redis多資料庫功能,可以在一台機器上部署多個Redis實例,使用埠號區分,實現多資料庫功能。

flushdb/flushall命令用於清除資料庫,兩者的區別的是flushdb只清除當前資料庫,flushall會清除所有資料庫。如果當前資料庫鍵值數量比較多,flushdb/flushall存在阻塞Redis的可能性,並且這兩個命令會將所有數據清除,一旦誤操作後果不堪設想。

排序

LPUSH myList 4 8 2 3 6
SORT myList DESC
LPUSH letters f l d n c
SORT letters ALPHA

BY參數

LPUSH list1 1 2 3
SET score:1 50
SET score:2 100
SET score:3 10
SORT list1 BY score:* DESC

GET參數

GET參數命令作用是使SORT命令的返回結果是GET參數指定的鍵值。

SORT tag:Java:posts BY post:*->time DESC GET post:*->title GET post:*->time GET #

GET #返迴文章ID。

STORE參數

SORT tag:Java:posts BY post:*->time DESC GET post:*->title STORE resultCache

EXPIRE resultCache 10 //STORE結合EXPIRE可以快取排序結果

事務

事務的原理是將一個事務範圍內的若干命令發送給Redis,然後再讓Redis依次執行這些命令。

事務的生命周期:

  1. 使用MULTI開啟一個事務

  2. 在開啟事務的時候,每次操作的命令將會被插入到一個隊列中,同時這個命令並不會被真的執行

  3. EXEC命令進行提交事務

DISCARD:放棄事務,即該事務內的所有命令都將取消

一個事務範圍內某個命令出錯不會影響其他命令的執行,不保證原子性:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 1
QUEUED
127.0.0.1:6379> set b 1 2
QUEUED
127.0.0.1:6379> set c 3
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR syntax error
3) OK

事務里的命令執行時會讀取最新的值:

WATCH命令

WATCH命令可以監控一個或多個鍵,一旦其中有一個鍵被修改,之後的事務就不會執行(類似於樂觀鎖)。執行EXEC命令之後,就會自動取消監控。

127.0.0.1:6379> watch name
OK
127.0.0.1:6379> set name 1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name 2
QUEUED
127.0.0.1:6379> set gender 1
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get gender
(nil)

UNWATCH:取消WATCH命令對多有key的監控,所有監控鎖將會被取消。

消息隊列

使用一個列表,讓生產者將任務使用LPUSH命令放進列表,消費者不斷用RPOP從列表取出任務。

BRPOP和RPOP命令相似,唯一的區別就是當列表沒有元素時BRPOP命令會一直阻塞連接,直到有新元素加入。
BRPOP queue 0 //0表示不限制等待時間

優先順序隊列

BLPOP queue:1 queue:2 queue:3 0
如果多個鍵都有元素,則按照從左到右的順序取元素

發布/訂閱模式

PUBLISH channel1 hi
SUBSCRIBE channel1
UNSUBSCRIBE channel1 //退訂通過SUBSCRIBE命令訂閱的頻道。

PSUBSCRIBE channel?* 按照規則訂閱
PUNSUBSCRIBE channel?* 退訂通過PSUBSCRIBE命令按照某種規則訂閱的頻道。其中訂閱規則要進行嚴格的字元串匹配,PUNSUBSCRIBE *無法退訂channel?*規則。

缺點:在消費者下線的情況下,生產的消息會丟失。

延時隊列

使用sortedset,拿時間戳作為score,消息內容作為key,調用zadd來生產消息,消費者用zrangebyscore指令獲取N秒之前的數據輪詢進行處理。

持久化

Redis支援兩種方式的持久化,一種是RDB的方式,一種是AOF的方式。前者會根據指定的規則定時將記憶體中的數據存儲在硬碟上,而後者在每次執行完命令後將命令記錄下來。一般將兩者結合使用。

RDB方式

RDB 是 Redis 默認的持久化方案。RDB持久化時會將記憶體中的數據寫入到磁碟中,在指定目錄下生成一個dump.rdb文件。Redis 重啟會載入dump.rdb文件恢複數據。

RDB持久化的過程(執行SAVE命令除外):

  • 創建一個子進程;
  • 父進程繼續接收並處理客戶端的請求,而子進程開始將記憶體中的數據寫進硬碟的臨時文件;
  • 當子進程寫完所有數據後會用該臨時文件替換舊的RDB文件。

Redis啟動時會讀取RDB快照文件,將數據從硬碟載入記憶體。通過RDB方式的持久化,一旦Redis異常退出,就會丟失最近一次持久化以後更改的數據。

觸發RDB快照:

  1. 手動觸發:

    • 用戶執行SAVE或BGSAVE命令。SAVE命令執行快照的過程會阻塞所有來自客戶端的請求,應避免在生產環境使用這個命令。BGSAVE命令可以在後台非同步進行快照操作,快照的同時伺服器還可以繼續響應客戶端的請求,因此需要手動執行快照時推薦使用BGSAVE命令;
  2. 被動觸發:

    • 根據配置規則進行自動快照,如SAVE 300 10,300秒內至少有10個鍵被修改則進行快照。
    • 如果從節點執行全量複製操作,主節點自動執行bgsave生成RDB文件並發送給從節點。
    • 默認情況下執行shutdown命令時,如果沒有開啟AOF持久化功能則自動執行bgsave。
    • 執行debug reload命令重新載入Redis時,也會自動觸發save操作。

優點:Redis載入RDB恢複數據遠遠快於AOF的方式。

缺點:

  1. RDB方式數據沒辦法做到實時持久化/秒級持久化。因為bgsave每次運行都要執行fork操作創建子進程,屬於重量級操作,頻繁執行成本過高。
  2. 存在老版本Redis服務和新版本RDB格式兼容性問題。RDB文件使用特定二進位格式保存,Redis版本演進過程中有多個格式的RDB版本,存在老版本Redis服務無法兼容新版RDB格式的問題。

AOF方式

AOF(append only file)持久化:以獨立日誌的方式記錄每次寫命令,Redis重啟時會重新執行AOF文件中的命令達到恢複數據的目的。AOF的主要作用是解決了數據持久化的實時性,目前已經是Redis持久化的主流方式。

默認情況下Redis沒有開啟AOF方式的持久化,可以通過appendonly參數啟用appendonly yes。開啟AOF方式持久化後每執行一條寫命令,Redis就會將該命令寫進aof_buf緩衝區,AOF緩衝區根據對應的策略向硬碟做同步操作。

默認情況下系統每30秒會執行一次同步操作。為了防止緩衝區數據丟失,可以在Redis寫入AOF文件後主動要求系統將緩衝區數據同步到硬碟上。可以通過appendfsync參數設置同步的時機。

appendfsync always //每次寫入aof文件都會執行同步,最安全最慢,只能支援幾百TPS寫入,不建議配置
appendfsync everysec  //保證了性能也保證了安全,建議配置
appendfsync no //由作業系統決定何時進行同步操作

重寫機制:

隨著命令不斷寫入AOF,文件會越來越大,為了解決這個問題,Redis引入AOF重寫機制壓縮文件體積。AOF文件重寫是把Redis進程內的數據轉化為寫命令同步到新AOF文件的過程。

優點:
(1)AOF可以更好的保護數據不丟失,一般AOF會每秒去執行一次fsync操作,如果redis進程掛掉,最多丟失1秒的數據。
(2)AOF以appen-only的模式寫入,所以沒有任何磁碟定址的開銷,寫入性能非常高。
缺點
(1)對於同一份文件AOF文件比RDB數據快照要大。
(2)不適合寫多讀少場景。
(3)數據恢複比較慢。

RDB和AOF如何選擇
(1)僅使用RDB這樣會丟失很多數據。
(2)僅使用AOF,因為這一會有兩個問題,第一通過AOF恢復速度慢;第二RDB每次簡單粗暴生成數據快照,更加安全健壯。
(3)綜合AOF和RDB兩種持久化方式,用AOF來保證數據不丟失,作為恢複數據的第一選擇;用RDB來做不同程度的冷備,在AOF文件都丟失或損壞不可用的時候,可以使用RDB進行快速的數據恢復。

集群

主從複製

redis的複製功能是支援多個資料庫之間的數據同步。主資料庫可以進行讀寫操作,當主資料庫的數據發生變化時會自動將數據同步到從資料庫。從資料庫一般是只讀的,它會接收主資料庫同步過來的數據。一個主資料庫可以有多個從資料庫,而一個從資料庫只能有一個主資料庫。

redis-server //啟動Redis實例作為主資料庫 
redis-server --port 6380 --slaveof  127.0.0.1 6379  //啟動另一個實例作為從資料庫 
slaveof 127.0.0.1 6379
SLAVEOF NO ONE //停止接收其他資料庫的同步並轉化為主資料庫。

同步機制

  1. 保存主節點資訊。
  2. 主從建立socket連接。
  3. 從節點發送ping命令進行首次通訊,主要用於檢測網路狀態。
  4. 許可權認證。如果主節點設置了requirepass參數,則需要密碼認證。從節點必須配置masterauth參數保證與主節點相同的密碼才能通過驗證。
  5. 同步數據集。第一次同步的時候,從資料庫啟動後會向主資料庫發送SYNC命令。主資料庫接收到命令後開始在後台保存快照(RDB持久化過程),並將保存快照過程接收到的命令快取起來。當快照完成後,Redis會將快照文件和快取的命令發送到從資料庫。從資料庫接收到後,會載入快照文件並執行快取的命令。以上過程稱為複製初始化。
  6. 複製初始化完成後,主資料庫每次收到寫命令就會將命令同步給從資料庫,從而實現主從資料庫數據的一致性。

Redis在2.8及以上版本使用psync命令完成主從數據同步,同步過程分為:全量複製和部分複製。

全量複製:一般用於初次複製場景,Redis早期支援的複製功能只有全量複製,它會把主節點全部數據一次性發送給從節點,當數據量較大時,會對主從節點和網路造成很大的開銷。

部分複製:用於處理在主從複製中因網路閃斷等原因造成的數據丟失場景,當從節點再次連上主節點後,如果條件允許,主節點會補發丟失數據給從節點。因為補發的數據遠遠小於全量數據,可以有效避免全量複製的過高開銷。

讀寫分離

通過redis的複製功能可以實現資料庫的讀寫分離,提高伺服器的負載能力。主資料庫主要進行寫操作,而從資料庫負責讀操作。很多場景下對資料庫的讀頻率大於寫,當單機的Redis無法應付大量的讀請求時,可以通過複製功能建立多個從資料庫節點,主資料庫負責寫操作,從資料庫負責讀操作。這種一主多從的結構很適合讀多寫少的場景。

從資料庫持久化

持久化的操作比較耗時,為了提高性能,可以建立一個從資料庫,並在從資料庫進行持久化,同時在主資料庫禁用持久化。

哨兵Sentinel

當master節點奔潰時,可以手動將slave提升為master,繼續提供服務。

  • 首先,從資料庫使用SLAVE NO ONE將從資料庫提升為主資料庫繼續服務;
  • 啟動奔潰的主資料庫,通過SLAVEOF命令將其設置為新的主資料庫的從資料庫,即可將數據同步過來。

通過哨兵機制可以自動切換主從節點。哨兵是一個獨立的進程,用於監控redis實例的是否正常運行。

作用

  1. 監測redis實例的狀態
  2. 如果master實例異常,會自動進行主從節點切換

客戶端連接redis的時候,先連接哨兵,哨兵會告訴客戶端redis主節點的地址,然後客戶端連接上redis並進行後續的操作。當主節點宕機的時候,哨兵監測到主節點宕機,會重新推選出某個表現良好的從節點成為新的主節點,然後通過發布訂閱模式通知其他的從伺服器,讓它們切換主機。

定時任務

  1. 每隔10s,每個Sentinel節點會向主節點和從節點發送info命令獲取最新的拓撲結構。
  2. 每隔2s,每個Sentinel節點會去獲取其他Sentinel節點對於主節點的判斷以及當前Sentinel節點的資訊,用於判斷主節點是否客觀下線和是否有新的Sentinel節點加入。
  3. 每隔1s,每個Sentinel節點會向主節點、從節點、其餘Sentinel節點發送一條ping命令做一次心跳檢測,來確認這些節點是否可達。

工作原理

  • 每個Sentinel以每秒鐘一次的頻率向它所知的Master,Slave以及其他 Sentinel 實例發送一個 PING 命令。
  • 如果一個實例距離最後一次有效回復 PING 命令的時間超過指定的值, 則這個實例會被 Sentinel 標記為主觀下線。
  • 如果一個Master被標記為主觀下線,則正在監視這個Master的所有 Sentinel 要以每秒一次的頻率確認Master是否真正進入主觀下線狀態。
  • 當有足夠數量的 Sentinel(大於等於配置文件指定的值)在指定的時間範圍內確認Master的確進入了主觀下線狀態, 則Master會被標記為客觀下線 。若沒有足夠數量的 Sentinel 同意 Master 已經下線, Master 的客觀下線狀態就會被移除。 若 Master 重新向 Sentinel 的 PING 命令返回有效回復, Master 的主觀下線狀態就會被移除。
  • 哨兵節點會選舉出哨兵領導者,負責故障轉移的工作。
  • 哨兵領導者會推選出某個表現良好的從節點成為新的主節點,然後通知其他從節點更新主節點。
/**
 * 測試Redis哨兵模式
 * @author liu
 */
public class TestSentinels {
    @SuppressWarnings("resource")
    @Test
    public void testSentinel() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(10);
        jedisPoolConfig.setMaxIdle(5);
        jedisPoolConfig.setMinIdle(5);
        // 哨兵資訊
        Set<String> sentinels = new HashSet<>(Arrays.asList("192.168.11.128:26379",
                "192.168.11.129:26379","192.168.11.130:26379"));
        // 創建連接池
        JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels,jedisPoolConfig,"123456");
        // 獲取客戶端
        Jedis jedis = pool.getResource();
        // 執行兩個命令
        jedis.set("mykey", "myvalue");
        String value = jedis.get("mykey");
        System.out.println(value);
    }
}

cluster

集群用於分擔寫入壓力,主從用於災難備份和高可用以及分擔讀壓力。

主從複製存在不能自動故障轉移、達不到高可用的問題。
哨兵模式解決了主從複製不能自動故障轉移、達不到高可用的問題,但還是存在主節點的寫能力、容量受限於單機配置的問題。
cluster模式實現了Redis的分散式存儲,每個節點存儲不同的內容,解決主節點的寫能力、容量受限於單機配置的問題。

哈希分區演算法

節點取余分區。使用特定的數據,如Redis的鍵或用戶ID,對節點數量N取余:hash(key)%N計算出哈希值,用來決定數據映射到哪一個節點上。
優點是簡單性。擴容時通常採用翻倍擴容,避免數據映射全部被打亂導致全量遷移的情況。

一致性哈希分區:為系統中每個節點分配一個token,範圍一般在0~232,這些token構成一個哈希環。數據讀寫執行節點查找操作時,先根據key計算hash值,然後順時針找到第一個大於等於該哈希值的token節點。
這種方式相比節點取余最大的好處在於加入和刪除節點隻影響哈希環中相鄰的節點,對其他節點無影響。

Redis Cluser採用虛擬槽分區,所有的鍵根據哈希函數映射到0~16383整數槽內,計算公式:slot=CRC16(key)&16383。每一個節點負責維護一部分槽以及槽所映射的鍵值數據。

故障轉移

Redis集群內節點通過ping/pong消息實現節點通訊,消息不但可以傳播節點槽資訊,還可以傳播其他狀態如:主從狀態、節點故障等。因此故障發現也是通過消息傳播機制實現的,主要環節包括:主觀下線(pfail)和客觀下線(fail)。

LUA腳本

Redis 通過 LUA 腳本創建具有原子性的命令: 當lua腳本命令正在運行的時候,不會有其他腳本或 Redis 命令被執行,實現組合命令的原子操作。

在Redis中執行Lua腳本有兩種方法:eval和evalsha。

eval 命令使用內置的 Lua 解釋器,對 Lua 腳本進行求值。

//第一個參數是lua腳本,第二個參數是鍵名參數個數,剩下的是鍵名參數和附加參數
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

evalsha

Redis還提供了evalsha命令來執行Lua腳本。首先要將Lua腳本載入到Redis服務端,得到該腳本的SHA1校驗和。Evalsha 命令根據給定的 sha1 校驗和,執行快取在伺服器中的腳本。

script load命令可以將腳本內容載入到Redis記憶體中。

redis 127.0.0.1:6379> SCRIPT LOAD "return 'hello moto'"
"232fd51614574cf0867b83d384a5e898cfd24e5a"

redis 127.0.0.1:6379> EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 0
"hello moto"

使用evalsha執行Lua腳本過程如下:

lua腳本作用

1、Lua腳本在Redis中是原子執行的,執行過程中間不會插入其他命令。

2、Lua腳本可以將多條命令一次性打包,有效地減少網路開銷。

應用場景

限制介面訪問頻率。

在Redis維護一個介面訪問次數的鍵值對,key是介面名稱,value是訪問次數。每次訪問介面時,會執行以下操作:

  • 通過aop攔截介面的請求,對介面請求進行計數,每次進來一個請求,相應的介面count加1,存入redis。
  • 如果是第一次請求,則會設置count=1,並設置過期時間。因為這裡set()和expire()組合操作不是原子操作,所以引入lua腳本,實現原子操作,避免並發訪問問題。
  • 如果給定時間範圍內超過最大訪問次數,則會拋出異常。
private String buildLuaScript() {
    return "local c" +
        "\nc = redis.call('get',KEYS[1])" +
        "\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
        "\nreturn c;" +
        "\nend" +
        "\nc = redis.call('incr',KEYS[1])" +
        "\nif tonumber(c) == 1 then" +
        "\nredis.call('expire',KEYS[1],ARGV[2])" +
        "\nend" +
        "\nreturn c;";
}

String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());

刪除策略

  1. 被動刪除。在訪問key時,如果發現key已經過期,那麼會將key刪除。

  2. 主動刪除。定時清理key,每次清理會依次遍歷所有DB,從db隨機取出20個key,如果過期就刪除,如果其中有5個key過期,那麼就繼續對這個db進行清理,否則開始清理下一個db。

  3. 記憶體不夠時清理。Redis有最大記憶體的限制,通過maxmemory參數可以設置最大記憶體,當使用的記憶體超過了設置的最大記憶體,就要進行記憶體釋放, 在進行記憶體釋放的時候,會按照配置的淘汰策略清理記憶體,淘汰策略一般有6種,Redis4.0版本後又增加了2種,主要由分為三類:

    • 第一類 不處理 noeviction。發現記憶體不夠時,不刪除key,執行寫入命令時直接返回錯誤資訊。(默認的配置)

    • 第二類 從所有結果集中的key中挑選,進行淘汰

      • allkeys-random 就是從所有的key中隨機挑選key,進行淘汰
      • allkeys-lru 就是從所有的key中挑選最近最少使用的數據淘汰
      • allkeys-lfu 就是從所有的key中挑選使用頻率最低的key,進行淘汰。(這是Redis 4.0版本後新增的策略)
    • 第三類 從設置了過期時間的key中挑選,進行淘汰

      這種就是從設置了expires過期時間的結果集中選出一部分key淘汰,挑選的演算法有:

      • volatile-random 從設置了過期時間的結果集中隨機挑選key刪除。
      • volatile-lru 從設置了過期時間的結果集中挑選最近最少使用的數據淘汰
      • volatile-ttl 從設置了過期時間的結果集中挑選可存活時間最短的key開始刪除(也就是從哪些快要過期的key中先刪除)
      • volatile-lfu 從過期時間的結果集中選擇使用頻率最低的key開始刪除(這是Redis 4.0版本後新增的策略)

其他

客戶端

Redis 客戶端與服務端之間的通訊協議是在TCP協議之上構建的。

Redis Monitor 命令用於實時列印出 Redis 伺服器接收到的命令,調試用。

redis 127.0.0.1:6379> MONITOR 
OK
1410855382.370791 [0 127.0.0.1:60581] "info"
1410855404.062722 [0 127.0.0.1:60581] "get" "a"

慢查詢

Redis原生提供慢查詢統計功能,執行slowlog get{n}命令可以獲取最近的n條慢查詢命令,默認對於執行超過10毫秒的命令都會記錄到一個定長隊列中,線上實例建議設置為1毫秒便於及時發現毫秒級以上的命令。慢查詢隊列長度默認128,可適當調大。

Redis客戶端執行一條命令分為4個部分:發送命令;命令排隊;命令執行;返回結果。慢查詢只統計命令執行這一步的時間,所以沒有慢查詢並不代表客戶端沒有超時問題。

Redis提供了slowlog-log-slower-than(設置慢查詢閾值,單位為微秒)和slowlog-max-len(慢查詢隊列大小)配置慢查詢參數。

相關命令:

showlog get n //獲取慢查詢日誌
slowlog len //慢查詢日誌隊列當前長度
slowlog reset //重置,清理列表

慢查詢解決方案:

  1. 修改為低時間複雜度的命令,如hgetall改為hmget等,禁用keys、sort等命令。
  2. 調整大對象:縮減大對象數據或把大對象拆分為多個小對象,防止一次命令操作過多的數據。

pipeline

redis客戶端執行一條命令分4個過程: 發送命令-〉命令排隊-〉命令執行-〉返回結果。使用Pipeline可以批量請求,批量返回結果,執行速度比逐條執行要快。

使用pipeline組裝的命令個數不能太多,不然數據量過大,增加客戶端的等待時間,還可能造成網路阻塞,可以將大量命令的拆分多個小的pipeline命令完成。

原生批命令(mset, mget)與Pipeline對比:

  1. 原生批命令是原子性,pipeline是非原子性。pipeline命令中途異常退出,之前執行成功的命令不會回滾。

  2. 原生批命令只有一個命令, 但pipeline支援多命令。

數據一致性

快取和DB之間怎麼保證數據一致性:
讀操作:先讀快取,快取沒有的話讀DB,然後取出數據放入快取,最後響應數據
寫操作:先刪除快取,再更新DB
為什麼是刪除快取而不是更新快取呢?

  1. 執行緒安全問題。同時有請求A和請求B進行更新操作,那麼會出現(1)執行緒A更新了快取(2)執行緒B更新了快取(3)執行緒B更新了資料庫(4)執行緒A更新了資料庫,由於網路等原因,請求B先更新資料庫,這就導致快取和資料庫不一致的問題。
  2. 如果業務需求寫資料庫場景比較多,而讀數據場景比較少,採用這種方案就會導致,數據壓根還沒讀到,快取就被頻繁的更新,浪費性能。
  3. 如果你寫入資料庫的值,並不是直接寫入快取的,而是要經過一系列複雜的計算再寫入快取。那麼,每次寫入資料庫後,都再次計算寫入快取的值,無疑是浪費性能的。

先刪除快取,再更新DB,同樣也有問題。假如A先刪除了快取,但還沒更新DB,這時B過來請求數據,發現快取沒有,去請求DB拿到舊數據,然後再寫到快取,等A更新完了DB之後就會出現快取和DB數據不一致的情況了。

解決方法:採用延時雙刪策略。更新完資料庫之後,延時一段時間,再次刪除快取,確保可以刪除讀請求造成的快取臟數據。評估項目的讀數據業務邏輯的耗時。然後寫數據的休眠時間則在讀數據業務邏輯的耗時基礎上,加幾百ms即可。

public void write(String key,Object data){
    redis.delKey(key);
    db.updateData(data);
    Thread.sleep(1000);//確保讀請求結束,寫請求可以刪除讀請求造成的快取臟數據
    redis.delKey(key);
}

可以將第二次刪除作為非同步的。自己起一個執行緒,非同步刪除。這樣,寫的請求就不用沉睡一段時間後了,加大吞吐量。

當刪快取失敗時,也會就出現數據不一致的情況。

解決方法:

圖片來源://tech.it168.com