Redis-技術彙總

Redis

Redis-優勢總結

1 性能高,讀每秒是11w,寫每秒是8w

2 豐富的數據結構,支持string,list,set,hash,sortedset

3 原子性操作,要不全部成功,要不全部失敗

4 發佈與訂閱,完成類似隊列功能

5 分佈式鎖的內在支持

6 高可用,高性能,支持集羣,支持哨兵,支持讀寫分離

Redis-使用場景

  1. 數據緩存(商品數據、新聞、熱點數據)

  2. 單點登錄

  3. 秒殺、搶購

  4. 網站訪問排名,排行榜

  5. 應用的模塊開發

Redis-RESP協議

Redis 的客戶端和服務端之間採取了一種獨立名爲 RESP(REdis Serialization Protocol) 的協議,它的特點是容易實現和解析快,可讀性強

在 RESP 中, 一些數據的類型通過它的第一個字節進行判斷:

  • 單行回覆:回覆的第一個字節是 “+”

  • 錯誤信息:回覆的第一個字節是 “-”

  • 整形數字:回覆的第一個字節是 “:”

  • 多行字符串:回覆的第一個字節是 “$”

  • 數組:回覆的第一個字節是 “*”

舉例

比如使用set命令, SET simpleKey simpleValue,

3 代表有三個字符串

9 代表有九個字符串

11 代表有十一個字符串

\\r\\n代表空格和換行

*3\\r\\n$3\\r\\nSET\\r\\n$9\\r\\nsimpleKey\\r\\n$11\\r\\nsimpleValue\\r\\n"

使用get命令.GET simpleKey

“*2\\r\\n$3\\r\\nGET\\r\\n$9\\r\\nsimpleKey\\r\\n”

2 代表命令有兩個字符串

9 代表KEY的長度是9

Redis-數據結構

字符串類型

常用命令
  • get,獲取指定的值

  • set,設置指定key對應的value

  • del,刪除指定的key

  • incr,原子操作+1

  • decr,原子操作-1

  • incrby,將key所存儲的值加上增量返回增加之後的值

  • decrby,將key所存儲的值減去decrment,返回減去之後的值

使用場景
緩存功能

字符串最經典的使用場景,redis最爲緩存層,Mysql作爲儲存層,絕大部分請求數據都是redis中獲取,由於redis具有支撐高併發特性,所以緩存通常能起到加速讀寫和降低 後端壓力的作用。

計數器

許多運用都會使用redis作爲計數的基礎工具,他可以實現快速計數、查詢緩存的功能,同時數據可以一步落地到其他的數據源。

如:視頻播放數系統就是使用redis作爲視頻播放數計數的基礎組件。

共享session

出於負載均衡的考慮,分佈式服務會將用戶信息的訪問均衡到不同服務器上,

用戶刷新一次訪問可能會需要重新登錄,爲避免這個問題可以用redis將用戶session集中管理,

在這種模式下只要保證redis的高可用和擴展性的,每次獲取用戶更新或查詢登錄信息都直接從redis中集中獲取。

限速

處於安全考慮,每次進行登錄時讓用戶輸入手機驗證碼,爲了短信接口不被頻繁訪問,會限制用戶每分鐘獲取驗證碼的頻率

散列類型

鍵值對集合,即編程語言中的Map類型

常用命令
  • hset,在散列裏面關聯起指定的鍵值對

  • hget,獲取指定散列鍵的值

  • hgetall, 獲取散列包含的所有鍵值對

  • hdel, 如果給定鍵存在於散列裏面,那麼移除這個鍵

使用場景

存儲、讀取、修改用戶屬性

列表類型

一個列表可以有序地存儲多個字符串,並且列表裏的元素是可以重複的

常用命令
  • LPUSH,將元素推入列表的左端

  • RPUSH,將元素推入列表的右端

  • LPOP,從列表左端彈出元素

  • RPOP,從列表右端彈出元素

  • LINDEX,獲取列表在給定位置上的一個元素

  • LRANGE,獲取列表在給定範圍上的所有元素

常用命令

Lpush

在key對應list的頭部添加字符串元素

±---------------------------------------------+
| 127.0.0.1:6379> lpush address “Shang Hai” |
| |
| (integer) 1 |
| |
| 127.0.0.1:6379> lpush address huangpu |
| |
| (integer) 2 |
| |
| 127.0.0.1:6379> lrange address 0 -1 |
| |
| 1) “huangpu” |
| |
| 2) “Shang Hai” |
±---------------------------------------------+

Rpush

在key對應list的尾部添加字符串元素

±----------------------------------------------+
| 127.0.0.1:6379> rpush address2 “Shang Hai” |
| |
| (integer) 1 |
| |
| 127.0.0.1:6379> rpush address2 “huangpu” |
| |
| (integer) 2 |
| |
| 127.0.0.1:6379> lrange address2 0 -1 |
| |
| 1) “Shang Hai” |
| |
| 2) “huangpu” |
±----------------------------------------------+

Lrem

從key對應list中刪除n個和value相同的元素

±-----------------------------------------+
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “pink” |
| |
| 2) “red” |
| |
| 3) “red” |
| |
| 4) “purple” |
| |
| 5) “red” |
| |
| 6) “yellow” |
| |
| 127.0.0.1:6379> lrem myColour 1 “red” |
| |
| (integer) 1 |
| |
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “pink” |
| |
| 2) “red” |
| |
| 3) “purple” |
| |
| 4) “red” |
| |
| 5) “yellow” |
±-----------------------------------------+

Ltrim

保留指定key的值範圍內的數據。即保留下標指定範圍的field,其他的被刪除。(用法:ltrim list鏈表名稱 位置索引1 位置索引2) 保留位置索引1 到位置索引2的元素,其餘全部刪除

±--------------------------------------+
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “yellow” |
| |
| 2) “purple” |
| |
| 3) “pink” |
| |
| 4) “red” |
| |
| 127.0.0.1:6379> ltrim myColour 2 -1 |
| |
| OK |
| |
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “pink” |
| |
| 2) “red” |
±--------------------------------------+

Lpop

從list的頭部刪除元素,並返回刪除元素。

±--------------------------------------+
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “pink” |
| |
| 2) “red” |
| |
| 127.0.0.1:6379> lpop myColour |
| |
| “pink” |
| |
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “red” |
±--------------------------------------+

Lindex

返回名稱爲key的list中index位置的元素,元素位置索引號從0開始

±--------------------------------------+
| 127.0.0.1:6379> lrange myColour 0 -1 |
| |
| 1) “red” |
| |
| 2) “black” |
| |
| 3) “blue” |
| |
| 127.0.0.1:6379> lindex myColour 0 |
| |
| “red” |
| |
| 127.0.0.1:6379> lindex myColour 1 |
| |
| “black” |
±--------------------------------------+

使用場景

1,最新消息排行等功能(比如朋友圈的時間線)

2,消息隊列

集合類型

集合類型也是用來保存多個字符串的元素,但和列表不同的是集合中不允許有重複的元素,並且集合中的元素是

無序的,不能通過索引下標獲取元素

常用命令
  • SADD,將元素添加到集合成功添加返回1,如果返回0則表示集合中已經有這個元素了

  • SRE,M從集合裏面移除元素 存在返回1,不存在返回0

  • SISMEMBER,快速地檢查一個元素是否已經存在於集合中

  • SMEMBERS,獲取集合包含的所有元素

使用場景

1、共同好友

2、利用唯一性,統計訪問網站的所有獨立ip

3、好友推薦時,根據tag求交集,大於某個閾值就可以推薦

有序類型

有序集合的鍵被成爲成員,每個成員都是各不相同的。有序集合的值被成爲分值,分值必須爲浮點數。

常用命令
  • ZADD,將一個帶有給定分值的成員添加到有序集合裏面

  • ZRANGE,根據元素在有序排列中所處的位置,從有序集合裏面獲取多個元素

  • ZRANGEBYSCORE,獲取有序集合在給定分值範圍內的所有元素

  • ZREM,如果給定成員存在於有序集合,那麼移除這個成員

  • ZSCORE,返回有序集合中,成員的分數值

  • ZRANGE, 通過索引區間返回有序集合成指定區間內的成員

  • ZRANK, 返回有序集合中指定成員的索引

  • ZINCRBY,有序集合中對指定成員的分數加上增量 increment

備註: 它採用了複合結構, 字典維護了name=>score的映射表, 而跳躍表則維護了按score排序的列表. 按name和按score的範圍查詢都天然支持.

命令詳解

Zadd

向名稱爲key的zset中添加元素member,score用於排序。如果該元素存在,則更新其順序。(用法:zadd 有序集合 順序編號 元素值)

±-----------------------------------------------+
| 127.0.0.1:6379> zadd zset1 1 two\ |
| (integer) 1\ |
| 127.0.0.1:6379> zadd zset1 2 one\ |
| (integer) 1\ |
| 127.0.0.1:6379> zadd zset1 3 seven\ |
| (integer) 1 |
| |
| 127.0.0.1:6379> zrange zset1 0 -1 \ |
| 1) “two”\ |
| 2) “one”\ |
| 3) “seven”\ |
| 127.0.0.1:6379> zrange zset1 0 -1 withscores\ |
| 1) “two”\ |
| 2) “1”\ |
| 3) “one”\ |
| 4) “2”\ |
| 5) “seven”\ |
| 6) “2” |
±-----------------------------------------------+

Zrem

刪除名稱爲key的zset中的元素。(用法:zrem 有序集合 要刪除的元素值)


127.0.0.1:6379> zrange zset1 0 -1 withscores\

  1. “two”\
  2. “1”\
  3. “one”\
  4. “2”\
  5. “seven”\
  6. “2”
    127.0.0.1:6379> zrem zset1 one
    (integer) 1
    127.0.0.1:6379> zrange zset1 0 -1 withscores\
  7. “two”\
  8. “1”\
  9. “seven”\
  10. “2”

Zincrby

如果在名稱爲key的zset中已經存在元素member,則該元素的score增加increment,否則向該集合中添加該元素,其score的值爲increment.即對元素的順序號進行增加或減少操作


