目錄結構如下:

簡介

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地址:https://github.com/Tyson0314/java-books

如果github訪問不了,可以訪問gitee倉庫。

gitee地址:https://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);
}

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

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

解決方法:

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