127.0.0.1:6379> zrange zset1 0 -1 withscores\

  1. “two”\
  2. “1”\
  3. “seven”\
  4. “2”
    127.0.0.1:6379> zincrby zset1 5 seven
    “7”
    127.0.0.1:6379> zrange zset1 0 -1 withscores\
  5. “two”\
  6. “1”\
  7. “seven”\
  8. “7”

Zrank

返回名稱爲key的member元素的排名(按score從小到大排序)即下標


127.0.0.1:6379> zrange zset1 0 -1 withscores\

  1. “two”\
  2. “1”\
  3. “seven”\
  4. “7”
    127.0.0.1:6379> zrank zset1 seven
    (integer) 1

Zrevrank

返回名稱爲key的member元素的排名(按score從大到小排序)即下標


127.0.0.1:6379> zrange zset1 0 -1 withscores\

  1. “two”\
  2. “1”\
  3. “seven”\
  4. “7”
    127.0.0.1:6379> zrevrank zset1 seven
    (integer) 0

Zrange

顯示集合中指定下標的元素值(按score從小到大排序)。如果需要顯示元素的順序編號,帶上withscores


127.0.0.1:6379> zrange zset1 0 -1 withscores\

  1. “two”\
  2. “1”\
  3. “five”\
  4. “2”\
  5. “one”\
  6. “3”\
  7. “seven”\
  8. “7”

Zrevrange

顯示集合中指定下標的元素值(按score從大到小排序)。如果需要顯示元素的順序編號,帶上withscores


127.0.0.1:6379> zrevrange zset1 0 -1 withscores\

  1. “seven”\
  2. “7”\
  3. “one”\
  4. “3”\
  5. “five”\
  6. “2”\
  7. “two”\
  8. “1”

Zcount

返回集合中score在給定區間的數量


127.0.0.1:6379> zcount zset1 2 7
(integer) 3


Zcard

返回集合中元素個數


127.0.0.1:6379> zrange zset1 0 -1\

  1. “two”\
  2. “five”\
  3. “one”\
  4. “seven”
    127.0.0.1:6379> zcard zset1
    (integer) 4

Zremrangebyrank

刪除集合中排名在給定區間的元素。(按索引下標刪除)


127.0.0.1:6379> zrange zset1 0 -1 withscores\

  1. “two”\
  2. “1”\
  3. “five”\
  4. “2”\
  5. “one”\
  6. “3”\
  7. “seven”\
  8. “7”
    127.0.0.1:6379> zremrangebyrank zset1 3 3
    (integer) 1
    127.0.0.1:6379> zrange zset1 0 -1 withscores\
  9. “two”\
  10. “1”\
  11. “five”\
  12. “2”\
  13. “one”\
  14. “3”

Zremrangebyscore

刪除集合中score在給定區間的元素(按順序score值來刪除)


127.0.0.1:6379> zrange zset1 0 -1 withscores\

  1. “two”\
  2. “1”\
  3. “five”\
  4. “2”\
  5. “one”\
  6. “3”\
  7. “seven”\
  8. “7”
    127.0.0.1:6379> zremrangebyscore zset1 5 7
    (integer) 1
    127.0.0.1:6379> zrange zset1 0 -1 withscores\
  9. “two”\
  10. “1”\
  11. “five”\
  12. “2”\
  13. “one”\
  14. “3”

使用場景

1、排行榜

2、帶權重的消息隊列

Redis-安裝部署

安裝部署

  1. 下載redis安裝包

  2. tar -zxvf 安裝包

  3. 在redis目錄下 執行 make

  4. 可以通過make test測試編譯狀態

  5. make install [prefix=/path]完成安裝

啓動/停止

./redis-server …/redis.conf

./redis-cli shutdown

以後臺進程的方式啓動,修改redis.conf daemonize =yes

連接到redis的命令

./redis-cli -h 127.0.0.1 -p 6379

其它命令

redis-benchmark 性能測試的工具

redis-check-aof aof文件進行檢測的工具

redis-check-dump rdb文件檢查工具

redis-sentinel sentinel 服務器配置

slowlog 慢日誌相關信息查看

redis.conf常用配置項

在這裏插入圖片描述

Redis-主從複製原理

Redis 的複製(replication)功能允許用戶根據一個 Redis 服務器來創建任意多個該服務器的複製品,其中被複制的服務器爲主服務器(master),而通過複製創建出來的服務器複製品則爲從服務器(slave)。 只要主從服務器之間的網絡連接正常,主從服務器兩者會具有相同的數據,主服務器就會一直將發生在自己身上的數據更新同步 給從服務器,從而一直保證主從服務器的數據相同


在這裏插入圖片描述

配置步驟

第1步:cp reids.conf redis2.conf

第2步:Vim redis2.conf(slave)

第3步:slaveof 192.168.0.12 6379(master的地址)

第4步:Vim redis.conf (master)

第5步:bind 0.0.0.0 #無ip 都可以訪問

第6步:./redis-server …/redis.conf #master

第7步./redis-server …/redis.2conf #slave

判斷角色

是否成功set get請求來判斷或執行info命令role

判斷是否成功

1、master客戶端set值,slave客戶端能不能獲取到

2、config get ‘slaveof*’

特點

1、master/slave角色

2、master/slave數據相同

3、降低master讀壓力在轉交從庫

缺點

無法保證高可用

沒有解決master寫的壓力

複製階段

全量複製

如果是slave node第一次連接master node,那麼會觸發一次full resynchronization(全量複製)
開始full resynchronization的時候,master會啓動一個後臺線程,開始生成一份RDB快照文件,同時還會將從客戶端收到的所有寫命令緩存在內存中。RDB文件生成完畢之後,master會將這個RDB發送給slave,slave會先寫入本地磁盤,然後再從本地磁盤加載到內存中。然後master會將內存中緩存的寫命令發送給slave,slave也會同步這些數據

無磁盤化複製

master在內存中直接創建rdb,然後發送給slave,不會在自己本地落地磁盤了
repl-diskless-sync
repl-diskless-sync-delay,等待一定時長再開始複製,因爲要等更多slave重新連接過來

增量複製(斷續複製)

如果主從複製過程中,網絡連接斷掉了,那麼可以接着上次複製的地方,繼續複製下去,而不是從頭開始複製一份
master node會在內存中創建一個backlog,master和slave都會保存一個replica offset還有一個master id,offset就是保存在backlog中的。如果master和slave網絡連接斷掉了,slave會讓master從上次的replica offset開始繼續複製
但是如果沒有找到對應的offset,那麼就會執行一次resynchronization

過期key處理

slave不會過期key,只會等待master過期key。如果master過期了一個key,或者通過LRU淘汰了一個key,那麼會模擬一條del命令發送給slave。

Redis-哨兵架構

Redis sentinel是一個分佈式系統中監控redis主從服務器,並在主服務器下線時自動進行故障轉移。其中三個特性:

監控(Monitoring): Sentinel 會不斷地檢查你的主服務器和從服務器是否運作正常。

提醒(Notification): 當被監控的某個 Redis 服務器出現問題時, Sentinel 可以通過 API 向管理員或者其他應用程序發送通知。

自動故障遷移(Automatic failover): 當一個主服務器不能正常工作時, Sentinel 會開始一次自動故障遷移操作。


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9DmqFNWi-1578369080148)(media/image2.png)]{width="6.0in" height="3.6819444444444445in"}

sdown和odown轉換機制

sdown和odown兩種失敗狀態

sdown是主觀宕機,就一個哨兵如果自己覺得一個master宕機了,那麼就是主觀宕機

odown是客觀宕機,如果quorum數量的哨兵都覺得一個master宕機了,那麼就是客觀宕機

sdown達成的條件很簡單,如果一個哨兵ping一個master,超過了is-master-down-after-milliseconds指定的毫秒數之後,就主觀認爲master宕機

sdown到odown轉換的條件很簡單,如果一個哨兵在指定時間內,收到了quorum指定數量的其他哨兵也認爲那個master是sdown了,那麼就認爲是odown了,客觀認爲master宕機

哨兵集羣的自動發現機制

哨兵互相之間的發現,是通過redis的pub/sub系統實現的,每個哨兵都會往__sentinel__:hello這個channel裏發送一個消息,這時候所有其他哨兵都可以消費到這個消息,並感知到其他的哨兵的存在

每隔兩秒鐘,每個哨兵都會往自己監控的某個master+slaves對應的__sentinel__:hello channel裏發送一個消息,內容是自己的host、ip和runid還有對這個master的監控配置

每個哨兵也會去監聽自己監控的每個master+slaves對應的__sentinel__:hello channel,然後去感知到同樣在監聽這個master+slaves的其他哨兵的存在

每個哨兵還會跟其他哨兵交換對master的監控配置,互相進行監控配置的同步

slave配置的自動糾正


哨兵會負責自動糾正slave的一些配置,比如slave如果要成爲潛在的master候選人,哨兵會確保slave在複製現有master的數據; 如果slave連接到了一個錯誤的master上,比如故障轉移之後,那麼哨兵會確保它們連接到正確的master上

Slave選舉master選舉算法

如果一個master被認爲odown了,而且majority哨兵都允許了主備切換,那麼某個哨兵就會執行主備切換操作,此時首先要選舉一個slave來

會考慮slave的一些信息

(1)跟master斷開連接的時長

(2)slave優先級

(3)複製offset

(4)run id

如果一個slave跟master斷開連接已經超過了down-after-milliseconds的10倍,外加master宕機的時長,那麼slave就被認爲不適合選舉爲master

(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state

接下來會對slave進行排序

(1)按照slave優先級進行排序,slave priority越低,優先級就越高
(2)如果slave priority相同,那麼看replica offset,哪個slave複製了越多的數據,offset越靠後,優先級就越高
(3)如果上面兩個條件都相同,那麼選擇一個run id比較小的那個slave
備註:優先級>offset>runid

quorum和majority

每次一個哨兵要做主備切換,首先需要quorum數量的哨兵認爲odown,然後選舉出一個哨兵來做切換,這個哨兵還得得到majority哨兵的授權,才能正式執行切換

  1. (quorum:確認odown的最少的哨兵數量, majority:授權進行主從切換的最少的哨兵數量)

如果quorum < majority,比如5個哨兵,majority就是3,quorum設置爲2,那麼就3個哨兵授權就可以執行切換

但是如果quorum >= majority,那麼必須quorum數量的哨兵都授權,比如5個哨兵,quorum是5,那麼必須5個哨兵都同意授權,才能執行切換

configuration epoch

哨兵會對一套redis master+slave進行監控,有相應的監控的配置

執行切換的那個哨兵,會從要切換到的新master(salve->master)那裏得到一個configuration epoch,這就是一個version號,每次切換的version號都必須是唯一的

如果第一個選舉出的哨兵切換失敗了,那麼其他哨兵,會等待failover-timeout時間,然後接替繼續執行切換,此時會重新獲取一個新的configuration epoch,作爲新的version號

configuraiton 傳播

哨兵完成切換之後,會在自己本地更新生成最新的master配置,然後同步給其他的哨兵,就是通過之前說的pub/sub消息機制

這裏之前的version號就很重要了,因爲各種消息都是通過一個channel去發佈和監聽的,所以一個哨兵完成一次新的切換之後,新的master配置是跟着新的version號的

其他的哨兵都是根據版本號的大小來更新自己的master配置的

部署哨兵

前提:先搭好一主兩從redis的主從複製,和之前的主從複製搭建一樣,再單獨部署一個redis,修改配置文件sentinel.conf,默認只需要修改一行.

sentinel monitor mymaster 192.168.1.2 6379 2 只需要修改這一行
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

  • down-after-milliseconds,超過多少毫秒跟一個redis實例斷了連接,哨兵就可能認爲這個redis實例掛了

  • parallel-syncs,新的master別切換之後,同時有多少個slave被切換到去連接新master,重新做同步,數字越低,花費的時間越多

  • failover-timeout,執行故障轉移的timeout超時時長

啓動哨兵服務.

./redis-server conf/redis6379.conf &

之後可以把主節點的進程給殺死,然後觀察是否有從節點,被選舉爲主節點.

哨兵優點

1 保證高可用

2 監控各個節點

3 自動故障轉移

哨兵缺點

1 主從模式,切換的時候需要一定的時候,可能會存在數據丟失

2 沒有解決Master寫的壓力

3 可能會產生Split-Brain

Redis-Twemproxy架構

Twemproxy是一個Twitter開源的一個redis和memcache快速/輕量級代理服務器;Twemproxy是一個快速的單線程代理程序,支持Memcached ASCII協議和redis協議


media/image3.png)]{width="6.0in" height="3.3722222222222222in"}

特點

1、多種hash算法:MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins

2、支持失敗節點自動刪除

3、後端Sharding分片邏輯對業務透明,業務方的讀寫方式和操作單個Redis一致

缺點

**1.**增加了新的proxy,需要維護其高可用。

2.failover邏輯需要自己實現,其本身不能支持故障的自動轉移

3.無法支持平滑的擴容或者縮容。

(不自動故障轉移,不支持動態擴容和縮容)

Redis-Codis架構

Codis 是一個分佈式 [Redis]{.underline} 解決方案, 對於上層的應用來說, 連接到 Codis Proxy 和連接原生的 Redis Server 沒有明顯的區別 (有一些命令不支持), 上層應用可以像使用單機的 Redis 一樣使用, Codis 底層會處理請求的轉發, 不停機的數據遷移等工作, 所有後邊的一切事情, 對於前面的客戶端來說是透明的, 可以簡單的認爲後邊連接的是一個內存無限大的 Redis 服務


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5pTlc2zN-1578369080150)(media/image4.png)]{width="6.492810586176728in" height="3.0833333333333335in"}

Codis VS Twemproxy

  • codis支持動態水平擴展,對client完全透明不影響服務的情況下可以完成增減redis實例的操作;

  • codis是用go語言寫的並支持多線程,twemproxy用C並只用單線程。 後者又意味着:codis在多核機器上的性能會好於twemproxy;codis的最壞響應時間可能會因爲GC的STW而變大,不過go1.5發佈後會顯著降低STW的時間;如果只用一個CPU的話go語言的性能不如C,因此在一些短連接而非長連接的場景中,整個系統的瓶頸可能變成accept新tcp連接的速度,這時codis的性能可能會差於twemproxy。

Codis VS Redis cluster

redis cluster基於smart client和無中心的設計,client必須按key的哈希將請求直接發送到對應的節點。這意味着:使用官方cluster必須要等對應語言的redis driver對cluster支持的開發和不斷成熟;client不能直接像單機一樣使用pipeline來提高效率,想同時執行多個請求來提速必須在client端自行實現異步邏輯。 而codis因其有中心節點、基於proxy的設計,對client來說可以像對單機redis一樣去操作proxy

Codis VS 組件

Jodis-Client

Codis-Proxy

  • 客戶端連接的 Redis 代理服務, codis-proxy 本身實現了 Redis 協議, 表現得和一個原生的 Redis 沒什麼區別

Codis-Group

  • 每個Group包括1個Redis Master及至少1個Redis Slave

Codis-Dashboard

  • 支持可視化儀表盤監控

Codis-Server

  • 就是就是具體的redis-master,redis-slave

Codis-HA

  • 通過codis開放的api實現自動切換主從的工具。該工具會在檢測到master掛掉的時候將其下線並選擇其中一個slave提升爲master繼續提供服務

Codis-Config

  • Codis 的管理工具, 支持包括, 添加/刪除 Redis 節點, 添加/刪除 Proxy 節點, 發起數據遷移等操作

Codis VS 集羣部署

角色分類

在這裏插入圖片描述

部署步驟

codis的部署步驟還是比較多的,大概有10步,不過按照一步步的步驟來就好,其中ha機制是要單獨部署的.

安裝zookeeper

在這裏插入圖片描述

安裝GO
wget https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz
tar -zxvf go1.4.1.linux-amd64.tar.gz
mv go /usr/local/
cd /usr/local/go/src/
bash all.bash
cat >> ~/.bashrc << _bashrc_export
export GOROOT=/usr/local/go
export PATH=\$PATH:\$GOROOT/bin
export GOARCH=amd64
export GOOS=linux
_bashrc_export
source ~/.bashrc

下載並編譯

codis(codis-config、codis-proxy、codis-server所在的機器)

mkdir /data/go
export GOPATH=/data/go
/usr/local/go/bin/go get github.com/wandoulabs/codis
cd  /data/go/src/github.com/wandoulabs/codis/
./bootstrap.sh
make gotest

啓動 dashboard

(codis-config上操作)

cat /etc/codis/config_10.ini ##撰寫配置文件
zk=10.10.0.47:2181,10.10.0.48:2181,10.10.1.76:2181
product=zh_news
proxy_id=codis-proxy_10
net_timeout=5000
proto=tcp4
dashboard_addr=10.10.32.10:18087
1	cd /data/go/src/github.com/wandoulabs/codis/ &&  ./bin/codis-config -c /etc/codis/config_10.ini  dashboard &


初始化 slots

(codis-config上操作)


cd /data/go/src/github.com/wandoulabs/codis/ &&  ./bin/codis-config -c /etc/codis/config_10.ini slot init
啓動 Codis Redis ,

和官方的Redis Server參數一樣(codis-server上操作)

cd /data/go/src/github.com/wandoulabs/codis/ && ./bin/codis-server /etc/redis_6379.conf &
添加 Redis Server Group

每一個 Server Group 作爲一個 Redis 服務器組存在, 只允許有一個 master, 可以有多個 slave, group id 僅支持大於等於1的整數(codis-config上操作)

cd /data/go/src/github.com/wandoulabs/codis/
./bin/codis-config -c /etc/codis/config_10.ini server add 1 10.10.32.42:6379 master
./bin/codis-config -c /etc/codis/config_10.ini server add 1 10.10.32.43:6380 slave
./bin/codis-config -c /etc/codis/config_10.ini server add 2 10.10.32.43:6379 master
./bin/codis-config -c /etc/codis/config_10.ini server add 2 10.10.32.44:6380 slave
./bin/codis-config -c /etc/codis/config_10.ini server add 3 10.10.32.44:6379 master
./bin/codis-config -c /etc/codis/config_10.ini server add 3 10.10.32.42:6380 slave

設置 server group 服務的 slot 範圍

Codis 採用 Pre-sharding 的技術來實現數據的分片, 默認分成 1024 個 slots (0-1023), 對於每個key來說, 通過以下公式確定所屬的 Slot Id : SlotId = crc32(key) % 1024 每一個 slot 都會有一個特定的 server group id 來表示這個 slot 的數據由哪個 server group 來提供.(codis-config上操作)

cd /data/go/src/github.com/wandoulabs/codis/
./bin/codis-config -c /etc/codis/config_10.ini slot range-set 0 300 1 online
./bin/codis-config -c /etc/codis/config_10.ini slot range-set 301 700 2 online
./bin/codis-config -c /etc/codis/config_10.ini slot range-set 701 1023 3 online

啓動 codis-proxy

(codis-proxy上操作)

cat /etc/codis/config_10.ini ##撰寫配置文件
zk=10.10.0.47:2181,10.10.0.48:2181,10.10.1.76:2181
product=zh_news
proxy_id=codis-proxy_10  ##10.10.32.49上改成codis-proxy_49,多個proxy,proxy_id 需要唯一
net_timeout=5000
proto=tcp4
dashboard_addr=10.10.32.10:18087
1
2	cd /data/go/src/github.com/wandoulabs/codis/ &&  ./bin/codis-proxy  -c /etc/codis/config_10.ini -L /data/log/codis-proxy_10.log  --cpu=4 --addr=0.0.0.0:19000 --http-addr=0.0.0.0:11000 &
cd /data/go/src/github.com/wandoulabs/codis/ &&  ./bin/codis-proxy  -c /etc/codis/config_49.ini -L /data/log/codis-proxy_49.log  --cpu=4 --addr=0.0.0.0:19000 --http-addr=0.0.0.0:11000 &

OK,整個集羣已經搭建成功了,訪問[http://10.10.32.10:18087/admin]{.underline},可以看一下效果

安裝codis-server的HA

odis-ha實現codis-server的主從切換,codis-server主庫掛了會提升一個從庫爲主庫,從庫掛了會設置這個從庫從集羣下線

export GOPATH=/data/go
/usr/local/go/bin/go get github.com/ngaut/codis-ha
cd  /data/go/src/github.com/ngaut/codis-ha
go build
cp codis-ha /data/go/src/github.com/wandoulabs/codis/bin/
使用方法:
codis-ha --codis-config=dashboard地址:18087 --productName=集羣項目名稱

使用supervisord管理codis-ha進程

yum -y install supervisord
/etc/supervisord.conf中添加如下內容:
[program:codis-ha]
autorestart = True
stopwaitsecs = 10
startsecs = 1
stopsignal = QUIT
command = /data/go/src/github.com/wandoulabs/codis/bin/codis-ha --codis-config=10.10.32.17:18087 --productName=zh_news
user = root
startretries = 3
autostart = True
exitcodes = 0,2

啓動supervisord服務

/etc/init.d/supervisord start
chkconfig supervisord  on


ps -ef |grep codis-ha 你回發現codis-ha進程已經啓動,這個時候你去停掉一個codis-server的master,看看slave會不會提升爲master

Codis VS 缺點

1 不支持隨redis的升級而升級

2 codis集羣內部通訊是通過主機名的,如果主機名沒有做域名解析那dashboard是通過主機名訪問不到proxy的http-addr地址的,這會導致從web界面上看不到 OP/s的數據

3 codis proxy 不支持熱重啓

RedisCluster-Slot架構

在redis3.0以後的版本才支持該功能

Redis Cluster中,Sharding採用slot(槽)的概念,一共分成16384個槽,這有點兒類似前面講的pre sharding思路。對於每個進入Redis的鍵值對,根據key進行散列,分配到這16384個slot中的某一箇中。使用的hash算法也比較簡單,就是CRC16後16384取模。Redis集羣中的每個node(節點)負責分攤這16384個slot中的一部分,也就是說,每個slot都對應一個node負責處理。當動態添加或減少node節點時,需要將16384個槽做個再分配,槽中的鍵值也要遷移。當然,這一過程,在目前實現中,還處於半自動狀態,需要人工介入。Redis集羣,要保證16384個槽對應的node都正常工作,如果某個node發生故障,那它負責的slots也就失效,整個集羣將不能工作。爲了增加集羣的可訪問性,官方推薦的方案是將node配置成主從結構,即一個master主節點,掛n個slave從節點。這時,如果主節點失效,Redis Cluster會根據選舉算法從slave節點中選擇一個上升爲主節點,整個集羣繼續對外提供服務。這非常類似服務器節點通過Sentinel監控架構成主從結構,只是Redis Cluster本身提供了故障轉移容錯的能力。

Cluster注意點

1 槽的總數爲16384個

2 基於CRC16算法: 循環冗餘校驗碼,是信息系統中一種常見的檢錯碼

3 動態添加或者減少Node,需要人工介入

4 建議配置主從結構

Cluster 部署

環境準備
192.168.0.11
192.168.0.12
192.168.0.13
每臺服務器11從,共33從 
相關安裝包存儲路徑:/root/svr/

下載
wget http://download.redis.io/releases/redis-3.2.9.tar.gz
tar xvf redis-3.2.9.tar.gz
cd redis-3.2.9

安裝

make install PREFIX=/root/svr/redis-3.2.9 安裝

配置
cd /usr/local/redis-3.2.9
創建集羣配置文件夾:mkdir cluster-conf
cd cluster-conf
創建集羣端口文件夾:mkdir 7001 mkdir 7002
cd 7001
複製配置文件:cp /root/svr/redis-3.2.9/redis.conf ./
Redis的log及持久化文件建議存儲到磁盤空間較大的目錄,本次存儲路徑:/root/svr/redis-cluster/


修改配置文件:vi redis.conf

port 7001
logfile "/root/svr/redis-3.2.9/cluster-conf/7001/redis.log"
dir /root/svr/redis-cluster/7001/ #事先創建好
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
bind 0.0.0.0

複製redis.conf至7002並修改端口及存儲路徑

scp其他服務器

scp -r redis-3.2.9/ [email protected]:/root/svr/

啓動

/root/svr/redis-3.2.9/bin/redis-server /root/svr/redis-3.2.9/cluster-conf/7002/redis.conf &

創建集羣

./redis-trib.rb create --replicas 1 192.168.0.11:7001 192.168.0.12:7001 192.168.0.13:7001 192.168.0.11:7002 192.168.0.12:7002 192.168.0.13:7002

以上命令的意思就是讓 redis-trib 程序創建一個包含三個主節點和三個從節點的集羣。

命令的意義如下:

1、給定 redis-trib.rb 程序的命令是 create , 這表示我們希望創建一個新的集羣。

2、選項 --replicas 1 表示我們希望爲集羣中的每個主節點創建一個從節點(百分比 選舉master按先後順序)

查看
ps -ef |grep redis
或者netsta -tnlp |grep redis 
./redis-cli -c -p 7001
表示安裝成功了

其它命令
增加
./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000

從節點(masterid 和被加的節點)
./redis-trib.rb add-node --slave masterid 192.168.0.11:7002 
   
移除
./redis-trib del-node 127.0.0.1:7000 `<node-id>`

關閉服務:./redis-cli -h 192.168.0.11 -p 7001 shutdown

刪除:rm -rf /root/svr/redis-cluster/7001/*

注意點

1 集羣啓動腳本語言依賴於ruby,所以要安裝yum install rubygems

2 修改redis.conf裏面的bind爲0.0.0.0

Cluster 優勢

  • 無中心架構。

  • 數據按照slot存儲分佈在多個節點,節點間數據共享,可動態調整數據分佈。

  • 可擴展性,可線性擴展到1000個節點,節點可動態添加或刪除。

  • 高可用性,部分節點不可用時,集羣仍可用。通過增加Slave做standby數據副本,能夠實現故障自動failover,節點之間通過gossip協議交換狀態信息,用投票機制完成Slave到Master的角色提升。

  • 降低運維成本,提高系統的擴展性和可用性。

Cluster不足

  • 嚴重依賴外部Redis-Trib

  • 缺乏監控管理

  • 需要依賴Smart Client(連接維護, 緩存路由表, MultiOp和Pipeline支持)

  • Failover節點的檢測過慢,不如"中心節點ZooKeeper"及時

  • Gossip消息的開銷

  • 無法根據統計區分冷熱數據

  • Slave"冷備",不能緩解讀壓力

RedisCluster-架構彙總

直連型

直連型,又可以稱之爲經典型或者傳統型,是官方的默認使用方式,架構圖見圖6。這種使用方式的優缺點在上面的介紹中已經有所說明,這裏不再過多重複贅述。但值得一提的是,這種方式使用Redis Cluster需要依賴Smart Client,諸如連接維護、緩存路由表、MultiOp和Pipeline的支持都需要在Client上實現,而且很多語言的Client目前都還是沒有的

優點
無中心節點
數據按照Slot存儲分佈在多個Redis實例上
平滑的進行擴容/縮容節點
自動故障轉移(節點之間通過Gossip協議交換狀態信息,進行投票機制完成Slave到Master角色的提升)
降低運維成本,提高了系統的可擴展性和高可用性
 
缺點
嚴重依賴外部Redis-Trib
缺乏監控管理
需要依賴Smart Client(連接維護, 緩存路由表, MultiOp和Pipeline支持)
Failover節點的檢測過慢,不如“中心節點ZooKeeper”及時
Gossip消息的開銷
無法根據統計區分冷熱數據
Slave“冷備”,不能緩解讀壓力

Proxy型

在Redis Cluster還沒有那麼穩定的時候,很多公司都已經開始探索分佈式Redis的實現了,比如有基於Twemproxy或者Codis的實現


優點:

	後端Sharding邏輯對業務透明,業務方的讀寫方式和操作單個Redis一致;
	可以作爲Cache和Storage的Proxy,Proxy的邏輯和Redis資源層的邏輯是隔離的;
	Proxy層可以用來兼容那些目前還不支持的Clients。
缺點:

	結構複雜,運維成本高;
	可擴展性差,進行擴縮容都需要手動干預;
	failover邏輯需要自己實現,其本身不能支持故障的自動轉移;
	Proxy層多了一次轉發,性能有所損耗。

直連型+Proxy


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fwblxWd0-1578369080151)(media/image5.png)]{width="6.0in" height="3.423611111111111in"}

目前業界Smart Proxy的方案瞭解到的有基於Nginx Proxy和自研的,自研的如餓了麼開源部分功能的Corvus,優酷土豆是則通過Nginx來實現,滴滴也在展開基於這種方式的探索。選用Nginx Proxy主要是考慮到Nginx的高性能,包括異步非阻塞處理方式、高效的內存管理、和Redis一樣都是基於epoll事件驅動模式等優點。優酷土豆的Redis服務化就是採用這種結構。

優點:

	提供一套HTTP Restful接口,隔離底層資源,對客戶端完全透明,跨語言調用變得簡單;
	升級維護較爲容易,維護Redis Cluster,只需平滑升級Proxy;
	層次化存儲,底層存儲做冷熱異構存儲;
	權限控制,Proxy可以通過密鑰管理白名單,把一些不合法的請求都過濾掉,並且也可以對用戶請求的超大value進行控制和過濾;
	安全性,可以屏蔽掉一些危險命令,比如keys *、save、flushall等,當然這些也可以在Redis上進行設置;
	資源邏輯隔離,根據不同用戶的key加上前綴,來實現動態路由和資源隔離;
	監控埋點,對於不同的接口進行埋點監控。
缺點:

	Proxy層做了一次轉發,性能有所損耗;

	增加了運維成本和管理成本,需要對架構和Nginx Proxy的實現細節足夠了解,因爲Nginx Proxy在批量接口調用高併發下可能會瞬間向Redis Cluster發起幾百甚至上千的協程去訪問,導致Redis的連接數或系統負載的不穩定,進而影響集羣整體的穩定性。

自研型

目前都是通過Proxy+RedisCluster,這種結構,其中proxy一般會使用go語言進行開發或者使用高性能的nginx proxy框架開發

1.	用戶在ACL平臺申請集羣資源,如果申請成功返回祕鑰信息。
2.	用戶請求接口必須包含申請的祕鑰信息,請求至LVS服務器。
3.	LVS根據負載均衡策略將請求轉發至Nginx Proxy。
4.	Nginx Proxy首先會獲取祕鑰信息,然後根據祕鑰信息去ACL服務上獲取集羣的種子信息。(種子信息是集羣內任意幾臺IP:PORT節點)
5.	然後把祕鑰信息和對應的集羣種子信息緩存起來。並且第一次訪問會根據種子IP:PORT獲取集羣Slot對應節點的Mapping路由信息,進行緩存起來。最後根據Key計算SlotId,從緩存路由找到節點信息。
6.	把相應的K/V信息發送到對應的Redis節點上。
7.	Nginx Proxy定時(60s)上報請求接口埋點的QPS,RT,Err等信息到Open-Falcon平臺。
8.	Redis Cluster定時(60s)上報集羣相關指標的信息到Open-Falcon平臺。
備註:client(key)->lvs->nginx proxy->routing->redis->slots

部分細節處理
資源邏輯隔離

根據用戶Post數據獲取該用戶申請的NameSpace,然後以NameSpace作爲該用戶請求Key的前綴,從而達到不同用戶的不同NameSpace,進行邏輯資源隔離

重試策略

針對後端Redis節點出現Moved,Ask,Err,TimeOut等進行重試,重試次數可配置

過載保護

通過在Nginx Proxy Limit模塊進行限速,超過集羣的承載能力,進行過載保護。從而保證部分用戶可用,不至於壓垮服務器

監控管理

Nginx Proxy接入了Open-Falcon對系統級別,應用級別,業務級別進行監控和告警

主動Failover節點

由於Redis Cluster是通過Gossip通信, 超過半數以上Master節點通信(cluster-node-timeout)認爲當前Master節點宕機,才真的確認該節點宕機。判斷節點宕機時間過長,在Proxy層加入Raft算法,加快失效節點判定,主動Failover

批量請求優化

主要是針對像mget,mset這種批量的查詢或者插入進行了很多的優化,因爲默認的rediscluster對multiop和pipeline的支持有限,在這裏主要使用了nginx的特性進行很多方面的優化.

####### 子請求變爲協程(輕量級的線程)

優化前:
	a) 用戶請求mget(k1,k2)到Proxy
	b) Proxy根據k1,k2分別發起子請求subrequest1,subrequest2
	c) 子請求根據key計算slotid,然後去緩存路由表查找節點
	d) 子請求請求Redis Cluster的相關節點,然後響應返回給Proxy
	e) Proxy會合並所有的子請求返回的結果,然後進行解析包裝返回給用戶
優化後:
a) 用戶請求mget(k1,k2)到Proxy
b) Proxy根據k1,k2分別計算slotid, 然後去緩存路由表查找節點
c) Proxy發起多個協程coroutine1, coroutine2併發的請求Redis Cluster的相關節點
d) Proxy會合並多個協程返回的結果,然後進行解析包裝返回給用戶
 

####### 合併相同槽,批量執行命令,減少網絡開銷

優化前:
	a) 用戶請求mget(k1,k2,k3,k4) 到Proxy。
	b) Proxy會解析請求串,然後計算k1,k2,k3,k4所對應的slotid。
	c) Proxy會根據slotid去路由緩存中找到後端服務器的節點,併發的發起多個請求到後端服務器。
	d) 後端服務器返回結果給Proxy,然後Proxy進行解析獲取key對應的value。
	e) Proxy把key,value對應數據包裝返回給用戶。
優化後:
a) 用戶請求mget(k1,k2,k3,k4) 到Proxy。
b) Proxy會解析請求串,然後計算k1,k2,k3,k4所對應的slotid,然後把相同的slotid進行合併爲一次Pipeline請求。
c) Proxy會根據slotid去路由緩存中找到後端服務器的節點,併發的發起多個請求到後端服務器。
d) 後端服務器返回結果給Proxy,然後Proxy進行解析獲取key對應的value。
e) Proxy把key,value對應數據包裝返回給用戶。

####### 請求併發度的合理控制

優化前
	a) 用戶請求批量接口mset(200個key)(這裏先忽略合併相同槽的邏輯)
	b) Proxy會解析這200個key,會同時發起200個協程請求併發的去請求Redis Cluster。
	c) Proxy等待所有協程請求完成,然後合併所有協程請求的響應結果,進行解析,包裝返回給用戶。
優化後
a) 用戶請求批量接口mset(200個key)(這裏先忽略合併相同槽的邏輯)
b) Proxy會解析這200個key,進行分組。100個key爲一組,分批次進行併發請求。
c) Proxy先同時發起第一組100個協程(coroutine1, coroutine100)請求併發的去請求Redis Cluster。
d) Proxy等待所有協程請求完成,然後合併所有協程請求的響應結果。
e) Proxy然後同時發起第二組100個協程(coroutine101, coroutine200)請求併發的去請求Redis Cluster。
f) Proxy等待所有協程請求完成,然後合併所有協程請求的響應結果。
g) Proxy把所有協程響應的結果進行解析,包裝,返回給用戶。

####### 單work分散多work

優化前
	a) 用戶請求批量接口mset(200個key)(這裏先忽略合併相同槽的邏輯)
	b) Proxy會解析這200個key,會同時發起200個協程請求併發的去請求Redis Cluster。
	c) Proxy等待所有協程請求完成,然後合併所有協程請求的響應結果,進行解析,包裝返回給用戶。
優化後
a) 用戶請求批量接口mset(200個key)(這裏先忽略合併相同槽的key的邏輯)
b) Proxy會解析這200個key,然後進行拆分分組以此來控制併發度。
c) Proxy會根據劃分好的組進行一組一組的發起請求。
d) Proxy等待所有請求完成,然後合併所有協程請求的響應結果,進行解析,包裝返回給用戶。

監控模塊

4.1 系統級別

通過Open-Falcon Agent採集服務器的CPU、內存、網卡流量、網絡連接、磁盤等信息

4.2 應用級別

通過Open-Falcon Plugin採集Nginx/Redis進程級別的CPU,內存,Pid等信息。

4.3 業務級別

通過在Proxy裏面埋點監控業務接口QPS,RT(50%,99%,999%),請求流量,錯誤次數等信息,定時的上報給Open-Falcon

備註: 深入理解Nginx模塊開發與架構解析,這本書對掌握Nginx模塊開發有很好的講解,可以去看一下.

運維工具
  • redis-rebalance.rb進行擴展開發。運維非常方便快捷

  • redis-migrate-tool是唯品會開源針對redis數據運維工具

  • redis-cluster-tool

思考,如果自己設計一個類似的代理集羣,有哪些核心東西和組件需要思考

	角色:
Client: gcache客戶端
Proxy: 訪問redis的代理,負責分發請求
Redis Group: redis的副本集(M-M,M-S)
Monitor: redis的監控
Zookeeper: 存儲路由信息

	高可用:
Proxy至少一個節點存活,可隨意加減
Zookeeper可全部宕,proxy緩存了路由
Redis Group中至少存活一個節點
Monitor 雙機熱備

	伸縮性:
Proxy可動態加機器,client通過zk感知
Group動態增加,擴展集羣/新業務存儲
Group內redis動態增加,應對讀多的操作

Redis-RDB原理

按照規則定時把內存裏面的數據保存到磁盤

觸發條件

  • 自己配置的規則條件
save <seconds> <changes>
save 900 1  當在900秒內被更改的key的數量大於1的時候,就執行快照
save 300 10
save 60 10000

  • 執行save或者bgsave操作
save: 執行內存的數據同步到磁盤的操作,這個操作會阻塞客戶端的請求
bgsave: 在後臺異步執行快照操作,這個操作不會阻塞客戶端的請求

  • 執行flushall命令

清除內存的所有數據,只要快照的規則不爲空,也就是第一個規則存在。那麼redis會執行快照

  • 執行復制的時候

實現原理

  • redis使用fork函數複製一份當前進程的副本(子進程)

  • 父進程繼續接收並處理客戶端發來的命令,而子進程開始將內存中的數據寫入硬盤中的臨時文件

  • 當子進程寫入完所有數據後會用該臨時文件替換舊的RDB文件,至此,一次快照操作完成

備註


redis在進行快照的過程中不會修改RDB文件,只有快照結束後纔會將舊的文件替換成新的,也就是說任何時候RDB文件都是完整的。 這就使得我們可以通過定時備份RDB文件來實現redis數據庫的備份, RDB文件是經過壓縮的二進制文件,佔用的空間會小於內存中的數據,更加利於傳輸

RDB優點

(1)RDB會生成多個數據文件,每個數據文件都代表了某一個時刻中redis的數據,這種多個數據文件的方式,非常適合做冷備,可以將這種完整的數據文件發送到一些遠程的安全存儲上去,比如說Amazon的S3雲服務上去,在國內可以是阿里雲的ODPS分佈式存儲上,以預定好的備份策略來定期備份redis中的數據

(2)RDB對redis對外提供的讀寫服務,影響非常小,可以讓redis保持高性能,因爲redis主進程只需要fork一個子進程,讓子進程執行磁盤IO操作來進行RDB持久化即可

(3)相對於AOF持久化機制來說,直接基於RDB數據文件來重啓和恢復redis進程,更加快速

RDB缺點

1)如果想要在redis故障時,儘可能少的丟失數據,那麼RDB沒有AOF好。一般來說,RDB數據快照文件,都是每隔5分鐘,或者更長時間生成一次,這個時候就得接受一旦redis進程宕機,那麼會丟失最近5分鐘的數據

(2)RDB每次在fork子進程來執行RDB快照數據文件生成的時候,如果數據文件特別大,可能會導致對客戶端提供的服務暫停數毫秒,或者甚至數秒

Redis-AOF原理

AOF可以將Redis執行的每一條寫命令追加到硬盤文件中,這一過程顯然會降低Redis的性能,但大部分情況下這個影響是能夠接受的,另外使用較快的硬盤可以提高AOF的性能

AOF重寫原理


Redis 可以在 AOF 文件體積變得過大時,自動地在後臺對 AOF 進行重寫: 重寫後的新 AOF 文件包含了恢復當前數據集所需的最小命令集合。 整個重寫操作是絕對安全的,因爲 Redis 在創建新 AOF 文件的過程中,會繼續將命令追加到現有的 AOF 文件裏面,即使重寫過程中發生停機,現有的 AOF 文件也不會丟失。 而一旦新 AOF 文件創建完畢,Redis 就會從舊 AOF 文件切換到新 AOF 文件,並開始對新 AOF 文件進行追加操作。AOF 文件有序地保存了對數據庫執行的所有寫入操作, 這些寫入操作以 Redis 協議的格式保存, 因此 AOF 文件的內容非常容易被人讀懂, 對文件進行分析(parse)也很輕鬆

AOF同步磁盤

redis每次更改數據的時候, aof機制都會講命令記錄到aof文件,但是實際上由於操作系統的緩存機制,數據並沒有實時寫入到硬盤,而是進入硬盤緩存。再通過硬盤緩存機制去刷新到保存到文件
	appendfsync always  每次執行寫入都會進行同步  , 這個是最安全但是是效率比較低的方式
	appendfsync everysec   每一秒執行
	appendfsync no  不主動進行同步操作,由操作系統去執行,這個是最快但是最不安全的方式

AOF優點

1)AOF可以更好的保護數據不丟失,一般AOF會每隔1秒,通過一個後臺線程執行一次fsync操作,最多丟失1秒鐘的數據

(2)AOF日誌文件以append-only模式寫入,所以沒有任何磁盤尋址的開銷,寫入性能非常高,而且文件不容易破損,即使文件尾部破損,也很容易修復

(3)AOF日誌文件即使過大的時候,出現後臺重寫操作,也不會影響客戶端的讀寫。因爲在rewrite
log的時候,會對其中的指導進行壓縮,創建出一份需要恢復數據的最小日誌出來。再創建新日誌文件的時候,老的日誌文件還是照常寫入。當新的merge後的日誌文件ready的時候,再交換新老日誌文件即可。

(4)AOF日誌文件的命令通過非常可讀的方式進行記錄,這個特性非常適合做災難性的誤刪除的緊急恢復。比如某人不小心用flushall命令清空了所有數據,只要這個時候後臺rewrite還沒有發生,那麼就可以立即拷貝AOF文件,將最後一條flushall命令給刪了,然後再將該AOF文件放回去,就可以通過恢復機制,自動恢復所有數據

AOF缺點

(1)對於同一份數據來說,AOF日誌文件通常比RDB數據快照文件更大

(2)AOF開啓後,支持的寫QPS會比RDB支持的寫QPS低,因爲AOF一般會配置成每秒fsync一次日誌文件,當然,每秒一次fsync,性能也還是很高的

(3)以前AOF發生過bug,就是通過AOF記錄的日誌,進行數據恢復的時候,沒有恢復一模一樣的數據出來。所以說,類似AOF這種較爲複雜的基於命令日誌/merge/回放的方式,比基於RDB每次持久化一份完整的數據快照文件的方式,更加脆弱一些,容易有bug。不過AOF就是爲了避免rewrite過程導致的bug,因此每次rewrite並不是基於舊的指令日誌進行merge的,而是基於當時內存中的數據進行指令的重新構建,這樣健壯性會好很多。

RDB VS AOF

1)不要僅僅使用RDB,因爲那樣會導致你丟失很多數據

(2)也不要僅僅使用AOF,因爲那樣有兩個問題,第一,你通過AOF做冷備,沒有RDB做冷備,來的恢復速度更快; 第二,RDB每次簡單粗暴生成數據快照,更加健壯,可以避免AOF這種複雜的備份和恢復機制的bug

(3)綜合使用AOF和RDB兩種持久化機制,用AOF來保證數據不丟失,作爲數據恢復的第一選擇; 用RDB來做不同程度的冷備,在AOF文件都丟失或損壞不可用的時候,還可以使用RDB來進行快速的數據恢復

備註:如果同時使用RDB和AOF兩種持久化機制,那麼在redis重啓的時候,會使用AOF來重新構建數據,因爲AOF中的數據更加完整

Redis-實現分佈式鎖

目前幾乎很多大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。分佈式的CAP理論告訴我們"任何一個分佈式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時滿足兩項。“所以,很多系統在設計之初就要對這三者做出取捨。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證"最終一致性”,只要這個最終時間是在用戶可以接受的範圍內即可。

在很多場景中,我們爲了保證數據的最終一致性,需要很多的技術方案來支持,比如分佈式事務、分佈式鎖等。

選擇原因

  • Redis有很高的性能

  • Redis命令對此支持較好,實現起來比較方便

相關命令

SETNX

SETNX key val
當且僅當key不存在時,set一個key爲val的字符串,返回1;若key存在,則什麼都不做,返回0。

expire

expire key timeout
爲key設置一個超時時間,單位爲second,超過這個時間鎖會自動釋放,避免死鎖。

delete

delete key
刪除key

實現原理

•	獲取鎖的時候,使用setnx加鎖,並使用expire命令爲鎖添加一個超時時間,超過該時間則自動釋放鎖,鎖的value值爲一個隨機生成的UUID,通過此在釋放鎖的時候進行判斷。
•	獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。
•	釋放鎖的時候,通過UUID判斷是不是該鎖,若是該鎖,則執行delete進行鎖釋放

代碼實現

一般封裝一個工具類,提供兩個方法一個是獲取鎖,一個是釋放鎖

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

import java.util.List;
import java.util.UUID;

public class DistributedLock {
    private final JedisPool jedisPool;

    public DistributedLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    /**
     * 加鎖
     * @param locaName  鎖的key
     * @param acquireTimeout  獲取超時時間
     * @param timeout   鎖的超時時間
     * @return 鎖標識
     */
    public String getLock(String locaName, long acquireTimeout, long timeout) {
        Jedis conn = null;
        String retIdentifier = null;
        try {
            // 獲取連接
            conn = jedisPool.getResource();
            // 隨機生成一個value
            String identifier = UUID.randomUUID().toString();
            // 鎖名,即key值
            String lockKey = "lock:" + locaName;
            // 超時時間,上鎖後超過此時間則自動釋放鎖
            int lockExpire = (int)(timeout / 1000);

            // 獲取鎖的超時時間,超過這個時間則放棄獲取鎖
            long end = System.currentTimeMillis() + acquireTimeout;

            while (System.currentTimeMillis() < end) {
                
                if (conn.setnx(lockKey, identifier) == 1) {
                    conn.expire(lockKey, lockExpire);
                    // 返回value值,用於釋放鎖時間確認
                    retIdentifier = identifier;
                    return retIdentifier;
                }
                // 返回-1代表key沒有設置超時時間,爲key設置一個超時時間
                if (conn.ttl(lockKey) == -1) {
                    conn.expire(lockKey, lockExpire);
                }

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retIdentifier;
    }

    /**
     * 釋放鎖
     * @param lockName 鎖的key
     * @param identifier    釋放鎖的標識
     * @return
     */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = "lock:" + lockName;
        boolean retFlag = false;
        try {
            conn = jedisPool.getResource();
            while (true) {
                // 監視lock,準備開始事務
                conn.watch(lockKey);
                // 通過前面返回的value值判斷是不是該鎖,若是該鎖,則刪除,釋放鎖
                if (identifier.equals(conn.get(lockKey))) {
                    Transaction transaction = conn.multi();
                    transaction.del(lockKey);
                    List<Object> results = transaction.exec();
                    if (results == null) {
                        continue;
                    }
                    retFlag = true;
                }
                conn.unwatch();
                break;
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }
}


備註:比如超時時間的選取,獲取鎖時間的選取對併發量都有很大的影響,所以在使用redis做分佈式鎖的時候還是有很多細節需要考慮的,如果對性能不是非常高,可以使用ZK來做.

當然可以不選用基於Jedis的,還有一個叫Redission專門封裝了基於redis的常用分佈式鎖操作,使用起來更加簡單.

Redis-管道模式機制

redis是一個cs模式的tcp server,使用和http類似的請求響應協議。一個client可以通過一個socket連接發起多個請求命令。每個請求命令發出後client通常會阻塞並等待redis服務處理,redis處理完後請求命令後會將結果通過響應報文返回給client

管道技術

利用pipeline的方式從client打包多條命令一起發出,不需要等待單條命令的響應返回,而redis服務端會處理完多條命令後會將多條命令的處理結果打包到一起返回給客戶端

代碼實現


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-BJWP6bgD-1578369080152)(media/image6.png)]{width="6.0in" height="3.79375in"}

備註:對於使用redis的java客戶端,只有Jedis,ShardJedis支持管道,但JedisCluster不支持管道技術,當然我們可以分析已實現的Pipeline機制,來擴展我們自己的實現,下面就分析一下如何實現基於集羣版的Pipeline。


pipeline模式下命令將被緩存到對應的連接(OutputStream)上,而在真正向服務端發送數據時,節點可能發生了改變,數據就可能發向了錯誤的節點,這導致批量操作失敗,而要處理這種失敗是非常複雜的

JedisCluster擴展Pipeline


package com.springboot.redis.pipeline;

import org.apache.log4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisMovedDataException;
import redis.clients.jedis.exceptions.JedisRedirectionException;
import redis.clients.util.JedisClusterCRC16;
import redis.clients.util.SafeEncoder;

import java.io.Closeable;
import java.lang.reflect.Field;
import java.util.*;

public class JedisClusterPipeline extends PipelineBase implements Closeable {

    private static final String SPLIT_WORD = ":";

    // 部分字段沒有對應的獲取方法,只能採用反射來做
    // 你也可以去繼承JedisCluster和JedisSlotBasedConnectionHandler來提供訪問接口
    private static final Field FIELD_CONNECTION_HANDLER;
    protected static final Field FIELD_CACHE;

    static {
        FIELD_CONNECTION_HANDLER = getField(BinaryJedisCluster.class, "connectionHandler");
        FIELD_CACHE = getField(JedisClusterConnectionHandler.class, "cache");
    }

    private JedisSlotBasedConnectionHandler connectionHandler;

    private JedisClusterInfoCache clusterInfoCache;



    private Queue<Client> clients = new LinkedList<Client>();   // 根據順序存儲每個命令對應的Client
    private Map<JedisPool, Map<Long, Jedis>> jedisMap = new HashMap<JedisPool, Map<Long, Jedis>>();   // 用於緩存連接
    private boolean hasDataInBuf = false;   // 是否有數據在緩存區


    public JedisClusterPipeline(JedisCluster jedisCluster) {
        connectionHandler = getValue(jedisCluster, FIELD_CONNECTION_HANDLER);
        clusterInfoCache = getValue(connectionHandler, FIELD_CACHE);
    }


    /**
     * 刷新集羣信息,當集羣信息發生變更時調用
     *
     * @param
     * @return
     */
    public void refreshCluster() {
        connectionHandler.renewSlotCache();
    }

    /**
     * 同步讀取所有數據. 與syncAndReturnAll()相比,sync()只是沒有對數據做反序列化
     */
    public void sync() {
        innerSync(null);
    }

    @Override
    public void close() {
        clean();
        clients.clear();
        for (Map.Entry<JedisPool, Map<Long, Jedis>> poolEntry : jedisMap.entrySet()) {
            for (Map.Entry<Long, Jedis> jedisEntry : poolEntry.getValue().entrySet()) {
                if (hasDataInBuf) {
                    flushCachedData(jedisEntry.getValue());
                }
                jedisEntry.getValue().close();
            }
        }
        jedisMap.clear();
        hasDataInBuf = false;
    }

    /**
     * 同步讀取所有數據 並按命令順序返回一個列表
     *
     * @return 按照命令的順序返回所有的數據
     */
    public List<Object> syncAndReturnAll() {
        List<Object> responseList = new ArrayList<Object>();
        innerSync(responseList);
        return responseList;
    }
    private void innerSync(List<Object> formatted) {
        HashSet<Client> clientSet = new HashSet<Client>();
        try {
            for (Client client : clients) {
                // 在sync()調用時其實是不需要解析結果數據的,但是如果不調用get方法,發生了JedisMovedDataException這樣的錯誤應用是不知道的,因此需要調用get()來觸發錯誤。
                // 其實如果Response的data屬性可以直接獲取,可以省掉解析數據的時間,然而它並沒有提供對應方法,要獲取data屬性就得用反射,不想再反射了,所以就這樣了
                Object data = generateResponse(client.getOne()).get();
                if (null != formatted) {
                    formatted.add(data);
                }
                // size相同說明所有的client都已經添加,就不用再調用add方法了
                if (clientSet.size() != jedisMap.size()) {
                    clientSet.add(client);
                }
            }
        } catch (JedisRedirectionException jre) {
            if (jre instanceof JedisMovedDataException) {
                // if MOVED redirection occurred, rebuilds cluster's slot cache,
                // recommended by Redis cluster specification
                refreshCluster();
            }
            throw jre;
        } finally {
            if (clientSet.size() != jedisMap.size()) {
                // 所有還沒有執行過的client要保證執行(flush),防止放回連接池後後面的命令被污染
                for (Map.Entry<JedisPool, Map<Long, Jedis>> poolEntry : jedisMap.entrySet()) {
                    for (Map.Entry<Long, Jedis> jedisEntry : poolEntry.getValue().entrySet()) {
                        if (clientSet.contains(jedisEntry.getValue().getClient())) {
                            continue;
                        }
                        flushCachedData(jedisEntry.getValue());
                    }
                }
            }
            hasDataInBuf = false;
            close();
        }
    }

    private void flushCachedData(Jedis jedis) {
        try {
            jedis.getClient().getAll();
        } catch (RuntimeException ex) {
        }
    }

    @Override
    protected Client getClient(String key) {
        byte[] bKey = SafeEncoder.encode(key);
        return getClient(bKey);
    }

    @Override
    protected Client getClient(byte[] key) {
        Jedis jedis = getJedis(JedisClusterCRC16.getSlot(key));
        Client client = jedis.getClient();
        clients.add(client);
        return client;
    }

    private Jedis getJedis(int slot) {

        // 根據線程id從緩存中獲取Jedis
        Jedis jedis = null;
        Map<Long, Jedis> tmpMap = null;
        //獲取線程id
        long id = Thread.currentThread().getId();
        //獲取jedispool
        JedisPool pool = clusterInfoCache.getSlotPool(slot);

        if (jedisMap.containsKey(pool)) {
            tmpMap = jedisMap.get(pool);
            if (tmpMap.containsKey(id)) {
                jedis = tmpMap.get(id);
            } else {
                jedis = pool.getResource();
                tmpMap.put(id, jedis);
            }
        } else {
            tmpMap = new HashMap<Long, Jedis>();
            jedis = pool.getResource();
            tmpMap.put(id, jedis);
            jedisMap.put(pool,tmpMap);
        }
        hasDataInBuf = true;
        return jedis;
    }

    private static Field getField(Class<?> cls, String fieldName) {
        try {
            Field field = cls.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field;
        } catch (NoSuchFieldException | SecurityException e) {
            throw new RuntimeException("cannot find or access field '" + fieldName + "' from " + cls.getName(), e);
        }
    }

    @SuppressWarnings({"unchecked"})
    private static <T> T getValue(Object obj, Field field) {
        try {
            return (T) field.get(obj);
        } catch (IllegalArgumentException | IllegalAccessException e) {
            System.out.println("get value fail");
            throw new RuntimeException(e);
        }
    }

}


這個實現主要參考了Jedis內置的ShardedJedisPipeline.java

Jedis-核心源碼分析

核心功能

它提供了redis三種操作方式,分別是基於單機模式,分片模式,集羣模式

  • 單機模式,就是Jedis

  • 分片模式,就是ShardedJedis

  • 集羣模式,就是JedisCluster

API實現本質

其實它的本質就是封裝了一個Socket,通過它來請求redis服務,並返回結果.

核心類分析

命令接口
  • JedisCommands:定義redis常用命令

  • MultikeyCommands:定義redis批量請求命令

  • AdvancedJedisCommands:定義redis高級請求命令,比如慢日誌查看

  • ScriptingCommands:定義redis對腳本的支持,這裏主要是lua腳本

  • BasicCommands,定義對redis一些基本屬性的命令,主要是ping,查看庫,同步數據等

  • ClusterCommands:定義查看集羣相關信息

  • SentinelCommands:定義哨兵相關監控方法

實現各種命令
  • BinaryJedis,它是封裝了所有實現的方法,留給其它子類直接繼承使用

  • Client:BinaryJedis直接調用它,它繼承與BinaryClient,都是有這個方法實現的

  • BinaryClient:調用Connection的sendCommand方法來執行最終請求命令的發送

  • Connection:通過封裝Socket的客戶端請求,同時實現了具體的sendCommand發送方法.


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-y8LT8Rvb-1578369080153)(media/image7.png)]{width="6.0in" height="3.591666666666667in"}

Connect方法就是創建socket連接


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cjO1yZ1y-1578369080154)(media/image8.png)]{width="6.0in" height="2.207638888888889in"}

Protocol.sendCommand方法就是封裝了一些請求頭,然後寫入信息


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tYCrSe8r-1578369080155)(media/image9.png)]{width="6.0in" height="2.7895833333333333in"}

通過上面流程分析,jedis本身就是線程不安全的,所以後面引入JedisPool來優化Jedis的連接數.

管道實現
  • PipelineBase:封裝了管道要實現的各種API,但是留有抽象方法獲取具體Client

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hnYFN7YM-1578369080156)(media/image10.png)]{width="4.8848064304461944in" height="0.739491469816273in"}

  • MultikeyPipelineBase:繼承與PipelineBase,它本身就是一個抽象類,直接透傳client對象給父類

  • Pipeline:繼承與上面這個類,且實現了setClient和getClient方法,主要就這個方法


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hidk7iRG-1578369080157)(media/image11.png)]{width="5.311836176727909in" height="2.4163648293963256in"}

  • BinaryJedis:提供了一個pipelined方法,來實例化一個pipeline

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-wpTj7aCr-1578369080158)(media/image12.png)]{width="6.0in" height="1.1666666666666667in"}

  • Queable:把每次請求的結果添加到隊列中,等所有請求都執行完畢,一起返回給客戶端.

備註:管道的技術就是等所有命令都執行完畢,會一次性把結果讀取出來,返回給客戶端,從而可以讓命令之間的操作是異步的.


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Fmss1Uci-1578369080159)(media/image13.png)]{width="5.624296806649169in" height="1.656043307086614in"}

API總結

單機模式

只能支持單redis實例

  • Jedis,單機非併發

  • JedisPool,單機併發

  • Pipeline,單機管道技術

分片模式

一個實例掛了,整個集羣都不可用,底層使用Hash,不好擴容

  • ShardedJedis,非併發多個數據分片

  • SharedJedisPool,併發,多個數據分片

  • ShardedJedisPipeline,使用分片管道技術

集羣模式

支持集羣模式,但是自身提供對管道的支持,需要開發者自己擴展

  • JedisCluster

Redis-性能優化

在使用redis過程中,如果遭遇攻擊或者高併發下,很容易出現一些十分可怕的問題,如果不及時處理,可能導致整個集羣不可用.

緩存併發

這種情況,一般是多個線程同時查詢緩存,因爲緩存沒有及時查詢到值,而會查詢數據庫,量大了,會把數據庫給拖垮.常用的解決方案有以下幾種

  • 分佈式鎖

  • 本地鎖

  • 軟過期

其中加鎖的話,要注意鎖的粒度,且可以在查詢DB之前,再一次判斷緩存中是否有值,核心代碼如下,不要直接在方法上加synchroized,這樣太粗了,耗性能


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-j2KfojS3-1578369080160)(media/image14.png)]{width="6.0in" height="2.571527777777778in"}

緩存穿透

這種一般是系統遭遇到攻擊,一直查詢一個數據庫不存在的值,從而擊垮數據庫.解決方案如下

  • 參數校驗,比如key的格式是否合法

  • 參數正確的情況下,可以把value設置爲null

  • 可以適當使用Filter,進行過濾

  • 數據量比較大的情況下,可以使用BoolmFilter進行過濾

(不存在的肯定能判斷出來,存在的有可能會判斷不存在)

緩存雪崩

這種主要是因爲在某段時間內,所有的key都失效了,從而導致所有的查詢都透傳到DB,讓DB徹底跨了,一般解決方案如下.

  • Key失效時間設置成隨機數(增加鹽),均勻點,避免同一時間內,有大量的key失效

  • 查詢key加分佈式鎖

  • key永不過期

把過期時間存在key對應的value裏,如果發現要過期了,通過一個後臺的異步線程進行緩存的構建,也就是“邏輯”過期
爲每個 value 設置一個邏輯過期時間,當發現超過邏輯過期時間後,會使用單獨的線程去構建緩存。

從實戰看,這種方法對於性能非常友好,唯一不足的就是構建緩存時候,其餘線程(非構建緩存的線程)可能訪問的是老數據,但是對於一般的互聯網功能來說這個還是可以忍受

緩存一致性

這種情況主要是產生在緩存裏面的數據和db的數據有時候會不完全一致,實際中也是要根據業務需求,來選擇適當的解決方案,通常我們說的一致性就分爲兩種,一種是強一致性,一種是弱一致性(最終一致性)。一般解決方案如下

延時雙刪策略

在寫庫前後都進行redis.del(key)操作,並且設定合理的超時時間。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cm2dVi1b-1578369080161)(media/image15.png)]{width="5.763888888888889in" height="1.4222222222222223in"}

具體的步驟

具體的步驟就是:
1)先刪除緩存
2)再寫數據庫
3)休眠500毫秒
4)再次刪除緩存

異步更新緩存

主要技術架構如下


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-PtvABqSB-1578369080161)(media/image16.png)]{width="5.733333333333333in" height="3.0080096237970255in"}

讀取binlog後分析 ,利用消息隊列,推送更新各臺的redis緩存數據。

這樣一旦MySQL中產生了新的寫入、更新、刪除等操作,就可以把binlog相關的消息推送至Redis,Redis再根據binlog中的記錄,對Redis進行更新。

其實這種機制,很類似MySQL的主從備份機制,因爲MySQL的主備也是通過binlog來實現的數據一致性。

這裏可以結合使用canal(阿里的一款開源框架),通過該框架可以對MySQL的binlog進行訂閱,而canal正是模仿了mysql的slave數據庫的備份請求,使得Redis的數據更新達到了相同的效果。

當然,這裏的消息推送工具你也可以採用別的第三方:kafka、rabbitMQ等來實現推送更新Redis。

(一般數據庫都是基於mysql,binglog+cancal+kafka+redis/db:maxwell)

緩存熱點Key

首先要想辦法通過手段識別出哪些是熱點數據.

客戶端熱點key緩存

客戶端熱點key緩存:將熱點key對應value並緩存在客戶端本地,並且設置一個失效時間。對於每次讀請求,將首先檢查key是否存在於本地緩存中,如果存在則直接返回,如果不存在再去訪問分佈式緩存的機器。

熱點key分散多個子key

將熱點key分散爲多個子key,然後存儲到緩存集羣的不同機器上,這些子key對應的value都和熱點key是一樣的。當通過熱點key去查詢數據時,通過某種hash算法隨機選擇一個子key,然後再去訪問緩存機器,將熱點分散到了多個子key上。

智能識別技術

Reids 內部採用多級 LRU 的數據結構,通過將訪問數據 Key 的頻率和大小設定不同權值,從而放到不同層級的 LRU 上。這樣淘汰時可以確保權值高的那批 Key 得到保留,最終保留下來且超過閾值設定的就會判斷爲熱點 Key。

動態散列技術

當發現熱點後,應用服務器和 redis 服務端就會聯動起來,根據預先設定好的訪問模型,將熱點數據動態散列到 redis 服務端其他數據節點的 Hot Zone 存儲區域去訪問。

Redis-BF去重

Bloom Filter是一種空間效率很高的隨機數據結構,它利用位數組很簡潔地表示一個集合,並能判斷一個元素是否屬於這個集合。Bloom Filter的這種高效是有一定代價的:在判斷一個元素是否屬於某個集合時,有可能會把不屬於這個集合的元素誤認爲屬於這個集合(false positive)。因此,Bloom Filter不適合那些"零錯誤"的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter通過極少的錯誤換取了存儲空間的極大節省。

下面我們具體來看Bloom Filter是如何用位數組表示集合的。初始狀態時,Bloom Filter是一個包含m位的位數組,每一位都置爲0

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-bZmA0gEI-1578369080162)(media/image17.png)]{width="3.4474857830271217in" height="0.5103532370953631in"}

爲了表達S={x1, x2,…,xn}這樣一個n個元素的集合,Bloom Filter使用k個相互獨立的哈希函數(Hash Function),它們分別將集合中的每個元素映射到{1,…,m}的範圍中。對任意一個元素x,第i個哈希函數映射的位置hi(x)就會被置爲1(1≤i≤k)。注意,如果一個位置多次被置爲1,那麼只有第一次會起作用,後面幾次將沒有任何效果。在下圖中,k=3,且有兩個哈希函數選中同一個位置(從左邊數第五位)。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-EOKSXIaq-1578369080163)(media/image18.png)]{width="3.8328543307086615in" height="1.0207053805774278in"}

在判斷y是否屬於這個集合時,我們對y應用k次哈希函數,如果所有hi(y)的位置都是11ik),那麼我們就認爲y是集合中的元素,否則就認爲y不是集合中的元素。下圖中*y1*就不是集合中的元素。y2或者屬於這個集合,或者剛好是一個false positive

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zKmUIPm2-1578369080164)(media/image19.png)]{width="3.4683169291338585in" height="0.8748906386701663in"}

備註: 它實際上是由一個很長的二進制向量和一系列隨機映射函數組成

Redis裏面已經提供了自帶的實現


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Gvp33hHs-1578369080165)(media/image20.png)]{width="5.6659590988626425in" height="3.1975174978127736in"}

一般對於大數據量進行去重,首先要計算一下轉換爲位存儲之後的內存,可以使用如下的計算公式

bitSize = (int) Math.ceil(maxKey * (Math.log(errorRate) / Math.log(0.6185)));

保證errorRate不變的前提下,bloomfilter 的maxKey越大,bloomfilter所需要的內存也就越大

Redis-緩存預熱

爲了更好的提供緩存服務,一般會對緩存進行預熱,避免系統一開始就查詢數據,這個可以根據的業務場景和數據量進行選擇

  • 數據量不大,可以在項目啓動的時候自動進行加載;

  • 如果數據量很大的話,可以把高頻率詞全部加載到內存

  • 如果實時性高且併發高的話(建立實時熱點統計系統),可以結合nginx+lua,之後上報到kafka,然後用storm進行實時消費,計算完結果同步到redis或者內存結構中,如果是分佈式還要考慮鎖的問題.

Redis-Lua結合

Lua是一種腳本語言,使用簡單且語法比較靈活,而且redis內置就提供了對它的支持,實際業務場景有很多都可以直接使用redis+lua進行解決,最典型的應用場景,比如用戶訪問限制,統計等。現在比如限制某個IP,1分鐘之內只能訪問10次

使用方式有兩種,

  • 一種是自己編寫lua腳本,然後通過調用redis命令來執行


在這裏插入圖片描述

  • 一種是直接在客戶端拼接lua腳本,然後直接調用jedis的api執行

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9NiGRgK2-1578369080168)(media/image23.png)]{width="6.0in" height="3.8243055555555556in"}

備註:語法爲 ./redis-cli --eval [lua腳本] [key…]空格,空格[args…],且lua接受參數的類型是集合.

Redis-優化與方案

  • 批量查詢使用Pipeline

  • 禁止使用耗性能的相關命令,比如keys,flushall,bgsave,monitor

  • 禁止系統中存在過大的KEY或者VALUE

  • 儘量合理設置每個KEY的有效期

常見問題

redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out。

解決方案:查看網絡以及慢日誌,看是否有存在比較耗時的命令,比如keys

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool

解決方案,合理設置timoue和tcp-keepavlive屬性的值

Can’t save in background: fork: Cannot allocate memory

解決方案,修改Linux相關內核參數. vm.overcommit_memory=1

Redis-緩存系統設計

容量規劃

  • 緩存內容的大小

  • 緩存內容的數量

  • 緩存的淘汰策略

  • 緩存的數據結構

  • 每秒讀取的峯值

  • 每秒寫入的峯值

性能優化

  • 線程模型

  • 緩存分片

  • 緩存預熱

  • 緩存併發

  • 緩存失效

  • 緩存穿透

高可用設計

  • 失效轉移

  • 持久化

  • 複製模型

  • 集羣模型

  • 緩存重建

緩存監控

  • 緩存服務監控

  • 緩存容量監控

  • 緩存請求監控

  • 緩存響應時間監控

思考事項

  • 是否有可能發生緩存穿透,雪崩,併發

  • 是否有大key,大value

  • 是否支持Lua的腳本

  • 是否避免了競爭條件-資源衝突

多級緩存架構

緩存是我們在解決高併發情況下絕對不可少的一種技術,但是緩存的範圍是很廣的,並不是唯獨指redis,encache,memcache等,從架構的角度來說,緩存可以分爲全局緩存架構和部分緩存架構,下面分析一下這兩種

全局緩存架構

作爲一個架構師,考慮緩存設計或者應用的時候,需要從整體考慮緩存的使用,這一般包括前端+後端+數據庫,換言之就是各個地方都有可能存在緩存,而且我們也要正確使用它,可以看下面這樣的一個圖


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tTfJUaIu-1578369080169)(media/image24.png)]{width="5.763888888888889in" height="3.314583333333333in"}

可以發現各個層都是有對應的緩存,當然這裏還缺少一個部分,就是瀏覽器,瀏覽器也有專門的緩存,所以如果要想通過使用緩存來提升網站整體性能,這些方面都需要考慮到

部分緩存架構

這種緩存一般是偏某個端,比如前段或者後端,針對是開發工程師而言,比如在實際環境中一般從性能角度考慮,服務端有可能會使用多級緩存架構,獲取數據的流程如下

1 首先從local緩存獲取數據,有直接返回客戶端,沒有走分佈式緩存

2 從分佈式緩存中獲取數據,如果有寫入local緩存並返回客戶端端,如果沒有

3 查詢數據庫,寫入local緩存和分佈式緩存,然後返回客戶端

架構圖如下


[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-iKX9ZsS0-1578369080170)(media/image25.png)]{width="5.763888888888889in" height="5.365972222222222in"}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章