深入学习Redis
文章目錄
- Redis
- 前言
- 一、概念和基礎(chǔ)
- 概念
- 優(yōu)勢
- 官方資料
- 使用場景
- 二、數(shù)據(jù)類型:5種基礎(chǔ)的數(shù)據(jù)類型
- Redis數(shù)據(jù)結(jié)構(gòu)簡介
- 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)詳解
- String字符串
- List列表
- Set集合
- Hash散列
- Zset有序集合
- 三、特殊的數(shù)據(jù)類型
- 三種特殊類型講解
- HyperLogLog
- Bitmap
- Geospatial
- 四、數(shù)據(jù)庫
- 常用指令
- 過期數(shù)據(jù)的刪除策略
- 定時刪除
- 惰性刪除
- 定期刪除
- 五、持久化
- RDB持久化
- 觸發(fā)方式
- RDB的優(yōu)缺點
- AOF持久化
- 實現(xiàn)AOF
- 配置文件
- 重寫機制
- 重啟加載
- 六、事務(wù)
- 七、主從復(fù)制
- 舊版復(fù)制功能的實現(xiàn)
- 復(fù)制
- 命令傳播
- 新版復(fù)制功能的實現(xiàn)
- 部分重同步
- 復(fù)制的完整流程
- 心跳檢測
- 八、哨兵
- 九、集群
- 集群的作用
- 集群的搭建
- 執(zhí)行Redis命令搭建集群
- 使用Ruby腳本搭建集群
- 集群設(shè)計
- 數(shù)據(jù)結(jié)構(gòu)
- 集群命令的實現(xiàn)
- 實踐須知
- 集群伸縮
- ASK錯誤
Redis
前言
Redis是一種支持key-value等多種數(shù)據(jù)結(jié)構(gòu)的存儲系統(tǒng),通過在內(nèi)存中讀取數(shù)據(jù),大大提高了數(shù)據(jù)的讀取速度,作為一個緩存中間件,是實現(xiàn)網(wǎng)站高并發(fā)以及高可用不可或缺的一部分。可應(yīng)用于緩存,事件發(fā)布或者訂閱,高速隊列等場景,支持網(wǎng)絡(luò),提供字符串、哈希表、列表、隊列、集合結(jié)構(gòu)直接存儲,基于內(nèi)存并可持久化。
一、概念和基礎(chǔ)
概念
Redis是一款基于內(nèi)存的高速緩存數(shù)據(jù)庫,全稱為:Remote Dictionary Server(遠程數(shù)據(jù)服務(wù)),使用C語言編寫,支持豐富的數(shù)據(jù)類型。
Redis與其他key-value緩存產(chǎn)品有以下幾個特點:
- Redis支持數(shù)據(jù)的持久化,可以將內(nèi)存中的數(shù)據(jù)保存在磁盤,重啟的時候再次加載進行使用
- Redis不僅僅支持簡單的key-value類型數(shù)據(jù),同時還提供list、set、zset以及hash等數(shù)據(jù)結(jié)構(gòu)的存儲
- Redis支持數(shù)據(jù)的備份,即master-slave模式的數(shù)據(jù)備份。
優(yōu)勢
- 性能極高 — Redis能讀的速度是110000次/s,寫的速度是81000次/s
- 原子性 — Redis所有操作都是原子性的,同時Redis還支持對幾個操作全并之后的原子性執(zhí)行
- 豐富的特性 — Redis支持publish/subscribe,通知,key過期等等特性。
- 持久化 — Redis支持RDB,AOF等持久化方式
- 發(fā)布訂閱 — Redis支持發(fā)布/訂閱模式
- 分布式 — Redis Cluster 集群
官方資料
Redis官網(wǎng):http://redis.io/
Redis官方文檔:http://redis.io/documentation
Redis下載:http://redis.io/download
參考資料:
http://redisdoc.com/index.html
https://www.cnblogs.com/kismetv/p/8654978.html#t41
https://www.pdai.tech/md/outline/x-outline.html#nosql-db—redis%E8%AF%A6%E8%A7%A3
使用場景
熱點數(shù)據(jù)的緩存
緩存是Redis中最常見的應(yīng)用場景,Redis讀寫性能優(yōu)異,在高并發(fā)服務(wù)中成為首選的緩存組件,并且Redis支持事務(wù),能保證數(shù)據(jù)的一致性。
限時活動
Redis中可以使用expire命令射在一個鍵的生存時間,過期之后Redis會自動刪除,利用這一特性可以應(yīng)用在限時搶購活動、獲取手機驗證碼等常見業(yè)務(wù)場景。
計數(shù)器相關(guān)問題
Redis中有incr命令可以實現(xiàn)原子性的遞增,可以運用于高并發(fā)的秒殺活動、分布式序列號的生成以及其他限制次數(shù)的業(yè)務(wù)中。
分布式鎖
Redis中有setnx命令,該命令全寫為:“set if not exists”,如果不存在則設(shè)置成功并返回1,否則返回0。在分布式集群系統(tǒng)中,一個定時任務(wù)可能會在多個機器上運行,為了保持數(shù)據(jù)的一致性,可以先在定時任務(wù)中運用setnx命令設(shè)置一個鎖,如果成功設(shè)置則執(zhí)行任務(wù),否則說明任務(wù)已經(jīng)開始執(zhí)行,從而避免多個任務(wù)同時執(zhí)行對數(shù)據(jù)產(chǎn)生影響。該分布式鎖常運用于大型秒殺系統(tǒng)。
延時操作
在電商系統(tǒng)中,當(dāng)用戶下單,訂單產(chǎn)生之后會占用庫存,可以設(shè)置一個時效檢驗用戶是否已經(jīng)付款購買,如果超時則讓該單據(jù)失效,同時還原庫存。在Redis2.8.0版本之后還提供了Keyspace Notifications功能,運行客戶端訂閱Pub/Sub頻道,接受Redis數(shù)據(jù)集的變化事件。通過上述我們就可以解決實際應(yīng)用的問題,當(dāng)訂單產(chǎn)生時,設(shè)置一個key,同時設(shè)置15分鐘后過期,在后臺實現(xiàn)一個監(jiān)聽器,監(jiān)聽key的時效,key失效后仍沒有完成訂單則取消訂單。
點贊、好友等相互關(guān)系的存儲
Redis利用集合的一些命令,如求交集、并集、差集等。
在微信朋友圈中,每個用戶的好友存入一個集合中,很容易實現(xiàn)求出兩個人的共同好友,并將共同好友的信息呈現(xiàn)在朋友圈中。
二、數(shù)據(jù)類型:5種基礎(chǔ)的數(shù)據(jù)類型
Redis數(shù)據(jù)結(jié)構(gòu)簡介
Redis所有的key(鍵)都是字符串,基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)包括String、List、Set、Zset、Hash。每種結(jié)構(gòu)都至少有兩種編碼,這樣的好處在于:一方面接口與實現(xiàn)實現(xiàn)了分離,需要增加或改變內(nèi)部編碼時,用戶不受影響,另一方面可以根據(jù)不同的應(yīng)用場景切換內(nèi)部編碼,提高效率。
| String字符串 | 二進制安全的,可以包含任何數(shù)據(jù),比如jpg格式的圖片或者可以序列化的對象,一個鍵最大能存儲512MB | 對整個字符串或字符串的一部分進行操作;對整數(shù)或浮點數(shù)進行自增或自減操作; |
| List列表 | 本質(zhì)是鏈表,鏈表上的每個節(jié)點都包含一個字符串 | 鏈表的兩端都可以進行push和pop操作,讀取單個或多個數(shù)據(jù) |
| Set集合 | 包含字符串的無序集合并且存儲的值唯一,集合是通過哈希表實現(xiàn)的,所以添加,刪除,查找的復(fù)雜度都是 O(1)。 | 字符串的集合,包含基礎(chǔ)的方法有看是否存在添加、獲取、刪除;還包含計算交集、并集、差集等 |
| Zset有序集合 | 包含鍵值對的無序散列表,是一個 string 類型的field 和 value 的映射表,hash 特別適合用于存儲對象。 | 添加、獲取、刪除單個元素 |
| Hash散列表 | string 類型元素的集合,且不允許重復(fù)的成員,不同的是每個元素都會關(guān)聯(lián)一個 double 類型的分數(shù)。redis 正是通過分數(shù)來為集合中的成員進行從小到大的排序。 | 字符串成員與浮點數(shù)分數(shù)之間的有序映射;元素的排列順序由分數(shù)的大小決定;包含方法有添加、獲取、刪除單個元素以及根據(jù)分值范圍或成員來獲取元素 |
基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)詳解
String字符串
String是redis中最基本的數(shù)據(jù)類型,一個key對應(yīng)一個value。
-
命令使用
命令簡述使用 SET 將字符串值 value 關(guān)聯(lián)到 key,如果 key 已經(jīng)持有其他值, SET 就覆寫舊值, 無視類型 SET key value SETNX “Set If Not Exists”的縮寫,鍵key 不存在的情況,將key設(shè)為 valule,并返回1,若鍵 key 已經(jīng)存在, 則 SETNX 命令不做任何動作,并返回0 SETNX key value SETEX 將鍵 key 的值設(shè)置為 value , 并將鍵 key 的生存時間設(shè)置為 seconds 秒鐘。如果鍵 key 已經(jīng)存在, 那么 SETEX 命令將覆蓋已有的值 SETEX key seconds value PSETEX 這個命令和 SETEX 命令相似, 但它以毫秒為單位設(shè)置 key 的生存時間, 而不是像 SETEX 命令那樣以秒為單位進行設(shè)置 PSETEX key milliseconds value GET 返回與鍵 key 相關(guān)聯(lián)的字符串值 GET key GETSET 將鍵 key 的值設(shè)為 value , 并返回鍵 key 在被設(shè)置之前的舊值 GETSET key value APPEND 如果鍵 key 已經(jīng)存在并且它的值是一個字符串, APPEND 命令將把 value 追加到鍵 key 現(xiàn)有值的末尾。如果key不存在,則該命令與SET,命令效果相同 APPEND key value INCR 為鍵 key 儲存的數(shù)字值加上一。如果鍵 key 不存在, 那么它的值會先被初始化為 0 , 然后再執(zhí)行 INCR 命令。如果鍵 key 儲存的值不能被解釋為數(shù)字, 那么 INCR 命令將返回一個錯誤 INCR key INCRBY 為鍵 key 儲存的數(shù)字值加上增量 increment INCRBY key increment INCRBYFLOAT 為鍵 key 儲存的值加上浮點數(shù)增量 increment INCRBYFLOAT key increment DECR 為鍵 key 儲存的數(shù)字值減去一。如果鍵 key 不存在, 那么鍵 key 的值會先被初始化為 0 , 然后再執(zhí)行 DECR 操作。如果鍵 key 儲存的值不能被解釋為數(shù)字, 那么 DECR 命令將返回一個錯誤 DECR key DECRBY 將鍵 key 儲存的整數(shù)值減去減量 decrement DECRBY key decrement MSET 同時為多個鍵設(shè)置值。 MSET key value [key value …] MGET 返回給定的一個或多個字符串鍵的值。 MGET key [key …] -
實戰(zhàn)場景
- 緩存:把常用消息,字符串,照片等信息存入Redis中,把Redis當(dāng)作緩存層,Mysql作為持久層,以減輕Musql的讀寫壓力
- 計數(shù)器:Redis是單線程模型,一個命令執(zhí)行完才會執(zhí)行下一個。
- Session:Spring Session + Redis實現(xiàn)Session共享。
List列表
實質(zhì)為鏈表,用雙端鏈表實現(xiàn)
-
命令使用
命令簡述使用 LPUSH/RPUSH 將一個或多個值 value 插入到列表 key 的表頭(表尾) LPUSH /RPUSH key value [value …] LPUSHX 將值 value 插入到列表 key 的表頭,當(dāng)且僅當(dāng) key 存在并且是一個列表。和 LPUSH 命令相反,當(dāng) key 不存在時 LPUSHX命令什么也不做。 LPUSHX key value LPOP/RPOP 移除并返回列表 key 的頭(尾)元素。 LPOP /RPOP key RPOPLPUSH 將列表 source 中的最后一個元素(尾元素)彈出,并返回給客戶端。 將 source 彈出的元素插入到列表 destination ,作為 destination 列表的的頭元素。 RPOPLPUSH source destination LREM 根據(jù)參數(shù) count 的值,移除列表中與參數(shù) value 相等的元素;count > 0: 從表頭開始向表尾搜索,移除與value相等的元素,數(shù)量為count; count < 0 : 從表尾開始向表頭搜索,移除與 value 相等的元素,數(shù)量為 count 的絕對值。count = 0 : 移除表中所有與 value 相等的值。 LREM key count value LLEN 返回列表 key 的長度。 LLEN key LINDEX 返回列表 key 中,下標(biāo)為 index 的元素。 LINDEX key index LSET 將列表 key 下標(biāo)為 index 的元素的值設(shè)置為 value 。 LSET key index value LRANGE 返回列表 key 中指定區(qū)間內(nèi)的元素,區(qū)間以偏移量 start 和 stop 指定。 LRANGE key start stop LTRIM 對一個列表進行修剪(trim),就是說,讓列表只保留指定區(qū)間內(nèi)的元素,不在指定區(qū)間之內(nèi)的元素都將被刪除。 LTRIM key start stop BLPOP 它是 LPOP key 命令的阻塞版本,當(dāng)給定列表內(nèi)沒有任何元素可供彈出的時候,連接將被 BLPOP 命令阻塞,直到等待超時或發(fā)現(xiàn)可彈出元素為止。 BLPOP key [key …] timeout -
列表的使用技巧
- lpush+lpop=Stack(棧)
- lpush+rpop=Queue(隊列)
- lpush+ltrim=Capped Collection(有限列表)
- push+brpop=Message Queue(消息隊列)
-
實戰(zhàn)場景
- 朋友圈:lpush新的動態(tài),lpop展示新的動態(tài)
- 消息隊列:利用RPOPLPUSH實現(xiàn)一個安全的消息隊列,不僅返回一個消息同時將這個消息備份到一個備份列表中,如果使用LPUSH將消息放入隊列,而另一個客戶端中 BRPOP取出隊列,該隊列方式是不安全的,如果一個客戶端取出消息后崩潰,而未處理完的消息也將因此丟失
- 事件提醒:有時候為了等待一個新的元素到達數(shù)據(jù)中,需要使用輪詢的方式對數(shù)據(jù)進行探查,另一種更好的方式是,使用系統(tǒng)提供的阻塞原語,在新元素到達時立即進行處理,而新元素沒達到時,一直阻塞,避免輪詢占用資源
Set集合
Redis 的 Set 是 String 類型的無序集合。集合成員是唯一的,這就意味著集合中不能出現(xiàn)重復(fù)的數(shù)據(jù)。
通過哈希表實現(xiàn)的,所以添加,刪除,查找的復(fù)雜度都是 O(1)。
-
命令使用
命令簡述使用 SADD 將一個或多個 member 元素加入到集合 key 當(dāng)中,已經(jīng)存在于集合的 member 元素將被忽略 SADD key member [member …] SISMEMBER 判斷 member 元素是否集合 key 的成員;如果 member 元素是集合的成員,返回 1 。 如果 member 元素不是集合的成員,或 key 不存在,返回 0 SISMEMBER key member SPOP 移除并返回集合中的一個隨機元素。 SPOP key SRANDMEMBER 只提供 key 參數(shù)時,返回一個元素; 如果提供了 count 參數(shù),那么返回一個數(shù)組;如果集合為空,返回空數(shù)組 SRANDMEMBER key [count] SREM 移除集合 key 中的一個或多個 member 元素,不存在的 member 元素會被忽略。 SREM key member [member …] SMOVE 將 member 元素從 source 集合移動到 destination 集合。 SMOVE source destination member SCARD 返回集合 key 的基數(shù)(集合中元素的數(shù)量)。 SCARD key SMEMBERS 返回集合 key 中的所有成員。 SMEMBERS key SINTER 返回一個集合的全部成員,該集合是所有給定集合的交集。不存在的 key 被視為空集。當(dāng)給定集合當(dāng)中有一個空集時,結(jié)果也為空集(根據(jù)集合運算定律)。 SINTER key [key …] SINTERSTORE 這個命令類似于 SINTER命令,但它將結(jié)果保存到 destination 集合,而不是簡單地返回結(jié)果集。 SINTERSTORE destination key [key …] SUNION 返回一個集合的全部成員,該集合是所有給定集合的并集。 SUNION key [key …] SDIFF 返回一個集合的全部成員,該集合是所有給定集合之間的差集。 SDIFF key [key …] -
實戰(zhàn)場景
- 標(biāo)簽(tag):給用戶添加標(biāo)簽,或者用戶給消息添加標(biāo)簽,這樣有同一標(biāo)簽或者類似標(biāo)簽的可以給推薦關(guān)注的事或者關(guān)注的人
- 點贊,或點踩,收藏等,可以放到set中實現(xiàn)
Hash散列
一個 string 類型的 field(字段) 和 value(值) 的映射表,hash 特別適合用于存儲對象。
-
命令使用
命令簡述使用 HSET 將哈希表 hash 中域 field 的值設(shè)置為 value 。如果給定的哈希表并不存在, 那么一個新的哈希表將被創(chuàng)建并執(zhí)行 HSET 操作。如果域 field 已經(jīng)存在于哈希表中, 那么它的舊值將被新值 value 覆蓋。 HSET hash field value HSETNX 當(dāng)且僅當(dāng)域 field 尚未存在于哈希表的情況下, 將它的值設(shè)置為 value ;如果給定域已經(jīng)存在于哈希表當(dāng)中, 那么命令將放棄執(zhí)行設(shè)置操作;如果哈希表 hash 不存在, 那么一個新的哈希表將被創(chuàng)建并執(zhí)行 HSETNX 命令 HSETNX hash field value HGET HGET 命令在默認情況下返回給定域的值。 HGET hash field HEXISTS 檢查給定域 field 是否存在于哈希表 hash 當(dāng)中。 HEXISTS hash field HDEL 刪除哈希表 key 中的一個或多個指定域,不存在的域?qū)⒈缓雎浴?/td> HDEL key field [field …] HLEN 返回哈希表 key 中域的數(shù)量。 HLEN key HINCRBY 為哈希表 key 中的域 field 的值加上增量 increment 。 HINCRBY key field increment HMSET 同時將多個 field-value (域-值)對設(shè)置到哈希表 key 中。 HMSET key field value [field value …] HMGET 返回哈希表 key 中,一個或多個給定域的值。 HMGET key field [field …] HVALS 返回哈希表 key 中所有域的值。 HVALS key HGETALL 返回哈希表 key 中,所有的域和值。 HGETALL key -
實戰(zhàn)場景
- 緩存:可以更方便更直觀地維護緩存信息,如果用戶信息等,并且比String更節(jié)省空間,方便管理。
Zset有序集合
Redis 有序集合和集合一樣也是 string 類型元素的集合,且不允許重復(fù)的成員。不同的是每個元素都會關(guān)聯(lián)一個 double 類型的分數(shù)。redis 正是通過分數(shù)來為集合中的成員進行從小到大的排序。
-
命令使用
命令簡述使用 ZADD 將一個或多個 member 元素及其 score 值加入到有序集 key 當(dāng)中。 ZADD key score member [[score member] [score member] …] ZSCORE 返回有序集 key 中,成員 member 的 score 值。 ZSCORE key member ZINCRBY 為有序集 key 的成員 member 的 score 值加上增量 increment 。 ZINCRBY key increment member ZREVRANGE 返回有序集 key 中,指定區(qū)間內(nèi)的成員。 ZREVRANGE key start stop [WITHSCORES] ZREM 移除有序集 key 中的一個或多個成員,不存在的成員將被忽略。 ZREM key member [member …] ZRANK 返回有序集 key 中成員 member 的排名。其中有序集成員按 score 值遞增(從小到大)順序排列。 ZRANK key member ZREVRANK 返回有序集 key 中成員 member 的排名。其中有序集成員按 score 值遞減(從大到小)排序。 ZREVRANK key member ZREMRANGEBYRANK 移除有序集 key 中,指定排名(rank)區(qū)間內(nèi)的所有成員。 ZREMRANGEBYRANK key start stop ZREMRANGEBYSCORE 移除有序集 key 中,所有 score 值介于 min 和 max 之間(包括等于 min 或 max )的成員。 ZREMRANGEBYSCORE key min max -
實戰(zhàn)場景
- 排行榜:可以應(yīng)用于一些需要排序的排行榜中,例如常見的微博熱搜等。
三、特殊的數(shù)據(jù)類型
三種特殊類型講解
除了上文中的五種基礎(chǔ)數(shù)據(jù)類型,還有三種特殊的數(shù)據(jù)類型分別是 **HyperLogLog(基數(shù)統(tǒng)計), Bitmaps (位圖) 和 geospatial (地理位置)。
HyperLogLog
Redis 2.8.9 版本更新:新增Hyperloglog 數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)存入后無法取出,只能用于基數(shù)的統(tǒng)計
-
什么是基數(shù)?
例如,A={1, 3, 5, 7, 5, 7, 8,}那么基數(shù)(不重復(fù)的元素)為5,基數(shù)集為{1,3,5,7,8};基數(shù)估計就是在誤差可接受的范圍內(nèi),快速計算基數(shù)。
-
HyperLogLog 基數(shù)統(tǒng)計用來解決什么問題?
作為一個高級不精確去重的數(shù)據(jù)結(jié)構(gòu),它常常用于統(tǒng)計數(shù)據(jù)時去重的操作。特點是可以利用極小的內(nèi)存空間完成獨立總數(shù)的統(tǒng)計,比如注冊 IP 數(shù)、每日訪問 IP 數(shù)、頁面實時UV、在線用戶數(shù),共同好友數(shù)等。
-
優(yōu)勢體現(xiàn)在哪?
一個大型網(wǎng)站中需要統(tǒng)計IP數(shù),粗略計算一個IP消耗10字節(jié),100萬的IP就是15MB,而HyperLogLog在Redis中每個鍵占用的內(nèi)容都是12字節(jié),理論存儲近似接近264個值,它基于一個基數(shù)估算的算法,只能比較準(zhǔn)確的估算出基數(shù),仍然會存在一定不可避免的誤差,但一個帶有0.81%誤差的近似值在實際應(yīng)用場景也是可以接受的。
-
命令使用
命令簡述使用返回值 PFADD 將任意數(shù)量的元素添加到指定的 HyperLogLog 里面 PFADD key element [element …] 整數(shù)回復(fù): 如果 HyperLogLog 的內(nèi)部儲存被修改了, 那么返回 1 , 否則返回 0 PFCOUNT 命令作用于單個鍵時, 返回儲存在給定鍵的 HyperLogLog 的近似基數(shù), 如果鍵不存在, 那么返回 0 PFCOUNT key [key …] 返回的可見集合(observed set)基數(shù)并不是精確值, 而是一個帶有 0.81% 標(biāo)準(zhǔn)錯誤(standard error)的近似值。 PFMERGE 將多個 HyperLogLog 合并(merge)為一個 HyperLogLog , 合并后的 HyperLogLog 的基數(shù)接近于所有輸入 HyperLogLog 的可見集合(observed set)的并集。 PFMERGE destkey sourcekey [sourcekey …] 字符串回復(fù):返回 OK
Bitmap
itmap 即位圖數(shù)據(jù)結(jié)構(gòu),都是操作二進制位來進行記錄,只有0 和 1 兩個狀態(tài)。
-
Bitmap位存儲可以解決什么問題?
由于只能存儲0和1兩個數(shù)據(jù),所以它適用于一些只需記錄狀態(tài)而不需要記錄具體數(shù)值的數(shù)據(jù),如用戶登錄狀態(tài)等。
-
命令使用
命令簡述使用返回值 SETBIT 對 key 所儲存的字符串值,設(shè)置或清除指定偏移量上的位(bit);位的設(shè)置或清除取決于 value 參數(shù),可以是 0 也可以是 1 ;當(dāng) key 不存在時,自動生成一個新的字符串值。字符串會進行伸展(grown)以確保它可以將 value 保存在指定的偏移量上,當(dāng)字符串值進行伸展時,空白位置以 0 填充。 SETBIT key offset value 指定偏移量原來儲存的位 GETBIT 對 key 所儲存的字符串值,獲取指定偏移量上的位(bit),當(dāng) offset 比字符串值的長度大,或者 key 不存在時,返回 0 。 GETBIT key offset 字符串值指定偏移量上的位(bit) BITCOUNT 計算給定字符串中,被設(shè)置為 1 的比特位的數(shù)量。 BITCOUNT key [start] [end] 被設(shè)置為 1 的位的數(shù)量。 BITPOS 返回位圖中第一個值為 bit 的二進制位的位置。 BITPOS key bit [start] [end] 整數(shù)回復(fù)。 BITOP 對一個或多個保存二進制位的字符串 key 進行位元操作,并將結(jié)果保存到 destkey 上。operation 可以是 AND 、 OR 、 NOT 、 XOR 這四種操作中的任意一種: BITOP AND destkey key [key ...] :對一個或多個 key 求邏輯并,并將結(jié)果保存到 destkey ;BITOP OR destkey key [key ...] :對一個或多個 key 求邏輯或,并將結(jié)果保存到 destkey ; BITOP XOR destkey key [key ...] :對一個或多個 key 求邏輯異或,并將結(jié)果保存到 destkey ; BITOP NOT destkey key:對給定 key 求邏輯非,并將結(jié)果保存到 destkey BITOP operation destkey key [key …] 保存到 destkey 的字符串的長度,和輸入 key 中最長的字符串長度相等。 -
實戰(zhàn)應(yīng)用
-
使用Bitmap實現(xiàn)用戶上線次數(shù)的統(tǒng)計
假設(shè)我們希望記錄網(wǎng)站上用戶的上線頻率,可以通過SETBIT key offset value 和 BITCOUNT key [start] [end]來實現(xiàn)。
比如,當(dāng)用戶某一天上線時,我們就使用SETBIT key offset value ,將用戶名username作為key,將那天的上線日作為offset參數(shù),并將這個offset賦值value為1
當(dāng)需要計算某用戶的上線次數(shù)時,就使用 BITCOUNT key [start] [end]命令,執(zhí)行:BITCOUNT username計算得出結(jié)果就是該用戶的總上線天數(shù)。
性能分析
上述案例中,即使網(wǎng)站運行10年,每個用戶存儲的信息占用的空間也不過10*365bit,也就是每個用戶456字節(jié),對于這種大小的數(shù)據(jù)處理速度相當(dāng)快速。
-
Geospatial
Redis 3.2 .0版本更新:新增Geospatial 數(shù)據(jù)結(jié)構(gòu),可以用于推算地理位置的信息
常用命令
-
GEOADD key longitude latitude member [longitude latitude member …]
GEOADD 命令以標(biāo)準(zhǔn)的 x,y 格式接受參數(shù), 所以用戶必須先輸入經(jīng)度, 然后再輸入緯度。 GEOADD 能夠記錄的坐標(biāo)是有限的: 非常接近兩極的區(qū)域是無法被索引的。 精確的坐標(biāo)限制由 EPSG:900913 / EPSG:3785 / OSGEO:41001 等坐標(biāo)系統(tǒng)定義, 具體如下:
- 有效的經(jīng)度介于 -180 度至 180 度之間。
- 有效的緯度介于 -85.05112878 度至 85.05112878 度之間。
-
GEOPOS key member [member …]
從鍵里面返回所有給定位置元素的位置(經(jīng)度和緯度)。
因為 GEOPOS 命令接受可變數(shù)量的位置元素作為輸入, 所以即使用戶只給定了一個位置元素, 命令也會返回數(shù)組回復(fù)。
GEOPOS 命令返回一個數(shù)組, 數(shù)組中的每個項都由兩個元素組成: 第一個元素為給定位置元素的經(jīng)度, 而第二個元素則為給定位置元素的緯度。 當(dāng)給定的位置元素不存在時, 對應(yīng)的數(shù)組項為空值。
127.0.0.1:6379> GEOPOS china:city shenzhen hangzhou 1) 1) "144.05000120401382446"2) "22.5200000879503861" 2) 1) "120.1600000262260437"2) "30.2400003229490224" -
GEODIST key member1 member2 [unit]
返回兩個給定位置之間的距離。
指定單位的參數(shù) unit 必須是以下單位的其中一個:
- m 表示單位為米。
- km 表示單位為千米。
- mi 表示單位為英里。
- ft 表示單位為英尺。
如果用戶沒有顯式地指定單位參數(shù), 那么 GEODIST 默認使用米作為單位。
127.0.0.1:6379> GEODIST china:city shenzhen hangzhou "2524417.0471" -
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
以給定的經(jīng)緯度為中心, 返回鍵包含的位置元素當(dāng)中, 與中心的距離不超過給定最大距離的所有位置元素。
在給定以下可選項時, 命令會返回額外的信息:
- WITHDIST : 在返回位置元素的同時, 將位置元素與中心之間的距離也一并返回。 距離的單位和用戶給定的范圍單位保持一致。
- WITHCOORD : 將位置元素的經(jīng)度和維度也一并返回。
- WITHHASH : 以 52 位有符號整數(shù)的形式, 返回位置元素經(jīng)過原始 geohash 編碼的有序集合分值。 這個選項主要用于底層應(yīng)用或者調(diào)試, 實際中的作用并不大。
命令默認返回未排序的位置元素。 通過以下兩個參數(shù), 用戶可以指定被返回位置元素的排序方式:
- ASC : 根據(jù)中心的位置, 按照從近到遠的方式返回位置元素。
- DESC : 根據(jù)中心的位置, 按照從遠到近的方式返回位置元素。
在默認情況下, GEORADIUS 命令會返回所有匹配的位置元素。 雖然用戶可以使用 COUNT <count> 選項去獲取前 N 個匹配元素, 但是因為命令在內(nèi)部可能會需要對所有被匹配的元素進行處理, 所以在對一個非常大的區(qū)域進行搜索時, 即使只使用 COUNT 選項去獲取少量元素, 命令的執(zhí)行速度也可能會非常慢。 但是從另一方面來說, 使用 COUNT 選項去減少需要返回的元素數(shù)量, 對于減少帶寬來說仍然是非常有用的。
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km 1) "xian" 2) "hangzhou" 127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist 1) 1) "xian"2) "483.8340" 2) 1) "hangzhou"2) "977.5143" 127.0.0.1:6379> geoadd china:city 118.76 32.04 manjing 112.55 37.86 taiyuan 123.43 41.80 shenyang (integer) 3 127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist count 2 1) 1) "xian"2) "483.8340" 2) 1) "manjing"2) "864.9816" 127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist withcoord count 2 1) 1) "xian"2) "483.8340"3) 1) "108.96000176668167114"2) "34.25999964418929977" 2) 1) "manjing"2) "864.9816"3) 1) "118.75999957323074341"2) "32.03999960287850968" -
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
這個命令和 GEORADIUS 命令一樣, 都可以找出位于指定范圍內(nèi)的元素, 但是 GEORADIUSBYMEMBER 的中心點是由給定的位置元素決定的, 而不是像 GEORADIUS 那樣, 使用輸入的經(jīng)度和緯度來決定中心點。
127.0.0.1:6379> GEORADIUSBYMEMBER china:city taiyuan 1000 km withcoord withdist 1) 1) "manjing"2) "859.5256"3) 1) "118.75999957323074341"2) "32.03999960287850968" 2) 1) "taiyuan"2) "0.0000"3) 1) "112.54999905824661255"2) "37.86000073876942196" 3) 1) "xian"2) "514.2264"3) 1) "108.96000176668167114"2) "34.25999964418929977"
四、數(shù)據(jù)庫
常用指令
| EXISTS | 檢查給定 key 是否存在 | EXISTS key | 若 key 存在,返回 1 ,否則返回 0 |
| TYPE | 返回 key 所儲存的值的類型 | TYPE key | none (key不存在) 、string (字符串) 、list (列表) 、set (集合) 、zset (有序集) 、hash (哈希表) 、stream (流) |
| RENAME | 將 key 改名為 newkey ,當(dāng) key 和 newkey 相同,或者 key 不存在時,返回一個錯誤。 | RENAME key newkey | 改名成功時提示 OK ,失敗時候返回一個錯誤。 |
| RENAMENX | 當(dāng)且僅當(dāng) newkey 不存在時,將 key 改名為 newkey ;當(dāng) key 不存在時,返回一個錯誤 | RENAMENX key newkey | 修改成功時,返回 1 ; 如果 newkey 已經(jīng)存在,返回 0 。 |
| MOVE | 將當(dāng)前數(shù)據(jù)庫的 key 移動到給定的數(shù)據(jù)庫 db 當(dāng)中,如果當(dāng)前數(shù)據(jù)庫(源數(shù)據(jù)庫)和給定數(shù)據(jù)庫(目標(biāo)數(shù)據(jù)庫)有相同名字的給定 key ,或者 key 不存在于當(dāng)前數(shù)據(jù)庫,那么 MOVE 沒有任何效果 | MOVE key db | 移動成功返回 1 ,失敗則返回 0 。 |
| DEL | 刪除給定的一個或多個 key ,不存在的 key 會被忽略。 | DEL key [key …] | 被刪除 key 的數(shù)量 |
| DBSIZE | 返回當(dāng)前數(shù)據(jù)庫的 key 的數(shù)量。 | DBSIZE | 當(dāng)前數(shù)據(jù)庫的 key 的數(shù)量。 |
| KEYS | 查找所有符合給定模式 pattern 的 key , 比如說: KEYS * 匹配數(shù)據(jù)庫中所有 key ;KEYS h?llo 匹配 hello , hallo 和 hxllo 等; KEYS h*llo 匹配 hllo 和 heeeeello 等; KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo | KEYS pattern | 符合給定模式的 key 列表。 |
| FLUSHDB | 清空當(dāng)前數(shù)據(jù)庫中的所有 key,此命令從不失敗 | FLUSHDB | 總是返回 OK 。 |
| FLUSHALL | 清空整個 Redis 服務(wù)器的數(shù)據(jù)(刪除所有數(shù)據(jù)庫的所有 key ),此命令從不失敗 | FLUSHALL | 總是返回 OK 。 |
| SELECT | 切換到指定的數(shù)據(jù)庫,數(shù)據(jù)庫索引號 index 用數(shù)字值指定,以 0 作為起始索引值,默認使用 0 號數(shù)據(jù)庫 | SELECT index | OK |
| SWAPDB db1 db2 | 對換指定的兩個數(shù)據(jù)庫, 使得兩個數(shù)據(jù)庫的數(shù)據(jù)立即互換 | SWAPDB db1 db2 | OK |
設(shè)置鍵的生存時間或過期時間
通過EXPIRE 或者PEXPIRE命令,客戶端可以以秒或者毫秒精度為數(shù)據(jù)庫中的某個鍵設(shè)置生存時間(Time To Live,后面簡稱TTL),在經(jīng)過了指定的時間后服務(wù)器就會刪除生存時間為0的鍵。
127.0.0.1:6379> SET key value OK 127.0.0.1:6379> EXPIRE key 5 //設(shè)置過期時間為5s (integer) 1 127.0.0.1:6379> get key //5s之內(nèi) "value" 127.0.0.1:6379> get key //5s后 (nil)在之前提到的SETEX命令可以在設(shè)置一個字符串鍵的同時為鍵設(shè)置過期時間,這個命令是一個限制了類型的命令,只適用于字符串類型的數(shù)據(jù),但SETEX命令設(shè)置過期時間的原理與EXPIRE命令設(shè)置過期時間的原理是完全一致的。
TTL 和PTTL命令可以接收一個待用生存時間或者過期時間的鍵,返回這個鍵的剩余生存時間。
127.0.0.1:6379> EXPIRE key 1000 (integer) 1 127.0.0.1:6379> TTL key (integer) 996EXPIREAT key timestamp命令可以設(shè)置鍵key的過期時間為timestamp指定的秒數(shù)時間戳
PEXPIREAT key timestamp命令可以設(shè)置鍵key的過期時間為timestamp指定的毫秒數(shù)時間戳
雖然有多種不同的單位以及不同形式的設(shè)置命令,但實際上EXPIRE PEXPIRE EXPIREAT都是基于PEXPIREAT來實現(xiàn)的,所以客戶端中無論執(zhí)行的是上面四個指令中的哪一個,經(jīng)過轉(zhuǎn)換后,執(zhí)行的效果與PEXPIREAT指令效果相同。
保存設(shè)置時間
RedisDB結(jié)構(gòu)的expires字典保存了數(shù)據(jù)庫中所有鍵的過期時間,稱之為過期字典
- 過期字典的鍵是一個指針,這個指針指向鍵空間的某個鍵對象,即某個數(shù)據(jù)庫的鍵
- 過期字典的值是一個long long類型的整數(shù),這個整數(shù)保存了鍵指向的數(shù)據(jù)庫鍵的過期時間——一個毫秒精度的UNIX時間戳
移除過期時間
PERSIST命令可以移除一個鍵的過期時間
127.0.0.1:6379> EXPIRE key 1989 (integer) 1 127.0.0.1:6379> TTL key (integer) 1987 127.0.0.1:6379> PERSIST key (integer) 1 127.0.0.1:6379> ttl key (integer) -1PERSIST命令就是PEXPIREAT命令的反操作,PERSIST命令在過期字典中查找給定的鍵,并接觸鍵和值(過期時間)在過期字典中的關(guān)聯(lián)。
過期數(shù)據(jù)的刪除策略
如果一個鍵過期了,那么它什么時候會被刪除呢?
這個問題有三種可能的答案,分別代表了三種不同的過期刪除策略:
- 定時刪除:在設(shè)置鍵的過期時間時,創(chuàng)建一個定時器(timer),讓定時器在鍵過期時間來臨時,立刻執(zhí)行對鍵的刪除
- 惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵,都會檢查取得的鍵是否過期,如果過期,就刪除該鍵,否則就返回該鍵
- 定期刪除:每隔一段時間,程序就對數(shù)據(jù)庫進行一次檢查,刪除里面的過期鍵,至于要刪除多少過期鍵,檢查多少個數(shù)據(jù)庫,如何檢查(因為數(shù)據(jù)量龐大時,不可能每個數(shù)據(jù)都進行檢查,要設(shè)計算法隨機檢查),則由算法決定。
以上三種刪除策略中,第一、三種屬于主動刪除,第二種則屬于被動刪除。
定時刪除
定時刪除策略對內(nèi)存是最友好的,屬于用時間換空間的一種策略,通過使用定時器,定時刪除鍵會保證過期鍵盡快被刪除,并釋放過期鍵占用的內(nèi)存。
另一方面,定時刪除策略的缺點是,它對CPU時間是最不友好的,在過期鍵數(shù)量龐大時,刪除過期鍵會占用一部分CPU處理時間,內(nèi)存不緊張但是CPU壓力大時,采用這種策略無疑會對服務(wù)器響應(yīng)時間和吞吐量產(chǎn)生相當(dāng)明顯的負面影響。
如果有大量的請求命令在等待服務(wù)器處理,而此時服務(wù)器當(dāng)前并不缺少內(nèi)存, 那么服務(wù)器應(yīng)該優(yōu)先將CPU占用分配給請求處理,而不是用在刪除過期鍵上。
除此之外,創(chuàng)建一個定時器需要用到Redis服務(wù)器中的時間事件,當(dāng)前時間事件的實現(xiàn)方式——無序鏈表,查找一個事件的時間復(fù)雜度為O(N),并不能高效地處理時間事件。
因此要讓服務(wù)器創(chuàng)建大量的定時器來執(zhí)行定時刪除策略,在現(xiàn)階段是不現(xiàn)實的。
惰性刪除
惰性刪除策略對CPU時間來說是最友好的:程序只會在取出鍵的時候才會對鍵進行過期檢查,這可以保證刪除鍵的操作只會在非做不可的情況下才進行,并且刪除的目標(biāo)僅限于當(dāng)前鍵,并不是大量地刪除,這個策略不會讓CPU在刪除無關(guān)的鍵上花費過多時間。
但是這種策略的確定是,它對內(nèi)存是最不友好的,如果數(shù)據(jù)庫中有相當(dāng)龐大數(shù)量的過期鍵并且占著大量的內(nèi)存,而這些鍵如果不被訪問到的話,它可能永遠不會被刪除,它所占據(jù)的內(nèi)存就永遠不會被釋放,服務(wù)器的內(nèi)存空間會被這些垃圾數(shù)據(jù)一直占據(jù)吞噬,服務(wù)器內(nèi)存也會越來越緊張。
舉個例子,對于一些和事件有關(guān)的數(shù)據(jù),比如日志,在某個時間點之后,它的訪問量就會大大減少,甚至不再訪問,如果這類過期數(shù)據(jù)一直堆積在數(shù)據(jù)庫中,那么造成的影響肯定是非常嚴(yán)重的。
定期刪除
從上述對兩種刪除策略的討論后得知,這兩種策略都是相當(dāng)極端的,在同樣極端的情況下會造成嚴(yán)重的后果。
而定期刪除策略是這兩種策略的一種整合和折中:
- 定期刪除策略每隔一段時間執(zhí)行一次過期鍵刪除,并通過限制刪除操作執(zhí)行的時間和頻率來減少操作對CPU的影響
- 除此之外,定期刪除過期鍵,有效地減少了過期鍵帶來的內(nèi)存浪費,避免了內(nèi)存泄漏的危險
定期刪除策略的難點是確定刪除操作執(zhí)行時長和頻率:
- 如果刪除得太頻繁或執(zhí)行時間過長,定期刪除策略會退化成定時刪除策略
- 如果刪除操作執(zhí)行的太少或者執(zhí)行時間過少,又會跟惰性刪除策略一樣出現(xiàn)內(nèi)存浪費的情況
因此,如果采用定期刪除策略,服務(wù)器必須根據(jù)情況,合理設(shè)置刪除操作的時間和執(zhí)行頻率。
五、持久化
Redis作為一個緩存組件為什么需要持久化?
Redis是一個基于內(nèi)存的數(shù)據(jù)庫,如果服務(wù)出現(xiàn)宕機的情況,數(shù)據(jù)將全部丟失,通常的解決方案是通過后端數(shù)據(jù)庫恢復(fù),但是后端數(shù)據(jù)庫如常見的Mysql數(shù)據(jù)庫有性能瓶頸,如果是大量丟失數(shù)據(jù)的恢復(fù),會對數(shù)據(jù)庫造成相當(dāng)大的壓力,開銷大效率低下,所以對于Redis實現(xiàn)數(shù)據(jù)持久化是相當(dāng)重要的,在出現(xiàn)數(shù)據(jù)丟失災(zāi)難中可以避免從后端數(shù)據(jù)庫恢復(fù)數(shù)據(jù)。
Redis提供了多種持久化方式
- RDB持久化可以在指定的時間間隔里生成數(shù)據(jù)集的快照(point-in-time-snapshot)并保存到磁盤上,由于是某一時刻的快照,所以快照中的數(shù)據(jù)要早于或等于內(nèi)存中的數(shù)據(jù)
- AOF持久化記錄服務(wù)器執(zhí)行的所有寫操作命令并以aof格式保存在磁盤上,并在服務(wù)器啟動時,通過執(zhí)行這些命令來還原數(shù)據(jù)集。AOF文件中的命令全部以Redis協(xié)議的格式來保存,新命令會追加到文件的末尾。Redis還可以使用BGREWRITEAOF命令來重寫文件,去除一些不影響最終數(shù)據(jù)結(jié)果的命令,這樣保證AOF文件保存的數(shù)據(jù)集占用內(nèi)存不會過大
- Redis還可以同時使用AOF和RDB來實現(xiàn)持久化,這是Redis4.0版本推出的,官方也支持在實際開發(fā)中使用這種用法。簡單來說,內(nèi)存快照以一定的頻率執(zhí)行,在兩次快照之間,使用 AOF 日志記錄這期間的所有命令操作。在這種情況下,快照不用很頻繁地執(zhí)行,避免了頻繁的fork對主線程的影響,避免主線程阻塞,也不需要記錄所有操作了,因此不會出現(xiàn)文件過大的問題,也可以避免重寫開銷過大。
- 虛擬內(nèi)存(VM)方式存儲,從Redis Version2.4開始,官方就明確表示不再使用,Version 3.2版本中更找不到關(guān)于虛擬內(nèi)存(VM)的任何配置范例,Redis的主要作者Salvatore Sanfilippo還專門寫了一篇論文,來反思Redis對虛擬內(nèi)存(VM)存儲技術(shù)的支持問題。
RDB持久化
RDB 就是 Redis DataBase 的縮寫,中文名為快照/內(nèi)存快照,RDB持久化是把當(dāng)前進程數(shù)據(jù)生成快照保存到磁盤上的過程
觸發(fā)方式
觸發(fā)RDB持久化的方式有兩種,分別是手動觸發(fā)和自動觸發(fā)
-
手動觸發(fā)
手動觸發(fā)分別對應(yīng)save和bgsave命令
-
save命令
阻塞當(dāng)前Redis服務(wù)器主線程,直至RDB過程完成位置,當(dāng)數(shù)據(jù)占用內(nèi)存較大時,這個過程會造成相當(dāng)長時間的阻塞,并且如果出現(xiàn)服務(wù)器宕機,這個過程中寫入的所有數(shù)據(jù)都會丟失,線上環(huán)境中慎重使用!
-
bgsave命令
Redis進程執(zhí)行fork操作時會創(chuàng)建子進程,RDB持久化過程由子進程負責(zé),完成后自動結(jié)束。這個過程中的保存工作全部由子進程完成,主進程無需進行任何磁盤的I/O操作。但是如果數(shù)據(jù)集相當(dāng)龐大時,fork可能會非常耗時,并且在CPU緊張時,服務(wù)器可能會在某時刻停止處理服務(wù)器,這種停止情況甚至可能長達整整一秒。
bgsave命令執(zhí)行期間,客戶端的save命令和bgsave命令會被服務(wù)器拒絕,因為要防止保存命令之間產(chǎn)生競爭條件。
其次bgsave和bgrewriteaof兩個命令不能同時執(zhí)行:
- 如果bgsave命令正在執(zhí)行,那么客戶端bgrewriteaof命令會延遲到bgsave命令完成后執(zhí)行
- 如果bgrewriteaof命令正在執(zhí)行,那么客戶端發(fā)送的bgsave命令會被拒絕
bgsave流程圖如下圖所示:
具體流程如下:
- Redis客戶端執(zhí)行bgsave命令或者自動觸發(fā)bgsave命令
- 主進程判斷是否已經(jīng)存在已經(jīng)在執(zhí)行的子進程,如果存在直接返回,父進程繼續(xù)處理命令請求,等待子進程完成快照任務(wù)后的信號;
- 如果不存在正在執(zhí)行的子進程,那么就調(diào)用fork函數(shù)創(chuàng)建一個新的子進程進程持久化,fork過程是一個阻塞的,創(chuàng)建完子進程主進程即可執(zhí)行其他操作
- 子進程先將數(shù)據(jù)寫到臨時的rdb文件,待快照數(shù)據(jù)寫入完成后再原子替換舊的rdb文件
- 發(fā)送信號給主進程,通知主進程rdb持久化已經(jīng)完成,主進程更新相關(guān)的統(tǒng)計信息。
-
-
自動觸發(fā)
以下四種情況會自動觸發(fā)
- redis.conf中配置的save m n,即在m秒中發(fā)生了n次數(shù)據(jù)修改,則會觸發(fā)bgsave命令生成rdb文件
- 主從復(fù)制時,從節(jié)點要從主節(jié)點中進行全量復(fù)制也會觸發(fā)bgsave操作,生成當(dāng)時的快照發(fā)送到從節(jié)點,進行主從復(fù)制的數(shù)據(jù)同步
- 執(zhí)行debug reload命令重新加載reids時也會觸發(fā)bgsave操作
- 默認情況下執(zhí)行shutdown命令,如果沒有開啟AOF持久化,也會自動觸發(fā)bgsave操作
redis.conf配置RDB持久化
快照周期:內(nèi)存快照雖然可以通過技術(shù)人員手動執(zhí)行save和bgsave命令來執(zhí)行,但是實際生產(chǎn)環(huán)境下都會設(shè)置周期執(zhí)行條件
-
Redis中默認的周期設(shè)置
# 周期性執(zhí)行條件的設(shè)置格式為 save <seconds> <changes># 默認的設(shè)置為: save 900 1 //900s中有一條key信息發(fā)生變化執(zhí)行快照 save 300 10 //300s中有十條key信息發(fā)生變化執(zhí)行快照 save 60 10000 //60s中有10000條key信息發(fā)生變化執(zhí)行快照# 以下設(shè)置方式為關(guān)閉RDB快照功能 save "" -
其他配置信息
#文件名稱 dbfilename "dump-6379.rdb"#端口號 port 6379#文件保存路徑 dir /www/server/redis/data/redis_cache# 如果持久化出錯,主進程是否停止寫入 stop-writes-on-bgsave-error yes#是否壓縮 rdbcompression yes#導(dǎo)入時進行檢查 rdbchecksum yesdbfilename:RDB文件在磁盤上的名稱。
dir:RDB文件的存儲路徑。默認設(shè)置為“./”,也就是Redis服務(wù)的主目錄。
stop-writes-on-bgsave-error:上文提到的在快照進行過程中,主進程照樣可以接受客戶端的任何寫操作的特性,是指在快照操作正常的情況下。如果快照操作出現(xiàn)異常(例如操作系統(tǒng)用戶權(quán)限不夠、磁盤空間寫滿等等)時,Redis就會禁止寫操作。這個特性的主要目的是使運維人員在第一時間就發(fā)現(xiàn)Redis的運行錯誤,并進行解決。一些特定的場景下,您可能需要對這個特性進行配置,這時就可以調(diào)整這個參數(shù)項。該參數(shù)項默認情況下值為yes,如果要關(guān)閉這個特性,指定即使出現(xiàn)快照錯誤Redis一樣允許寫操作,則可以將該值更改為no。
rdbcompression:該屬性將在字符串類型的數(shù)據(jù)被快照到磁盤文件時,啟用LZF壓縮算法。Redis官方的建議是請保持該選項設(shè)置為yes,因為“it’s almost always a win”。
rdbchecksum:從RDB快照功能的version 5 版本開始,一個64位的CRC冗余校驗編碼會被放置在RDB文件的末尾,以便對整個RDB文件的完整性進行驗證。這個功能大概會多損失10%左右的性能,但獲得了更高的數(shù)據(jù)可靠性。所以如果您的Redis服務(wù)需要追求極致的性能,就可以將這個選項設(shè)置為no。
深入理解RDB
-
在生產(chǎn)環(huán)境中為Redis開辟的內(nèi)存區(qū)域都比較大,那么將內(nèi)存中的數(shù)據(jù)同步到硬盤的過程可能就會持續(xù)較長的時間,而實際情況中,這段時間都會收到數(shù)據(jù)的讀寫請求,那么如何保持數(shù)據(jù)一致性呢?
RDB的核心思想是Copy-On-Write,來保證進行快照操作的時間里需要壓縮寫入磁盤的的數(shù)據(jù)在內(nèi)存中不會發(fā)生變化。正常的快照操作中,一方面Redis主進程會fork一個快照子進程來做持久化,保持Redis不會停止對客戶端的數(shù)據(jù)請求處理,另一方面這段時間內(nèi)的數(shù)據(jù)變化會以副本的方式放在一個新的內(nèi)存區(qū)域,與此同時bgsave進程會將該副本寫入rdb文件,等待快照操作執(zhí)行完畢后才會同步到原來的數(shù)據(jù)存儲內(nèi)存區(qū)域。
-
在進行快照操作的這段時間里,如果服務(wù)器發(fā)生宕機怎么辦?
快照操作過程中不會影響上一次的備份數(shù)據(jù)rdb文件,Redis在進行快照操作的時候會創(chuàng)建一個臨時文件進行數(shù)據(jù)寫入,操作成功之后才會將這個臨時文件覆蓋掉上一次的備份文件。而如果快照操作中服務(wù)器發(fā)生宕機,服務(wù)器重啟時將以上次的備份文件作為數(shù)據(jù)恢復(fù)參考,而服務(wù)器宕機之前更新的數(shù)據(jù)則無法恢復(fù)會丟失。
-
參照上述問題,服務(wù)器發(fā)生宕機會導(dǎo)致數(shù)據(jù)丟失,那么可以一秒做一次快照而減少數(shù)據(jù)丟失量嗎?
對于快照來說,所謂的“連拍”就是連續(xù)地進行快照持久化,這樣做,快照的間隔時間很短,確實可以減少丟失數(shù)據(jù)的數(shù)量。
那么快照間隔時間是不是可以縮短到很短呢?因為每次快照操作都是由bgsave子進程去完成的,并不會影響主進程對客戶端請求的處理,也不會阻塞主進程。
其實這種想法是錯誤的,雖然bgsave子進程執(zhí)行時不會阻塞主進程,但是如果頻繁地執(zhí)行全量快照,也會帶來其他的開銷:
- 頻繁將全量數(shù)據(jù)寫入磁盤,這樣大量數(shù)據(jù)的IO操作本身就會對磁盤造成相當(dāng)大的壓力,并且多個快照競爭有限的磁盤寬帶,前一個快照還沒執(zhí)行完,后一個快照又開始進行了,從而使得快照操作越來越慢,但是新的快照又開始進行了,從而導(dǎo)致惡性循環(huán)。
- bgsave子進程需要通過fork操作會從主進程中創(chuàng)建出來,雖然子進程創(chuàng)建過后不會阻塞主進程,但是fork子進程這個操作本身是會阻塞主進程的,而且如果主進程已經(jīng)被大量請求占用,而此時又頻繁地創(chuàng)建子進程,這樣也是會阻塞主進程的,只是方式與save有所不同,但本質(zhì)都是一樣的,這樣又回到了原來的問題。
RDB的優(yōu)缺點
- 優(yōu)點
- RDB文件是某個時間節(jié)點的快照,默認使用LZF算法進行壓縮,壓縮后的文件體積遠小于于內(nèi)存大小,適合備份、全量復(fù)制等場景。
- Redis加載RDB文件恢復(fù)數(shù)據(jù)遠遠快于AOF,因此RDB非常適用于災(zāi)難恢復(fù)(Disaster Recovery)。
- 缺點
- RDB方式實時性不夠,無法做到秒級的數(shù)據(jù)持久化,雖然Redis允許設(shè)置不同的保存條件來控制保存RDB文件的頻率,但是RDB文件需要做的是全量復(fù)制需要保存整個數(shù)據(jù)集,所以它并不是一個輕松的過程,當(dāng)服務(wù)器故障發(fā)生宕機,可能因此丟失相當(dāng)多的數(shù)據(jù)。
- 每次進行快照時,主進程需要fork()出一個子進程,由子進程來完成持久化工作。在數(shù)據(jù)集比較龐大時,fork()過程都可能會非常耗時,造成服務(wù)器某時刻停止對服務(wù)器的請求處理。
AOF持久化
Redis是寫后日志,先執(zhí)行命令,將數(shù)據(jù)寫入內(nèi)存,再記錄日志,日志里記錄的是Redis收到的每一條命令
為什么采用寫后日志?
- 避免額外的檢查開銷:Redis在想AOF里面記錄日志時,并不會先去執(zhí)行對命令的語法檢測,所以先記錄日志再執(zhí)行命令的話,日志中有可能記錄下錯誤的命令,當(dāng)Redis利用日志恢復(fù)數(shù)據(jù)時,可能就會出錯。
- 不會阻塞當(dāng)前的寫操作
當(dāng)也同樣存在風(fēng)險:
- 如果命令執(zhí)行完即將寫入日志時,服務(wù)器宕機,這些數(shù)據(jù)操作未能寫入日志,則這些數(shù)據(jù)將丟失。
實現(xiàn)AOF
AOF日志記錄Redis的每個寫操作,因此不需要觸發(fā),具體步驟分為命令追加append,文件寫入write和文件同步sync以及文件重寫rewrite
-
命令追加append
當(dāng)AOF持久化功能開啟后,服務(wù)器在執(zhí)行完一個寫命令之后,會將寫命令追加到服務(wù)器的aof_buf緩沖區(qū),而不是直接寫入文件,主要是避免了每次寫命令直接寫入磁盤,導(dǎo)致磁盤IO操作成為Redis負載的瓶頸。
命令追加的格式是Redis命令請求的協(xié)議格式,它是一種純文本格式,有兼容性好,可讀性強,容易處理等優(yōu)點。
-
文件寫入write和文件同步sync
Redis提供了多種AOF緩存區(qū)的文件同步策略,策略涉及到操作系統(tǒng)的write函數(shù)和fsync函數(shù),為了提高文件寫入效率,現(xiàn)代操作系統(tǒng)中,當(dāng)用戶調(diào)用write函數(shù)將數(shù)據(jù)寫入文件時,操作系統(tǒng)常會將數(shù)據(jù)暫存到一個內(nèi)存緩沖區(qū)中,當(dāng)緩沖區(qū)被存滿或者達到一定時限后才會寫入磁盤中,這樣的操作雖然提高了效率但是也存在安全隱患,如果服務(wù)器宕機,內(nèi)存緩沖區(qū)的數(shù)據(jù)將全部丟失。因此系統(tǒng)里也提供了fsync函數(shù)等同步函數(shù),可以強制將緩沖區(qū)的數(shù)據(jù)寫入磁盤,以此保證數(shù)據(jù)的安全性。
AOF持久化中aof_buf緩存區(qū)的文件同步策略由appendfsync參數(shù)控制,各個參數(shù)含義如下:
- always:命令寫入aof_buf后立刻調(diào)用系統(tǒng)的fsync函數(shù)同步到AOF文件,fsync操作完成后線程返回。這種情況下,aof_buf緩沖區(qū)已然失去作用,每次寫命令都要同步到AOF文件中,磁盤IO成為性能瓶頸,嚴(yán)重降低了Redis的性能;即便是用固態(tài)硬盤,每秒也只能處理幾萬個請求,而且會大大降低硬盤的壽命。
- no:命令寫入aof_buf后調(diào)用系統(tǒng)write操作,不對AOF文件進行fsync同步,同步操作由操作系統(tǒng)負責(zé),通常同步周期為30s。這種情況下,同步操作完全由系統(tǒng)控制,文件的同步時間變得不可控,而且緩沖區(qū)堆積的數(shù)據(jù)會很多,安全性無法得到保證。
- everysec:命令寫入aof_buf后調(diào)用系統(tǒng)的write,write完成后線程返回;fsync同步文件操作由專門的線程每秒調(diào)用一次。everysec是前述兩種策略的折中,是性能和數(shù)據(jù)安全性的平衡,實際開發(fā)中我們會優(yōu)先選擇這種策略。
-
文件重寫rewrite
Redis執(zhí)行的寫命令越來越大,AOF文件也會越來越大,過大的文件會影響服務(wù)器的運行,也會使文件恢復(fù)時用時過長。
文件重寫是指定期對AOF文件進行重寫,減少AOF文件的體積。Redis通過創(chuàng)建一個新的AOF文件來替換現(xiàn)有的AOF,新舊兩個AOF文件保存的數(shù)據(jù)相同,但新AOF文件沒有了冗余命令。
文件重寫之所以可以壓縮AOF文件原因在于:
-
過期的數(shù)據(jù)不再寫入文件
-
無效的命令不再寫入文件:如有些數(shù)據(jù)被重復(fù)設(shè)值,有些數(shù)據(jù)被刪除了等待
-
多條命令可以合并為一個:如sadd key value1,sadd key value2,sadd key value3可以合并為sadd key value1 value2 value3,不過為了防止單條命令過大造成客戶端緩沖區(qū)溢出,對于list,set,hash,zset類型的key,并不一定只使用一條命令,而是以某個常量為界將命令拆分為多條。這個常量在redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD中定義,不可更改,3.0版本中值是64。
#define REDIS_AOF_REWRITE_ITEMS_PRE_CMD 64
-
配置文件
#文件名稱 appendfilename "appendonly-6379.aof"# appendonly參數(shù)開啟AOF持久化 appendonly yes# 同步策略 # appendfsync always appendfsync everysec # appendfsync no# aof重寫期間是否同步 no-appendfsync-on-rewrite no# 重寫觸發(fā)配置 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb# 加載aof出錯如何處理 aof-load-truncated yes# 文件重寫策略 aof-rewrite-incremental-fsync yes要特別注意:no-appendfsync-on-rewrite:always和everysec的設(shè)置會使真正的I/O操作高頻度的出現(xiàn),甚至?xí)霈F(xiàn)長時間的卡頓情況,這個問題出現(xiàn)在操作系統(tǒng)層面上,所有靠工作在操作系統(tǒng)之上的Redis是沒法解決的。為了盡量緩解這個情況,Redis提供了這個設(shè)置項,保證在完成fsync函數(shù)調(diào)用時,不會將這段時間內(nèi)發(fā)生的命令操作放入操作系統(tǒng)的Page Cache(這段時間Redis還在接受客戶端的各種寫操作命令)。
重寫機制
-
AOF重寫會阻塞嗎?
AOF重寫過程是由后臺進程bgrewriteaof完成的,主線程需要fork出子進程,這個過程會占用主進程內(nèi)存,所以如果頻繁進行重寫是會造成主線程阻塞的。
-
AOF日志何時會重寫?
auto-aof-rewrite-min-size:表示運行AOF重寫文件的最小大小,默認為64MB
auto-aof-rewrite-percentage:這個值的計算方式是,當(dāng)前aof文件大小比上次重寫后的aof文件大小的差值與上次重寫后aof文件大小的比值;percentage = (last - now) / last
-
重寫日志時,有新的數(shù)據(jù)寫入怎么做?
關(guān)于文件重寫,要特別注意兩點:
- 重寫由子進程進行
- 重寫階段Redis執(zhí)行的寫命令需要追加到新的AOF文件中,為此Redis引入了aof_rewrite_buf緩存區(qū)
?
? 對比上圖,文件重寫的流程如下:
-
1>執(zhí)行AOF請求
如果當(dāng)前進程正在執(zhí)行bgrewriteaof,則返回請求,請求不執(zhí)行
如果當(dāng)前進程正在執(zhí)行bgsave,則重寫命令延遲到bgsave完成之后進行
-
2>父進程fork創(chuàng)建子進程,開銷相當(dāng)于bgsave創(chuàng)建子進程的開銷
-
3.1>主進程fork操作完成后繼續(xù)相應(yīng)其他命令
所有修改命令依然寫入aof_buf緩沖區(qū)根據(jù)appendfsync參數(shù)采取策略同步到磁盤,保證原有數(shù)據(jù)同步
-
3.2>fork操作運用寫時復(fù)制基數(shù),子進程只能共享fork操作時的內(nèi)存數(shù)據(jù)
由于父進程依然響應(yīng)命令,會有新的數(shù)據(jù)寫入,Redis使用aof_rewrite_buf重寫緩沖區(qū)來保存這部分數(shù)據(jù),防止新生成的文件生成期間丟失這部分數(shù)據(jù)。
-
4>子進程按照命令合并規(guī)則寫入到新的AOF文件
每次批量寫入的硬盤數(shù)據(jù)量由aof-rewrite-incremental-fsync參數(shù)控制,默認為32MB,防止單次寫入數(shù)據(jù)造成磁盤IO阻塞
-
5.1>新的AOF文件寫入完成后,發(fā)送信號給主進程,通知主進程更新統(tǒng)計信息
-
5.2>父進程將aof_rewrite_buf重寫緩沖區(qū)新寫入的數(shù)據(jù)更新到新的AOF文件中
-
5.3>使用新的AOF文件代替舊的AOF文件
總結(jié):
1.父進程fork子進程完成AOF文件重寫
2.父進程將新寫入的數(shù)據(jù)保存到aof_buf和aof_rewrite_buf緩沖區(qū),子進程重寫完畢后從aof_rewrite_buf緩存區(qū)將新的數(shù)據(jù)寫入新的AOF文件
3.新的AOF文件替代舊的AOF文件
-
為什么AOF重寫不復(fù)用舊的AOF文件?
1.父子進程寫同一個文件會產(chǎn)生競爭關(guān)系,影響了父進程的性能
2.如果AOF重寫失敗,會污染原本的AOF文件,無法再作為數(shù)據(jù)恢復(fù)的參考
重啟加載
AOF和RDB文件都可以用于服務(wù)器重啟時的數(shù)據(jù)恢復(fù)。下面展示Redis持久化文件加載流程:
流程說明:
1)AOF持久化開啟且存在AOF文件時,優(yōu)先加載AOF文件。
2)AOF關(guān)閉或者AOF文件不存在時,加載RDB文件。
3)加載AOF/RDB文件成功后,Redis啟動成功。
4)AOF/RDB文件存在錯誤時,Redis啟動失敗并打印錯誤信息。
那么為什么會優(yōu)先加載AOF呢?因為AOF保存的數(shù)據(jù)更完整,通過上面的分析我們知道AOF基本上最多損失1s的數(shù)據(jù)。
六、事務(wù)
Redis通過MULTI,EXEC,WATCH,DISCARD等命令來實現(xiàn)事務(wù)Transaction功能。事務(wù)體哦概念股了一種將多個命令請求打包,然后一次性按順序地執(zhí)行多個命令的機制,并且事務(wù)執(zhí)行期間,服務(wù)器不會中斷事務(wù)而去改去執(zhí)行其他客戶端的命令請求,它會將事務(wù)中的所有命令都執(zhí)行完畢,然后 才去執(zhí)行其他客戶端的命令請求。
以下是一個事務(wù)執(zhí)行的過程,該事務(wù)從一個MULTI開始,接著將多個操作命令放入事務(wù)中,然后最后EXEC將事務(wù)提交給服務(wù)器執(zhí)行。
127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set key value QUEUED 127.0.0.1:6379(TX)> get key QUEUED 127.0.0.1:6379(TX)> set key otherValue QUEUED 127.0.0.1:6379(TX)> get key QUEUED 127.0.0.1:6379(TX)> EXEC 1) OK 2) "value" 3) OK 4) "otherValue"特別注意:
- 事務(wù)是一個單獨的隔離操作:事務(wù)中的所有命令都會序列化,按順序地執(zhí)行,事務(wù)執(zhí)行過程中也不會被其他客戶端發(fā)來的命令請求打斷
- 事務(wù)是一個原子操作:事務(wù)中的命令要么全部被執(zhí)行,要么全部不執(zhí)行
EXEC命令復(fù)制觸發(fā)并執(zhí)行事務(wù)中的所有命令
- 如果客戶端使用MULTI開啟一個事務(wù)后,因為斷線導(dǎo)致EXEC沒有執(zhí)行,那么事務(wù)中的所有命令都不會被執(zhí)行
- 而如果EXEC成功執(zhí)行,那么事務(wù)中所有命令都會被執(zhí)行
當(dāng)使用AOF持久化時,Redis會使用單個write命令將事務(wù)寫入磁盤中,如果Redis服務(wù)器宕機,那么只有部分事務(wù)命令會成功寫入磁盤。
如果Redis重新啟動發(fā)現(xiàn)了AOF文件有這樣的問題,那么它會退出并匯報一個錯誤。
使用redis-check-aof可以修復(fù)這一問題,它會移除AOF文件中不完整事務(wù)信息,以保證服務(wù)器順利啟動。
放棄事務(wù)
當(dāng)執(zhí)行DISCARD命令時,事務(wù)會被放棄,事務(wù)隊列清空,并且客戶端從事務(wù)狀態(tài)退出:
127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set key value QUEUED 127.0.0.1:6379(TX)> DISCARD OK 127.0.0.1:6379> get key "otherValue"WATCH命令
WATCH命令是一個樂觀鎖Optimistic Locking,它可以在EXEC命令執(zhí)行之前,監(jiān)視任意順序的數(shù)據(jù)庫鍵,并在執(zhí)行EXEC命令時,檢查被監(jiān)視的值是否已經(jīng)被修改,如果是,服務(wù)器將拒絕執(zhí)行事務(wù),并向客戶端代表事務(wù)執(zhí)行已經(jīng)失敗的回復(fù)。
redis> set key oldValue OK redis> watch key OK redis> MULTI OK redis(TX)> set key newValue QUEUED redis(TX)> EXEC (nil) //事務(wù)失敗為什么上述事務(wù)失敗了呢?
| T1 | SET key oldValue | |
| T2 | WATCH key | |
| T3 | MULTI | |
| T4 | SET key newValue | |
| T5 | SET key otherValue | |
| T6 | EXEC |
由上圖所知,T5時刻,在客戶端A執(zhí)行EXEC命令前,key值已經(jīng)被客戶端B修改,此時服務(wù)器發(fā)現(xiàn)被監(jiān)視的鍵key的值已經(jīng)被修改了,所以服務(wù)器會拒絕執(zhí)行客戶端A的事務(wù),并向客戶端A返回空回復(fù)。
上述客戶端A的事務(wù)是不安全的,服務(wù)器會拒絕執(zhí)行客戶端提交的不安全的事務(wù),以保證數(shù)據(jù)的一致性。
上述這種形式的鎖叫做樂觀鎖,是一種強大的鎖機制。
同時可以使用UNWATCH命令取消對所有鍵key的監(jiān)視,注意!不是取消對單個或著幾個鍵的監(jiān)視,是取消所有。
事務(wù)的ACID性質(zhì)
-
原子性
事務(wù)中具有原子性是指:事務(wù)中的多個操作當(dāng)作一個整體來執(zhí)行,服務(wù)器要么執(zhí)行所有操作,要么一個操作都不執(zhí)行。
以下展示的是一個成功執(zhí)行的事務(wù),所有命令都被執(zhí)行:
127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> get key QUEUED 127.0.0.1:6379(TX)> set key value QUEUED 127.0.0.1:6379(TX)> get key QUEUED 127.0.0.1:6379(TX)> EXEC 1) (nil) 2) OK 3) "value"與此相反,以下展示了一個執(zhí)行錯誤的事務(wù),這個事務(wù)因為命令入隊時錯誤而被服務(wù)器拒絕執(zhí)行,事務(wù)中的所有命令都不會被執(zhí)行:
127.0.0.1:6379> set key value OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set key newValue QUEUED 127.0.0.1:6379(TX)> gett key (error) ERR unknown command `gett`, with args beginning with: `key`, 127.0.0.1:6379(TX)> EXEC (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get key "value"Redis的事務(wù)與傳統(tǒng)的關(guān)系型數(shù)據(jù)庫事務(wù)的最大區(qū)別就在于,Redis不支持事務(wù)回滾機制rollback,即使隊列中某個命令執(zhí)行期間出現(xiàn)了錯誤,整個事務(wù)也不會回滾,而是繼續(xù)執(zhí)行下去。
在下面的例子中,SADD命令執(zhí)行期間發(fā)生了錯誤,后續(xù)的命令也會繼續(xù)執(zhí)行下去,而且之前執(zhí)行的事務(wù)也不會受到影響
127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set key newValue QUEUED 127.0.0.1:6379(TX)> SADD key ERROR QUEUED 127.0.0.1:6379(TX)> get key QUEUED 127.0.0.1:6379(TX)> EXEC 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 3) "newValue"Redis的作者在事務(wù)功能的文檔中解釋說,不支持事務(wù)回滾是因為這種復(fù)雜的功能與Redis追求簡單高效的設(shè)計初衷不相符,并且他認為Redis事務(wù)執(zhí)行時的錯誤都是因為程序錯誤產(chǎn)生的,這種錯誤通常只會出現(xiàn)在開發(fā)環(huán)境中,而很少在實際的生成環(huán)境中出現(xiàn),所以他認為沒有必要為Redis開發(fā)事務(wù)回滾功能。
-
一致性
事務(wù)具有一致性是指:如果數(shù)據(jù)庫在執(zhí)行事務(wù)之前是一致的,那么事務(wù)執(zhí)行之后無論事務(wù)是否執(zhí)行成功,數(shù)據(jù)庫也應(yīng)該仍然是一致的。
一致性是指數(shù)據(jù)庫符合數(shù)據(jù)庫本身的定義和要求,沒有包含非法或者無效的錯誤數(shù)據(jù)
Redis通過謹慎的錯誤檢測和簡單的設(shè)計來保證事務(wù)的一致性。
-
隔離性
事務(wù)的隔離性指的是,即使數(shù)據(jù)庫中有多個事務(wù)并發(fā)執(zhí)行,各個事務(wù)也不會互相影響,并且在并發(fā)情況下執(zhí)行的事務(wù)和串行執(zhí)行的事務(wù)產(chǎn)生的結(jié)果完全相同。
因為Redis使用單線程的方式來執(zhí)行事務(wù),并且服務(wù)器保證,在事務(wù)執(zhí)行期間,其他客戶端不會中斷該事務(wù),因此Redis的事務(wù)總是以串行的方式執(zhí)行,并且事務(wù)也是具有隔離性的。
-
耐久性
事務(wù)的耐久性指定是,當(dāng)一個事務(wù)執(zhí)行完畢時,執(zhí)行這個事務(wù)所得的結(jié)果已經(jīng)被保存到永久性存儲介質(zhì)中,即使服務(wù)器執(zhí)行完事務(wù)后發(fā)生宕機,執(zhí)行事務(wù)的結(jié)果也不會丟失。
因為Redis的事務(wù)不過是簡單地用隊列包裹起了一組Redis命令,使之成為一個整體,Redis并沒有為事務(wù)提供任何額外的持久化功能,所以Redis事務(wù)中的耐久性是基于Redis所使用的持久化模式。
- 當(dāng)服務(wù)器在無持久化的內(nèi)存模式運作時,事務(wù)不具有耐久性,一旦服務(wù)器停機,包括事務(wù)數(shù)據(jù)在內(nèi)的所有服務(wù)器數(shù)據(jù)都將丟失。
- 當(dāng)服務(wù)器在RDB持久化模式運作下,服務(wù)器只會在特定的保存條件被滿足時,才會執(zhí)行bgsave命令,對數(shù)據(jù)庫進行保存操作,并且異步執(zhí)行的bgsave不能保證事務(wù)數(shù)據(jù)被第一時間保存的硬盤里面,因此RDB持久化模式下的事務(wù)也不具有耐久性
- 當(dāng)服務(wù)器運行在AOF持久化模式下,并且appendfsync參數(shù)的值為always時,程序總會執(zhí)行命令之后調(diào)用同步sync函數(shù),將數(shù)據(jù)真正保存到硬盤里面,這種配置下的事務(wù)是具有耐久性
- 當(dāng)服務(wù)器運行在AOF持久化模式下,并且appendfsync參數(shù)的值為everysec時,程序會每秒同步一次命令數(shù)據(jù)到硬盤中,如果服務(wù)器發(fā)生宕機,可能會造成事務(wù)數(shù)據(jù)丟失,這種配置下的事務(wù)也不具有耐久性
- 當(dāng)服務(wù)器運行在AOF持久化模式下,并且appendfsync參數(shù)的值為no時,程序會交由操作系統(tǒng)為決定何時,將命令數(shù)據(jù)同步到硬盤中,因為事務(wù)數(shù)據(jù)可能在等待同步中丟失,這種配置下的事務(wù)也不具有耐久性
七、主從復(fù)制
在Redis中,用戶可以通過執(zhí)行SLAVEOF命令或者設(shè)置slaveof配置,讓一個服務(wù)器去復(fù)制(replicate)另一個服務(wù)器,被復(fù)制的的服務(wù)器叫主服務(wù)器(master),對主服務(wù)器進行復(fù)制的服務(wù)器叫從服務(wù)器(slave)。
進行復(fù)制的主從服務(wù)器雙方的數(shù)據(jù)庫都將保存相同的數(shù)據(jù),概念上將這種現(xiàn)象稱為“數(shù)據(jù)庫狀態(tài)一致”。
舊版復(fù)制功能的實現(xiàn)
Redis2.8版本之前的復(fù)制功能分為同步(sync)和命令傳播(command propagate)兩個操作。
- 同步用于將從服務(wù)器的數(shù)據(jù)庫狀態(tài)更新至主服務(wù)器當(dāng)前所處的數(shù)據(jù)庫狀態(tài)
- 命令傳播用于當(dāng)主服務(wù)器數(shù)據(jù)庫的狀態(tài)被修改,導(dǎo)致主從服務(wù)器數(shù)據(jù)庫狀態(tài)不一致,讓主從服務(wù)器數(shù)據(jù)庫回到一致狀態(tài)。
復(fù)制
當(dāng)從服務(wù)器復(fù)制主服務(wù)器時,需要先進行執(zhí)行同步操作,從服務(wù)器需要通過向主服務(wù)器發(fā)送SYNC命令來完成,以下是SYNC命令的執(zhí)行步驟:
-
從服務(wù)器向主服務(wù)器發(fā)送SYNC命令
-
收到SYNC命令的主服務(wù)器執(zhí)行BGSAVE命令,在后于生成RDB文件,并使用一個緩沖區(qū)記錄從現(xiàn)在開始執(zhí)行的所有寫命令。
-
當(dāng)主服務(wù)器的BGSAVE命令執(zhí)行完畢時,主服務(wù)器會將BGSAVE生成的RDB文件發(fā)送給從服務(wù)器,從服務(wù)器接收并載入RDB文件,將自己數(shù)據(jù)庫狀態(tài)更新至主服務(wù)器執(zhí)行BGSAVE命令時的數(shù)據(jù)庫狀態(tài)。
-
主服務(wù)器將記錄在緩沖區(qū)里面所有寫命令發(fā)送給從服務(wù)器,從服務(wù)器執(zhí)行這些寫命令,將自己的數(shù)據(jù)庫狀態(tài)更新至主服務(wù)器數(shù)據(jù)庫當(dāng)前所處的狀態(tài)。
同步完整過程:
命令傳播
在同步操作執(zhí)行完畢之后,主從服務(wù)器兩者的數(shù)據(jù)庫達到一致,每當(dāng)主服務(wù)器執(zhí)行客戶端發(fā)送的寫入命令時,數(shù)據(jù)庫就會被修改,主從服務(wù)器數(shù)據(jù)庫狀態(tài)不一致。
為了讓主從服務(wù)器回到一致狀態(tài),主服務(wù)器需要對從服務(wù)器執(zhí)行命令傳播操作:主服務(wù)器會將自己執(zhí)行的寫命令,發(fā)送給從服務(wù)器執(zhí)行相同的寫命令之后,主從服務(wù)器再次回到一致狀態(tài)。
舊版復(fù)制的缺陷:
斷線后重新復(fù)制:處于命令傳播階段的主從服務(wù)器因為網(wǎng)絡(luò)問題斷開了連接終止了復(fù)制,但從服務(wù)器重寫連接主服務(wù)器后會重新進行復(fù)制,但是這種復(fù)制是全量復(fù)制,開銷相當(dāng)大。主從服務(wù)器斷線期間,主服務(wù)器執(zhí)行的寫命令可能多或少,但是服務(wù)器為了彌補這一小部分缺失的數(shù)據(jù),就要主從服務(wù)器重寫執(zhí)行一次SYNC,這種做法無疑是低效的。
新版復(fù)制功能的實現(xiàn)
為了解決舊版復(fù)制在處理斷線重復(fù)值情況的低效率問題,Redis從2.8版本之后推出了PSYNC命令來代替SYNC命令執(zhí)行復(fù)制操作。
PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)兩種模式:
- 完整重同步用于處理初次復(fù)制情況,完整重同步的執(zhí)行步驟和SYNC命令的執(zhí)行步驟基本一樣,通過主服務(wù)器創(chuàng)建并發(fā)送RDB文件,以及向從服務(wù)器發(fā)送保存在緩沖區(qū)里面的寫命令來進行同步
- 部分重同步則用于處理斷線后重復(fù)值的情況:當(dāng)從服務(wù)器斷線后重新連接主服務(wù)器,只復(fù)制斷開期間主服務(wù)器寫入的數(shù)據(jù)即可,不需要再做一次完全重同步。
部分重同步
部分重同步功能由以下三個部分構(gòu)成:
- 主從服務(wù)器的復(fù)制偏移量(replication offset)
- 主服務(wù)器的復(fù)制積壓緩沖區(qū)(replication backlog)
- 服務(wù)器的運行ID(run ID)
復(fù)制偏移量
執(zhí)行復(fù)制的主從服務(wù)器都會分別維護一個復(fù)制偏移量:
- 主服務(wù)器每次向從服務(wù)器傳播N個字節(jié)的數(shù)據(jù)時,就會將自己的復(fù)制偏移量加入N
- 從服務(wù)器收到主服務(wù)器傳播來的N個字節(jié)的數(shù)據(jù)時,也會將自己的復(fù)制偏移量加入N
通過對比主從服務(wù)器的復(fù)制偏移量,程序可以很容易地知道主從服務(wù)器是否處于一致。
復(fù)制積壓緩沖區(qū)
復(fù)制積壓緩沖區(qū)是由主服務(wù)器維護的一個固定長度(fixed-size)先進先出(FIFO)隊列組成,默認存儲大小為1MB。
當(dāng)主服務(wù)器進行命令傳播時,不僅會將寫命令發(fā)送給所有從服務(wù)器,還會將寫命令入隊到復(fù)制積壓緩沖區(qū)。
因此,主服務(wù)器的復(fù)制積壓緩沖區(qū)里面會保存著一部分最近播放的寫命令,并且復(fù)制緩沖區(qū)會為隊列中的每個字節(jié)記錄相應(yīng)的復(fù)制偏移量。當(dāng)從服務(wù)器重寫連接上主服務(wù)器之后,從服務(wù)器會通過PSYNC將自己的復(fù)制偏移量offset發(fā)送給主服務(wù)器,主服務(wù)器會根據(jù)這個復(fù)制偏移量來決定對從服務(wù)器執(zhí)行何種同步操作:
- 如果offset偏移量之后的數(shù)據(jù)仍然存在于復(fù)制積壓緩沖區(qū)中,那么主服務(wù)器將對從服務(wù)器進行部分重同步操作。
- 相反,如果offset偏移量之后的數(shù)據(jù)已經(jīng)不存在于復(fù)制積壓緩沖區(qū),那么主服務(wù)器將對從服務(wù)器執(zhí)行完整重同步操作。
根據(jù)需要調(diào)整復(fù)制積壓緩沖區(qū)的大小
Redis為復(fù)制積壓緩沖區(qū)設(shè)置的默認大小為1MB,如果主服務(wù)器需要執(zhí)行大量的寫操作,或者從服務(wù)器斷線后重連接的時間較長,那么這個值可能并不合適,這個值設(shè)置不得當(dāng),可能會讓從服務(wù)器重新連接主服務(wù)器后,讓主服務(wù)器判定從服務(wù)器需要進行完整重同步,那么PSYNC命令的部分重同步模式就不能正常發(fā)揮作用。
復(fù)制積壓緩沖區(qū)的最小大小可以根據(jù)公式reconnect_second*write_size_per_second來估算,reconnect_second為重新連接所需要時間,write_size_per_second為主服務(wù)器平均每秒寫入的命令數(shù)據(jù)量,然后在此基礎(chǔ)上將這個大小翻倍,即可滿足大部分斷線情況下重連后都能用部分重同步。
可參考:repl-backlog-size = 2*reconnect_second*write_size_per_second
服務(wù)器運行ID
-
每個Redis服務(wù)器無論主從都會自己的運行ID
-
運行ID在服務(wù)器啟動時,自動生成,由40個隨機的十六進制字符組成。
當(dāng)從服務(wù)器對主服務(wù)器進行初次復(fù)制時,主服務(wù)器會將自己的運行ID傳送給從服務(wù)器,而從服務(wù)器也會將這個運行ID保存起來。
當(dāng)從服務(wù)器斷線重連上一個主服務(wù)器時,會向主服務(wù)器將之前保存的運行ID:
- 如果從服務(wù)器保存的運行ID與當(dāng)前連接的主服務(wù)器運行ID相同,則說明之前連接的就是這個服務(wù)器,主服務(wù)器可以嘗試執(zhí)行部分重同步。
- 反之,則說明之前連接的主服務(wù)器不是當(dāng)前連接的服務(wù)器,主服務(wù)器會對從服務(wù)器執(zhí)行完整同步操作。
PSYNC命令的實現(xiàn)
PSYNC命令的調(diào)用有兩種方式:
- 如果從服務(wù)器以前沒有復(fù)制過任何服務(wù)器,或者之前執(zhí)行過SLAVEOF no one命令,那么從服務(wù)器在開始依次新的復(fù)制時將對主服務(wù)器發(fā)送PSYNC ? -1命令,主動請求主服務(wù)器進行完整重同步。
- 相反,從服務(wù)器已經(jīng)復(fù)制過某個主服務(wù)器的數(shù)據(jù),那么從服務(wù)器在開始一次新的復(fù)制前,會向主服務(wù)器發(fā)送PSYNC <runid> <offset>命令,runid是上次復(fù)制的主服務(wù)器的運行ID,而offset是當(dāng)前從服務(wù)器的復(fù)制偏移量,接受到這個命令的主服務(wù)器會根據(jù)這兩個參數(shù)來決定對從服務(wù)器執(zhí)行哪種同步操作。
根據(jù)情況,收到PSYNC的主服務(wù)器回向從服務(wù)器返回以下的三種回復(fù)中的其中一種:
- 如果主服務(wù)器返回+FULLRESYNC <runid> <offset>,表示主服務(wù)器將與從服務(wù)器執(zhí)行完整重同步操作,runid是這個主服務(wù)器的運行ID,而從服務(wù)器會將這個運行ID保存起來,在下次發(fā)送PSYNC命令時使用,而offset是主服務(wù)器當(dāng)前的復(fù)制偏移量,從服務(wù)器會將這個值作為自己的初始化偏移量
- 如果主服務(wù)器返回+CONTINUE,那么表示主服務(wù)器將與從服務(wù)器執(zhí)行部分重同步操作,從服務(wù)器只需要等候主服務(wù)器將自己缺少的那部分數(shù)據(jù)發(fā)送過來完成同步即可。
- 如果主服務(wù)器返回-ERR,那么表示主服務(wù)器的版本低于2.8識別不了PSYNC。
復(fù)制的完整流程
通過向從服務(wù)器發(fā)送SLAVEOF命令,可以讓一個從服務(wù)器去復(fù)制主服務(wù)器:SLAVEOF <master_ip> <master_port>
步驟一:設(shè)置主服務(wù)器的地址和端口
從服務(wù)器將客戶端給定的主服務(wù)器IO地址以及端口保存到服務(wù)器狀態(tài)里的masterhost屬性和masterport屬性里面。
需要注意的是:SLAVEOF命令是一個異步任務(wù),在完成masterhost屬性和masterport屬性的設(shè)置工作之后,從服務(wù)器將發(fā)送SLAVEOF命令的客戶端返回OK,表示復(fù)制指令已經(jīng)被接受,但是真正的復(fù)制工作是在OK返回之后才開始真正執(zhí)行的。
步驟二:建立套接字連接
在SLAVEOF命令執(zhí)行后,從服務(wù)器會根據(jù)命令所設(shè)置的IP地址以及端口號,創(chuàng)建連向主服務(wù)器的套接字(socket)連接。
如果連接成功,那么從服務(wù)器將會為這個套接字關(guān)聯(lián)一個專門用于處理復(fù)制工作的文件事件處理器,這個處理器將負責(zé)執(zhí)行后續(xù)的復(fù)制工作,比如接受RDB文件以及接受主服務(wù)器傳播過來的寫命令。
主服務(wù)器在接受從服務(wù)器的套接字連接之后,將會該套接字創(chuàng)建相應(yīng)的客戶端狀態(tài),并將從服務(wù)器看作是一個連接到主服務(wù)器的客戶端來對待,此時從服務(wù)器同時具有服務(wù)器和客戶端兩個身份,而接下來的復(fù)制工作都會以從服務(wù)器向主服務(wù)器發(fā)送命令請求的形式來執(zhí)行,因此理解“從服務(wù)器是主服務(wù)器的客戶端”這點相當(dāng)重要。
步驟三:發(fā)送PING命令
從服務(wù)器成為主服務(wù)器的客戶端之后,做的第一件事情就是向主服務(wù)器發(fā)送一個PING命令。
PING命令有以下作用:
- 因為主從服務(wù)器創(chuàng)建了套接字連接之后未進行過任何通信,所以先要檢查套接字的讀寫狀態(tài)是否正常。
- 檢查主服務(wù)器是否能正常處理命令請求。
從服務(wù)器發(fā)送PING命令之后會遇到以下三種情況:
- 主服務(wù)器向從服務(wù)器返回一個命令回復(fù),但從服務(wù)器未能在限定時間里讀取命令回復(fù)的內(nèi)容,則表示主從服務(wù)器之間的網(wǎng)絡(luò)連接狀態(tài)不佳,此時需要從服務(wù)器斷開連接并重新建立連向主服務(wù)器的套接字
- 主服務(wù)器返回一個錯誤,表示主服務(wù)器暫時無法處理從服務(wù)器的處理請求,不能執(zhí)行之后的復(fù)制工作,此時需要從服務(wù)器斷開連接并重新建立連向主服務(wù)器的套接字。
- 如果從服務(wù)器成功讀取到PONG回復(fù),那么表示主從服務(wù)器的連接狀態(tài)正常,可以繼續(xù)執(zhí)行以下的復(fù)制操作。
步驟四:身份驗證
從服務(wù)器收到主服務(wù)器返回的PONG之后,下一步要進行的就是是否需要進行身份驗證:
- 如果從服務(wù)器設(shè)置了masterauth,那么需要進行
- 反之,則不需要進行
需要進行身份驗證的情況下,從服務(wù)器向主服務(wù)器發(fā)送一條AUTH命令,命令的參數(shù)為從服務(wù)器中masterauth參數(shù)的值。
從服務(wù)器身份驗證階段可能遇到的情況有以下幾種:
- 主服務(wù)器沒有設(shè)置requirepass選項,而且從服務(wù)器也沒有設(shè)置masterauth,那么主服務(wù)器將繼續(xù)從服務(wù)器發(fā)送的命令,復(fù)制操作可以繼續(xù)執(zhí)行
- 如果從服務(wù)器通過AUTH命令發(fā)送的密碼與主服務(wù)器requirepass所設(shè)置的密碼相同,則繼續(xù)執(zhí)行復(fù)制操作,反之,主服務(wù)器將返回一個invalid password錯誤
- 如果主服務(wù)器設(shè)置了requirepass但是從服務(wù)器沒有設(shè)置masterauth,那么主服務(wù)器會返回NOAUTH錯誤
- 如果主服務(wù)器沒有設(shè)置requirepass,但是從服務(wù)器設(shè)置了masterauth,那么主服務(wù)器將返回no password is set錯誤
所有錯誤都會讓從服務(wù)器終止當(dāng)前的復(fù)制工作,并重新創(chuàng)建套接字開始重新進行驗證,直至身份驗證通過或者從服務(wù)器放棄復(fù)制為止。
步驟五:發(fā)送端口信息
身份驗證過后,從服務(wù)器將執(zhí)行REPLCONF listening-port <port-number>,向主服務(wù)器發(fā)送從服務(wù)器的監(jiān)聽端口號。
主服務(wù)器接受該命令之后會將端口號記錄在slave_listening_port屬性中。目前該屬性的唯一作用就是主服務(wù)器執(zhí)行INFO replication命令時打印出從服務(wù)器的端口號信息。
步驟六:同步
這一步中,從服務(wù)器將向主服務(wù)器發(fā)送PSYNC命令,執(zhí)行同步操作,將自己的數(shù)據(jù)更新至與主服務(wù)器數(shù)據(jù)庫當(dāng)前所處狀態(tài)
步驟七:命令傳播
完成同步之后,主從服務(wù)器會進入命令傳播階段,主服務(wù)器會一直將執(zhí)行的寫命令發(fā)送給從服務(wù)器,從服務(wù)器會一直接受并執(zhí)行主服務(wù)器發(fā)送來的命令,以此保證主從服務(wù)器數(shù)據(jù)庫的一致性。
心跳檢測
在命令傳播階段,從服務(wù)器會默認以每秒一次的頻率向主服務(wù)器發(fā)送命令:REPLCONF ACK <replication_offset>
其中replication_offset是當(dāng)前從服務(wù)器的復(fù)制偏移量,發(fā)送REPLCONF ACK <replication_offset>命令對主從復(fù)制有以下三個作用:
- 檢測主從服務(wù)器的網(wǎng)絡(luò)連接狀態(tài)
- 輔助實現(xiàn)min-slaves參數(shù)
- 檢測命令丟失
以下將介紹這三個作用的具體實現(xiàn)
檢測主從服務(wù)器的網(wǎng)絡(luò)連接狀態(tài)
主從服務(wù)器通過發(fā)送和接收REPLCONF ACK <replication_offset>命令來檢查兩者之間的網(wǎng)絡(luò)連接是否正常:如果主服務(wù)器超過一秒鐘沒有接收到REPLCONF ACK <replication_offset>命令,那么主服務(wù)器就知道從服務(wù)器的連接狀態(tài)出現(xiàn)問題了。
通過向主服務(wù)器發(fā)送INFO replication命令,在列出的從服務(wù)器列表中的lag一欄中,會看到對應(yīng)的從服務(wù)器上一次向主服務(wù)器發(fā)送REPLCONF ACK <replication_offset>命令距離現(xiàn)在過了多少秒:
在一般情況下,lag值應(yīng)該在0秒和1秒之間跳動,如果超過1秒,則說明主從服務(wù)器可能出現(xiàn)問題。
輔助實現(xiàn)min-slaves選項
Redis的min-slaves-to-write和min-slaves-max-lag兩個選項可以防止主服務(wù)器在不安全的情況下執(zhí)行寫命令。
min-slaves-to-write 3 min-slaves-max-lag 10以上配置中,如果從服務(wù)器數(shù)量少于3個或者三個從服務(wù)器的延遲(lag)值都大于10秒時,則主服務(wù)器將拒絕執(zhí)行寫命令。
檢測命令丟失
如果因為網(wǎng)絡(luò)故障,主服務(wù)器傳播給從服務(wù)器的寫命令在中途丟失,那么從服務(wù)器向主服務(wù)器發(fā)送REPLCONF ACK <replication_offset>命令,主服務(wù)器發(fā)現(xiàn)從服務(wù)器當(dāng)前復(fù)制偏移量少于自己的偏移量,然后就會根據(jù)從服務(wù)器提交的偏移量在復(fù)制積壓緩沖區(qū)里找到從服務(wù)器缺失的數(shù)據(jù),并將這些數(shù)據(jù)重新發(fā)送給從服務(wù)器。
主服務(wù)器向從服務(wù)器補發(fā)缺失數(shù)據(jù)這一操作的原理與部分重同步操作的原理相似,兩者的區(qū)別在于,補發(fā)缺失數(shù)據(jù)是在主從服務(wù)器沒有斷開連接的情況下執(zhí)行的,而部分重同步是主從服務(wù)器斷線后重連執(zhí)行的,
八、哨兵
哨兵(Sentinel)是Redis高可用性(High Availability)解決方案:由一個或多個Sentinel實例組成的哨兵系統(tǒng)可以監(jiān)視多個主服務(wù)器以及這些主服務(wù)器屬下的從服務(wù)器,并在被監(jiān)視的主服務(wù)器進行下線狀態(tài)后,進行故障轉(zhuǎn)移,讓某個從服務(wù)器升級為主服務(wù)器,代替已下線的主服務(wù)器繼續(xù)處理請求命令。
哨兵實現(xiàn)的功能:
- 監(jiān)控(Monitoring):哨兵會不斷檢查主節(jié)點和從節(jié)點是否正常運作
- 自動故障轉(zhuǎn)移(Automaitc Failover):當(dāng)主節(jié)點不能正常工作時,哨兵會開始自動轉(zhuǎn)移故障,將失效主節(jié)點的其中一個從節(jié)點升級為主節(jié)點,并讓其他從節(jié)點復(fù)制新的主節(jié)點
- 配置提供者(Configuration Provider):客戶端初始化時,通過連接哨兵來獲得當(dāng)前Redis服務(wù)的主節(jié)點地址
- 通知(Notification):哨兵可以將故障轉(zhuǎn)移的結(jié)果發(fā)送給客戶端
以下是一個哨兵系統(tǒng)監(jiān)視服務(wù)器的例子:
其中,雙環(huán)的server1是當(dāng)前的主服務(wù)器,單環(huán)的表示主服務(wù)器的三個從服務(wù)器,哨兵系統(tǒng)監(jiān)視著四個服務(wù)器。
此時server1出現(xiàn)故障,進入下線狀態(tài),三個從服務(wù)器的復(fù)制操作將被終止,并且哨兵系統(tǒng)會監(jiān)察到server1已下線。
當(dāng)server1的下線時長超過了用戶設(shè)定的下線時長上限時,哨兵系統(tǒng)就會對server1進行故障轉(zhuǎn)移
- 首先,哨兵系統(tǒng)會挑選server1的下屬的其中之一從服務(wù)器,并將這個從服務(wù)器升級為主服務(wù)器。
- 哨兵系統(tǒng)會向其他的服務(wù)器發(fā)送新的復(fù)制指令,讓他們成為新的主服務(wù)器的從服務(wù)器,當(dāng)所有從服務(wù)器都開始復(fù)制新的主服務(wù)器時,故障轉(zhuǎn)移操作執(zhí)行完畢。
- 另外,哨兵系統(tǒng)還會監(jiān)視已下線的server1,并在它重新上線時,將它設(shè)置為新的主服務(wù)器的從服務(wù)器。
啟動并初始化Sentinel
啟動一個Sentinel:
redis-sentinel sentinel.conf當(dāng)一個Sentinel啟動時,它需要執(zhí)行以下步驟:
- 初始化服務(wù)器
- 將普通Redis服務(wù)器使用的代碼替換成Sentinel專用代碼
- 初始化Sentinel狀態(tài)
- 根據(jù)給定的配置文件,初始化Sentinel的監(jiān)視主服務(wù)器列表
- 創(chuàng)建連向主服務(wù)器的網(wǎng)絡(luò)連接
初始化服務(wù)器
Sentinel本質(zhì)上就是一個運行在特殊模式下的Redis服務(wù)器,這個模式叫做哨兵模式。啟動Sentinel的第一步就是初始化一個普通的Redis服務(wù)器。不過Sentinel執(zhí)行的工作跟普通的Redis服務(wù)器執(zhí)行的工作不一樣,所以兩者的初始化過程也不完全相同。
例如,Sentinel服務(wù)器并不使用數(shù)據(jù)庫,所以初始化Sentinel不會載入RDB文件或者AOF文件。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-M2QS5iXy-1631271000165)(C:/Users/Supreme%20honor/Desktop/NoteBook/redis21.jpg)]
使用Sentinel專用代碼
啟動Sentinel的第二個步驟就是將一部分普通Redis服務(wù)器使用的代碼進行替換。
例如普通Redis服務(wù)器使用redis.h/REDIS_SERVERPORT常量值作為服務(wù)器端口
# define REDIS_SERVERPORT 6379而Sentinel服務(wù)器使用sentinel.c/REDIS_SENTINEL_PORT常量作為服務(wù)器端口
# define REDIS_SENTINEL_PORT 26379并且Sentinel會使用sentinel.c/sentinelcmds作為服務(wù)器的專用命令表,sentinelcmds命令表也解釋了為什么Sentinel模式下,Redis服務(wù)器不能執(zhí)行諸如SET,DEL,DBSIZE等命令,因為服務(wù)器載入的時候就沒有載入這些命令。
Sentinel模式下客戶端可以執(zhí)行的全部命令有:PING SENTINEL INFO SUBCRIBE UNSUBSCRIBE PSUBSCRIBE PUNSUBSCRIBE
初始化Sentinel狀態(tài)
服務(wù)器 初始化一個sentinel.c/sentinelState結(jié)構(gòu)(簡稱:Sentinel狀態(tài)),這個結(jié)構(gòu)中保存了服務(wù)器中所有和Sentinel有關(guān)的狀態(tài)信息。
struct sentinelState {//當(dāng)前紀(jì)元,用于實現(xiàn)故障轉(zhuǎn)移uint64_t current_epoch;//保存了所有被sentinel監(jiān)視的主服務(wù)器//字典的鍵是主服務(wù)器的名字,值是指向主服務(wù)器的指針dict *masters;//是否進入TILT模式 int tilt;//正在執(zhí)行的腳本數(shù)量int running_scripts;//最后一次執(zhí)行時間處理器的時間mstime_t previous_time;//進入TILT模式的時間mastime_t tilt_start_time;//一個FIFO隊列。包含了所有需要執(zhí)行的用戶腳本list *scripts_queue; }初始化Sentinel狀態(tài)的masters屬性
如上述代碼可知,sentinelState結(jié)構(gòu)體中的masters字典記錄了所有被Sentinel監(jiān)視的主服務(wù)器的相關(guān)信息。
其中:
-
字典的鍵是被監(jiān)視的主服務(wù)器的名稱
-
字典的值是被監(jiān)視的主服務(wù)器對應(yīng)的sentinel.c/sentinelRedisInstance結(jié)構(gòu)。
每一個sentinel.c/sentinelRedisInstance都代表一個被Sentinel監(jiān)視的Redis服務(wù)器實例,這個實例可以是主服務(wù)器,從服務(wù)器或另外一個哨兵Sentinel。
typedef struct sentinelRedisInstance {//標(biāo)識值,記錄了實例的類型,以及該實例的狀態(tài)int flags;//實例的名稱//主服務(wù)器的名稱在配置文件中設(shè)置//從服務(wù)器以及Sentinel的名稱由Sentinel自動設(shè)置//格式為 ip:portchar *name;//運行IDchar *runid;//配置紀(jì)元,實現(xiàn)故障轉(zhuǎn)移uint64_t config_epoch;//實例的地址sentinelAddr * addr;//SENTINEL down-after-milliseconds選項設(shè)定的值//實例無響應(yīng)多少秒之后才會被判定為主觀下線mstime_t down_after_period;//SENTINEL monitor <master-name> <ip> <port> <quorum>//判斷該實例客觀下線需要的的支持投票數(shù)量int quorum;//SENTINEL parallel-synuc <master-name> <number>選項的值//在執(zhí)行故障轉(zhuǎn)移操作時,可以同時對新的主服務(wù)器進行同步的從服務(wù)器數(shù)量int parallel_syncs;//SENTINEL failover-timeout <master-name> <ms>選項的值//刷新故障遷移狀態(tài)的最大時限mstime_t failover_timeout;//... }sentinelRedisInstance;typedef struct sentinelAddr {char *ip;int port; }sentinelAddr;創(chuàng)建連向主服務(wù)器的網(wǎng)絡(luò)連接
初始化Sentinel的最后一步是創(chuàng)建連向被監(jiān)視的主服務(wù)器的網(wǎng)絡(luò)連接,Sentinel將成為主服務(wù)器的客戶端,它可以向主服務(wù)器發(fā)送命令,并從命令回復(fù)中獲取相關(guān)的信息。
對于每個被Sentinel監(jiān)視的主服務(wù)器來說,Sentinel會創(chuàng)建兩個連向主服務(wù)器的異步網(wǎng)絡(luò)連接:
- 一個是命令連接,這個連接用于向主服務(wù)器發(fā)送命令,并接受命令回復(fù)。
- 另一個是訂閱連接,這個連接用于訂閱主服務(wù)器的__sentinel__:hello頻道
為什么要有兩個連接?
- Redis目前的發(fā)布與訂閱功能中,被發(fā)送的信息都不會在Redis服務(wù)器中保存,如果這條信息發(fā)送時,要接受信息的客戶端不在線或者掉線,那么這個客戶端就會丟失該信息,因此為了不丟失__sentinel__:hello頻道的信息,Sentinel必須專門用另外一個訂閱連接來接受該頻道的信息。
- 除了訂閱頻道之外,Sentinel還必須向被它監(jiān)視的主服務(wù)器發(fā)送命令,以此來與主服務(wù)器進行通訊,所有Sentinel還必須向主服務(wù)器創(chuàng)建命令連接。
- 因為Sentinel需要和多個實例創(chuàng)建多個網(wǎng)絡(luò)連接,所以Sentinel使用的是異步連接。
獲取主服務(wù)器信息
Sentinel默認會以每十秒一次的頻率,通過連接向被監(jiān)視的主服務(wù)器發(fā)送INFO命令,并通過分析INFO命令的回復(fù)來獲取主服務(wù)器的當(dāng)前信息。
通過分析主服務(wù)器返回的INFO命令回復(fù),Sentinel可以獲得以下兩方面信息:
- 一方面是關(guān)于主服務(wù)器 本身的信息,包括run_id域記錄的服務(wù)器運行ID,以及role域記錄的服務(wù)器角色。
- 另一方面是關(guān)于主服務(wù)器屬下的所有從服務(wù)器的信息,每一個從服務(wù)器由一個slave字符串開頭的行記錄,每行中會顯示從服務(wù)器的IP地址和port端口號,根據(jù)這些信息,Sentinel無需用戶提供從服務(wù)器的地址信息就可以自動發(fā)現(xiàn)從服務(wù)器。
根據(jù)上述信息,Sentinel對主服務(wù)器的實例結(jié)構(gòu)進行更新,例如,主服務(wù)器重啟后的運行ID與之前保存的運行ID不同,Sentinel會檢測到該情況,對實例結(jié)構(gòu)的運行ID進行更新。
獲取從服務(wù)器信息
當(dāng)Sentinel發(fā)現(xiàn)主服務(wù)器有新的從服務(wù)器出現(xiàn)時,Sentinel除了會為這個新的從服務(wù)器創(chuàng)建新的實例結(jié)構(gòu)之外,還會創(chuàng)建連接到從服務(wù)器的命令連接和訂閱連接。
創(chuàng)建命令連接之后,Sentinel會以十秒一次的頻率通過向從服務(wù)器發(fā)送INFO命令,并獲得以下內(nèi)容的回復(fù):
- 從服務(wù)器的運行ID
- 從服務(wù)器的角色role
- 主服務(wù)器的IP地址master_host,以及主服務(wù)器的端口號master_port
- 主從服務(wù)器的連接狀態(tài)master_link_status
- 從服務(wù)器的優(yōu)先級slave_priority
- 從服務(wù)器的復(fù)制偏移量slave_repl_offset
根據(jù)這些信息,Sentinel會對從服務(wù)器的實例結(jié)構(gòu)進行更新。
向主從服務(wù)器發(fā)送信息
默認情況下,Sentinel會以每兩秒一次的頻率通過命令連接向所有被監(jiān)視的主服務(wù)器和從服務(wù)器發(fā)送以下格式命令:
PUBLISH _sentinel_:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
分別記錄Sentinel和其監(jiān)視的主服務(wù)器的IP地址,端口號,運行ID以及當(dāng)前的配置紀(jì)元。
接收來自主從服務(wù)器的的頻道信息
當(dāng)Sentinel與一個主服務(wù)器或從服務(wù)器建立起訂閱連接之后,Sentinel就會通過訂閱連接向服務(wù)器發(fā)送以下命令:
SUBCRIBE _sentinel_:hello
Sentinel對_sentinel_:hello頻道的訂閱會一直持續(xù)到Sentinel與服務(wù)器的連接斷開為止。
每個與Sentinel連接到服務(wù)器,Sentinel既通過命令連接向服務(wù)器的_sentinel_:hello頻道發(fā)送信息,又通過訂閱連接從服務(wù)器的_sentinel_:hello頻道接收信息。
當(dāng)一個Sentinel從_sentinel_:hello頻道接收到信息之后,會對該信息進行分析,提取出Sentinel IP Sentinel post Sentinel runID等八個參數(shù)信息,并進行以下檢查:
- 如果信息中的運行ID與接收信息的運行ID相同,則說明是自己發(fā)送的,丟棄該信息,不予處理。
- 反之,說明這條信息是由監(jiān)視同一個服務(wù)器的其他Sentinel發(fā)來的,接收信息的Sentinel會根據(jù)信息重點各種參數(shù),而對主服務(wù)器的實例結(jié)構(gòu)進行調(diào)整更新。
更新sentinels字典
Sentinel為主服務(wù)器創(chuàng)建的實例結(jié)構(gòu)中的sentinels字典,不僅保存Sentinel本身,還有所有同樣監(jiān)視這個主服務(wù)器的其他Sentienl資料。
創(chuàng)建連向其他Sentinel的命令連接
當(dāng)Sentinel通過頻道信息發(fā)現(xiàn)了一個新的Sentinel時,它不僅為會新Sentinel在sentinels字典中創(chuàng)建對應(yīng)的實例結(jié)構(gòu),還會創(chuàng)建一個連向新Sentinel的命令連接,而新的Sentinel也會創(chuàng)建連接到這個Sentinel的命令連接,從而讓哨兵系統(tǒng)中的多個Sentinel形成相互連接的網(wǎng)絡(luò)。
使用命令連接的各個Sentinel通過命令請求來進行信息交換。
Sentinel之間不會創(chuàng)建訂閱連接
Sentinel在連接主從服務(wù)器時會創(chuàng)建命令連接和訂閱連接,但是在連接Sentinel時只會創(chuàng)建命令連接,這是因為Sentinel需要通過接收主從服務(wù)器發(fā)來的頻道信息發(fā)現(xiàn)未知的Sentinel,所以才需要創(chuàng)建訂閱連接,而互相已知的Sentinel則只需要通過命令連接進行通訊即可。
檢測主觀下線狀態(tài)
在默認情況下,Sentinel會以每秒一次的頻率向其他創(chuàng)建了命令連接的實例(主從服務(wù)器、其他Sentinel在內(nèi))發(fā)送PING命令,通過實例返回的回復(fù)的來判斷實例是否在線。
服務(wù)器對PING命令的有效回復(fù)是以下三種的其中一種:
- +PONG
- -LOADING錯誤
- -MASTERDOWN錯誤
如果服務(wù)器返回了除以上三種之外的其他回復(fù),又或者在指定時間內(nèi)沒有回復(fù)PING命令,則Sentinel認為服務(wù)器返回的回復(fù)無效。
一個服務(wù)器在master-down-after-milliseconds毫秒內(nèi)一直返回?zé)o效信息則會被Sentinel判定為主觀下線。
檢查客觀下線狀態(tài)
當(dāng)Sentinel將一個主服務(wù)器判定為主觀下線之后,為了確認這個主服務(wù)器是否真的下線,它會向其他監(jiān)視這一主服務(wù)器的其他Sentinel進行詢問,當(dāng)Sentinel從其他Sentinel接收到足夠的已下線判斷時,Sentinel就會將這個主服務(wù)器判定為客觀下線,并進行故障轉(zhuǎn)移。
發(fā)送SENTINEL is-master-down-by-addr <ip> <port> <current-epoch> <runid>命令向其他Sentinel詢問意見。
| ip | 被Sentinel判定為主觀下線的主服務(wù)器的IP地址 |
| port | 被Sentinel判定為主觀下線的主服務(wù)器的端口號 |
| current_epoch | Sentinel當(dāng)前的配置紀(jì)元,用于選舉領(lǐng)頭Sentienl |
| runid | 可以是* 符號或者是Sentinel的運行ID:* 符號表示命令僅僅用于主服務(wù)器的客觀下線狀態(tài),而Sentinel的運行ID用于選舉領(lǐng)頭Sentinel |
目標(biāo)Sentinel接收SENTINEL is-master-down-addr命令
當(dāng)一個Sentinel(目標(biāo)Sentinel)接收到另一個Sentinel(源Sentinel)發(fā)來的SENTINEL is-master-down-addr命令時,目標(biāo)Sentinel會分析并取出命令請求中的各個參數(shù),并根據(jù)主服務(wù)器的IP和端口號,判斷主服務(wù)器是否已經(jīng)下線,然后向源Sentinel返回SENTINEL is-master-down-by <down_state> <leader_runid> <leader_epoch>命令
| down_state | 返回目標(biāo)Sentinel對服務(wù)器的檢查結(jié)果,1表示主服務(wù)器已經(jīng)下線,0表示主服務(wù)器未下線 |
| leader_runid | 可以是* 符號或者目標(biāo)Sentinel的局部領(lǐng)頭Sentinel的運行ID,* 符號表示主服務(wù)器的下線狀態(tài),而局部領(lǐng)頭Sentinel 的運行ID則用于選舉領(lǐng)頭Sentinel |
| leader_epoch | 目標(biāo)Sentinel的局部領(lǐng)頭Sentinel的配置紀(jì)元,用于選舉領(lǐng)頭Sentinel,僅在leader_runid不為* 時有效,如果leader_runid的值為* ,那么leader_epoch的值為0 |
舉例:一個目標(biāo)Sentinel返回SENTINEL is-master-down-by <1> <*> <0>命令給源Sentinel,則說明目標(biāo)Sentinel同意主服務(wù)器已經(jīng)下線。
源Sentinel接收SENTINEL is-master-down-by命令
源Sentinel根據(jù)其他目標(biāo)Sentinel發(fā)回的SENTINEL is-master-down-by命令,Sentinel統(tǒng)計其他Sentinel同意主服務(wù)器下線的數(shù)量,當(dāng)這一數(shù)量達到配置指定的判斷客觀下線所需數(shù)量時,Sentinel就會將主服務(wù)器實例結(jié)構(gòu)的flags屬性的SRI_O_DOWN標(biāo)識打開,表示該服務(wù)器已經(jīng)下線。
客觀下線的判斷條件
Sentinel配置文件中寫入了sentinel monitor mymaster 127.0.0.1 6379 2——配置的含義是:該哨兵節(jié)點監(jiān)控192.168.92.128:6379這個主節(jié)點,該主節(jié)點的名稱是mymaster,最后的2的含義與主節(jié)點的故障判定有關(guān):至少需要2個哨兵節(jié)點同意,才能判定主節(jié)點故障并進行故障轉(zhuǎn)移。
選舉領(lǐng)頭Sentinel
當(dāng)一個主服務(wù)器被判斷為客觀下線,監(jiān)視這個下線的主服務(wù)器的各個Sentinel回進行協(xié)商,選出一個領(lǐng)頭Sentinel,并由領(lǐng)頭Sentinel對下線的主服務(wù)器進行故障轉(zhuǎn)移操作。
以下是Redis選舉領(lǐng)頭Sentinel的規(guī)則和方法:
故障轉(zhuǎn)移
在選舉出領(lǐng)頭Sentinel之后,領(lǐng)頭Sentinel將對已下線的主服務(wù)器進行故障轉(zhuǎn)移操作:
- 從已下線的主服務(wù)器屬性的從服務(wù)器中挑選一個轉(zhuǎn)換為主服務(wù)器
- 讓已下線的主服務(wù)器屬性的其他從服務(wù)器改為復(fù)制新的主服務(wù)器
- 將已下線的主服務(wù)器設(shè)置為新的主服務(wù)器的從服務(wù)器,當(dāng)它重新連接上來時就會成為新的主服務(wù)器的從服務(wù)器
挑選新的主服務(wù)器
在已下線的主服務(wù)器屬下的所有從服務(wù)器中,挑選一個狀態(tài)良好,數(shù)據(jù)完整的從服務(wù)器,然后向它發(fā)送SLAVEOF no one命令,將這個從服務(wù)器轉(zhuǎn)換為主服務(wù)器。
新的主服務(wù)器是如何挑選的呢?
領(lǐng)頭Sentinel回將已下線的主服務(wù)器的所有從服務(wù)器保存到一個列表中,如何進行一項一項地篩選:
-
刪除列表中所有處于下線或者斷線的從服務(wù)器,保證列表中的服務(wù)器都是在線狀態(tài)良好的
-
刪除列表中所有最近五秒內(nèi)沒有回復(fù)過領(lǐng)頭Sentinel的INFO命令的從服務(wù)器,保證列表中都是最近進行成功通訊的服務(wù)器
-
刪除所有與已下線主服務(wù)器斷開連接超過down-after-millisecond * 10 毫秒的從服務(wù)器,保證列表中的從服務(wù)器都沒有過早地與主服務(wù)器斷開連接,以此保證數(shù)據(jù)完整。
-
從以上淘汰中存留下來的服務(wù)器,會根據(jù)復(fù)制偏移量來繼續(xù)進行篩選,(復(fù)制偏移量最大的從服務(wù)就是保存著最新數(shù)據(jù)的服務(wù)器);
如果復(fù)制偏移量不可用,則會根據(jù)服務(wù)器的runID來進行選擇,選擇runID小的服務(wù)器成為主服務(wù)器。
修改從服務(wù)器的復(fù)制目標(biāo)
當(dāng)新的主服務(wù)器出現(xiàn)之后,領(lǐng)頭Sentinel下一步做的就是,讓其他從服務(wù)器去復(fù)制新的主服務(wù)器,可以通過向從服務(wù)器發(fā)送SLAVEOF實現(xiàn)。
將舊的主服務(wù)器變成從服務(wù)器
故障轉(zhuǎn)移操作最后要做的就是將已下線的主服務(wù)器設(shè)置為新的主服務(wù)器的從服務(wù)器。
當(dāng)已下線的主服務(wù)器重新連接后Sentinel就會向其發(fā)送SLAVEOF命令,使其成為新的主服務(wù)器的從服務(wù)器,如下圖所示:
Sentinel自動故障轉(zhuǎn)移的一致性特質(zhì)
Sentinel自動故障轉(zhuǎn)移使用Raft算法來選舉領(lǐng)頭Sentinel,從而確保在一個給定的紀(jì)元里面,只有一個領(lǐng)頭產(chǎn)生。
這表示同一個紀(jì)元中,不會有兩個Sentinel同時被選為領(lǐng)頭,并且各個Sentinel在同一個紀(jì)元中,只會對一個領(lǐng)頭進行投票。
更高的配置紀(jì)元總是優(yōu)于較低的紀(jì)元,因此每個Sentinel都會主動使用更新的紀(jì)元來代替自己的配置。
可以這樣說,我們將Sentinel配置看作一個帶有版本號的狀態(tài),一個狀態(tài)會以最后寫入者的方式保留下來,當(dāng)一個有著比較舊的配置的Sentinel接收到其他Sentinel發(fā)來的版本更新的配置時,就會將自己的配置進行更新。
Sentinel狀態(tài)的持久化
Sentinel 的狀態(tài)會被持久化在 Sentinel 配置文件里面。
每當(dāng) Sentinel 接收到一個新的配置, 或者當(dāng)領(lǐng)頭 Sentinel 為主服務(wù)器創(chuàng)建一個新的配置時, 這個配置會與配置紀(jì)元一起被保存到磁盤里面。
這意味著停止和重啟 Sentinel 進程都是安全的。
九、集群
上述的高可用方案:持久化,主從復(fù)制和哨兵,但這些方案仍然存在不足,其中主要的問題就是存儲能力受單機限制,以及無法實現(xiàn)寫操作的負載均衡。
集群的作用
集群,即Redis Cluster,是Redis3.0開始引入的分布式存儲方案。
集群有多個節(jié)點(Node)組成,Redis的數(shù)據(jù)分布在這些節(jié)點中。集群中的節(jié)點分為主節(jié)點和從節(jié)點:主節(jié)點負責(zé)讀寫請求和集群信息的維護,從節(jié)點進行主節(jié)點數(shù)據(jù)和狀態(tài)信息的復(fù)制。
集群的作用歸納為以下兩點:
-
數(shù)據(jù)分區(qū)(數(shù)據(jù)分片)
集群將數(shù)據(jù)分散到多個節(jié)點,一方面突破了Redis單機內(nèi)存大小的限制,另一方面每個節(jié)點都可以對外提供讀寫服務(wù),極大提高了集群的響應(yīng)能力。
-
高可用
集群支持主從復(fù)制和主節(jié)點的自動故障轉(zhuǎn)移,轉(zhuǎn)移機制與哨兵機制類似;當(dāng)某一節(jié)點出現(xiàn)故障時,集群仍然可以對外提供服務(wù)。
集群的搭建
集群的搭建有兩種方式:(1)手動執(zhí)行Redis命令,一步步完成搭建;(2)使用Ruby腳本搭建。兩者原理相同,后者對前者使用到的Redis命令進行封裝打包。
執(zhí)行Redis命令搭建集群
集群的搭建分為四步:
- 啟動節(jié)點:將節(jié)點以集群模式啟動,此時節(jié)點是獨立的,沒有建立與其他節(jié)點的連接
- 節(jié)點握手:讓各個獨立的節(jié)點連接成一個網(wǎng)絡(luò)
- 分配槽:將16384個槽分配給各個主節(jié)點
- 指定主從關(guān)系
啟動節(jié)點
集群節(jié)點的啟動依然是使用redis-server命令,但需要以集群模式啟動,以下是節(jié)點的配置文件
#redis-6379.conf dbfilename "dump-6379.rdb" port 6379 daemonize no rdbcompression yes rdbchecksum yes save 10 2 appendonly yes appendfsync always appendfilename "appendonly-6379.aof" bind 127.0.0.1 #logfile "/www/server/redis/redis-6379.log" databases 16 cluster-enabled yes cluster-config-file "nodes-6379.conf" cluster-node-timeout 10000cluster-enabled yes:Redis實例可以分為單機模式standAlone和集群模式cluster,這個設(shè)置可以開啟節(jié)點的集群模式
集群模式下的節(jié)點,其redis-mode為cluster,如下圖所示:
cluster-config-file:指定了集群配置文件的位置,每個節(jié)點運行過程中會維護一份集群配置文件;當(dāng)集群信息發(fā)生變化,集群中的所有節(jié)點會將最新信息更新到該配置文件中;當(dāng)節(jié)點重啟時會讀取該配置文件,獲取集群信息。Redis節(jié)點以集群模式啟動時,會首先尋找是否有集群信息文件,如果有則使用文件中的配置啟動,如果沒有,則初始化配置并將配置保存到文件中。
編輯好配置文件后,通過redis-server命令啟動節(jié)點:
redis-server redis-6379.conf節(jié)點啟動以后,通過cluster nodes命令可以查看節(jié)點的情況,如下圖所示。
其中返回值第一項表示節(jié)點id,由40個16進制字符串組成,集群模式下的節(jié)點的run_id與單機模式下的節(jié)點run_id有所不同,Redis每次啟動都會重新創(chuàng)建run_id,但是集群模式下只會在初始時創(chuàng)建一次,然后保存到集群配置文件中,之后節(jié)點重啟會從配置文件從讀取,而不再重新創(chuàng)建。
需要注意的是:啟動節(jié)點階段,節(jié)點之間是沒有主從關(guān)系的,因此節(jié)點中不需要添加slaveof配置。
節(jié)點握手
節(jié)點啟動后是互相獨立的,并不知道其他節(jié)點存在,因此集群模式中需要進行節(jié)點握手,將獨立的節(jié)點組成一個網(wǎng)絡(luò)。
節(jié)點握手使用cluster meet {ip} {port}命令實現(xiàn)。
分配槽
在Redis集群中,借助槽實現(xiàn)數(shù)據(jù)分區(qū),集群有16384個槽,槽是數(shù)據(jù)管理和遷移的基本單位,當(dāng)數(shù)據(jù)庫中的16384個槽分配了節(jié)點,集群處于上線狀態(tài)(ok),如果有一個槽沒有分配節(jié)點,則集群處于下線狀態(tài)(fail)。
redis-cli -p 7000 cluster addslots {0..5461} redis-cli -p 7001 cluster addslots {5462..10922} redis-cli -p 7002 cluster addslots {10923..16383}此時查看集群狀態(tài),顯示所有槽分配完畢,集群進入上線狀態(tài):
指定主從關(guān)系
集群中指定關(guān)系不再使用slaveof命令,而是使用cluster replicate run_id
例如:
redis-cli -p 8000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae redis-cli -p 8001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4 redis-cli -p 8002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1至此,集群搭建完畢。
使用Ruby腳本搭建集群
在{REDIS_HOME}/src目錄下有一個redis-trib.rb文件,這是一個Ruby腳本,可以實現(xiàn)集群的自動搭建。
安裝Ruby環(huán)境
輸入以下命令
apt-get install ruby #安裝ruby環(huán)境 gem install redis #gem是ruby的包管理工具啟動節(jié)點
redis-server redis-6379.conf搭建集群
redis-trib.rb腳本提供了眾多命令,其中create用于搭建集群:
redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1-replicas 1:表示每個主節(jié)點有一個從節(jié)點;多個{id:port}表示節(jié)點地址,前面的做主節(jié)點,后面的做從節(jié)點。
注意:使用redis-trib.rb腳本搭建集群時,要求節(jié)點不能包含任何槽和數(shù)據(jù),否則會報以下錯誤:
執(zhí)行創(chuàng)建命令之后,腳本會給出創(chuàng)建集群的計劃,如下圖所示,計劃包括哪些節(jié)點是主節(jié)點,哪些是從節(jié)點,以及如何分配槽。
#是否執(zhí)行計劃 Can I set the above configuration?(type 'yes' to accept):yes輸入yes執(zhí)行計劃,至此,集群搭建完畢。
集群設(shè)計
設(shè)計集群方案時,需要考慮以下因素:
- 高可用要求:根據(jù)故障自動轉(zhuǎn)移原理,至少需要3個主節(jié)點才能完成故障轉(zhuǎn)移,且三個主節(jié)點應(yīng)在不同的物理機上,每個主節(jié)點至少需要一個從節(jié)點,主從節(jié)點應(yīng)在不同的物理機上,因此高可用集群至少需要6個節(jié)點來支持。
- 數(shù)據(jù)量和訪問量:估算應(yīng)用需要的數(shù)據(jù)量和總訪問量,結(jié)合每個主節(jié)點的容量和能承受的訪問量(可以通過benchmark估算),計算所需的主節(jié)點個數(shù)。
- 節(jié)點數(shù)量限制:Redis官方給出的節(jié)點數(shù)量限制是1000,主要是考慮節(jié)點間通信帶來的消耗。實際應(yīng)用中需要避免大量集群,如果節(jié)點數(shù)量不足以滿足應(yīng)用對Redis數(shù)據(jù)量和訪問量的要求,可以考慮:(1)業(yè)務(wù)分割,大集群劃分為多個小集群;(2)減少不必要的數(shù)據(jù);(3)調(diào)整過期數(shù)據(jù)刪除策略。
- 適度冗余:Redis可以在不影響集群服務(wù)的情況下適度增加節(jié)點,保證數(shù)據(jù)容冗余。
數(shù)據(jù)結(jié)構(gòu)
節(jié)點需要專門的數(shù)據(jù)結(jié)構(gòu)來存儲集群的狀態(tài)。所謂集群的狀態(tài),是一個很大的概念,包括:集群是否處于上線狀態(tài),集群中有哪些節(jié)點,節(jié)點的主從狀態(tài),槽指派的分布等。
節(jié)點為了存儲集群狀態(tài)而提供的數(shù)據(jù)結(jié)構(gòu)中,最關(guān)鍵的是clusterNode和clusterState結(jié)構(gòu),前者記錄集群中一個節(jié)點的狀態(tài),后者記錄了集群作為一個整體的狀態(tài)。
每個節(jié)點都會使用一個clusterNode結(jié)構(gòu)來記錄自己的狀態(tài),并為集群中所有的節(jié)點創(chuàng)建一個clusterNode結(jié)構(gòu),以此記錄其他節(jié)點的狀態(tài):
struct clusterNode {//節(jié)點創(chuàng)建時間mstime_t ctime;//節(jié)點名稱char name[REDIS_CLUSTER_NAMELEN];//節(jié)點的ip和端口號char ip[REDIS_IP_STR_LEN];int port;//節(jié)點標(biāo)識:整型,每個bit都代表了不同狀態(tài),如節(jié)點的主從狀態(tài)、是否在線、是否在握手等int flags;//配置紀(jì)元:故障轉(zhuǎn)移時起作用,類似于哨兵的配置紀(jì)元uint64_t configEpoch;//槽在該節(jié)點中的分布:占用16384/8個字節(jié),16384個比特;每個比特對應(yīng)一個槽:比特值為1,則該比特對應(yīng)的槽在節(jié)點中;比特值為0,則該比特對應(yīng)的槽不在節(jié)點中unsigned char slots[16384/8];//節(jié)點中槽的數(shù)量int numslots;//... };除了上述字段,clusterNode還包含了節(jié)點連接、主從復(fù)制、故障發(fā)現(xiàn)和轉(zhuǎn)移需要的信息等。
clusterState
typedef struct clusterState {//自身節(jié)點clusterNode *myself;//配置紀(jì)元uint64_t currentEpoch;//集群狀態(tài):在線還是下線int state;//集群中至少包含一個槽的節(jié)點數(shù)量int size;//哈希表,節(jié)點名稱->clusterNode節(jié)點指針dict *nodes;//槽分布信息:數(shù)組的每個元素都是一個指向clusterNode結(jié)構(gòu)的指針;如果槽還沒有分配給任何節(jié)點,則為NULLclusterNode *slots[16384]; };集群命令的實現(xiàn)
cluster meet
通過向節(jié)點A發(fā)送cluster meet命令,客戶端可以讓接收命令的節(jié)點A將另一個節(jié)點B添加到節(jié)點A 所在的集群中。
CLUSTER MEET <ip> <port>收到命令的節(jié)點A將與節(jié)點B進行握手,以此確定彼此的存在,并為將來進一步的通信打好基礎(chǔ)。具體步驟:
- 節(jié)點A為節(jié)點B創(chuàng)建一個clusterNode結(jié)構(gòu)來存儲節(jié)點B的信息,并將該結(jié)構(gòu)添加到自己的clusterState.nodes字典里。
- 之后,節(jié)點A根據(jù)CLUSTER MEET <ip> <port>命令中指定的IP地址和端口號,向節(jié)點B發(fā)送一條MEET消息。
- 節(jié)點B接收到節(jié)點A發(fā)送的MEET消息,節(jié)點B為節(jié)點A創(chuàng)建一個clusterNode結(jié)構(gòu),并將該結(jié)構(gòu)添加到自己的clusterState.nodes字典里。
- 之后節(jié)點B向節(jié)點A返回一條PONG消息。
- 節(jié)點A將接收到節(jié)點B返回的PONG消息,通過這條消息,節(jié)點A可以得知節(jié)點B已經(jīng)成功地接收自己的MEET消息。
- 之后節(jié)點A將向節(jié)點B返回一條PING消息。
- 節(jié)點B將接收到節(jié)點A返回的PING消息,通過這條消息,節(jié)點B可以得知節(jié)點A已經(jīng)成功接收了自己返回的PONG消息,至此,握手完成。
cluster addslots
集群中槽的分配信息,存儲在clusterNode的slots數(shù)組中和clusterState的slots數(shù)組中,兩個數(shù)組之間的區(qū)別是,前者存儲的該節(jié)點中分配了哪些槽,而后者存儲的每個槽所指向的節(jié)點,即集群中所有槽分別分布在哪個節(jié)點。
cluster addslots命令接收一個或多個槽作為參數(shù),例如在A節(jié)點上執(zhí)行cluster addslots {0,1989}命令,是將編號為0-1989的槽分配給A節(jié)點,具體執(zhí)行步驟如下:
- 遍歷槽,檢查它們0-1989是否都沒有分配節(jié)點,如果有一個槽已經(jīng)分配,則命令執(zhí)行失敗;檢查方法是遍歷槽在clusterState.slots[]中對應(yīng)的值是否為NULL值。
- 遍歷槽,將其分配給節(jié)點A,將clusterNode.slots[]中對應(yīng)的比特修改為1,以及clusterState.slots[]中對應(yīng)的指針指向節(jié)點A。
- 執(zhí)行完畢后,通過節(jié)點通信機制通知其他節(jié)點,所有節(jié)點都會知道0-1989的槽分配給了節(jié)點A。
實踐須知
集群伸縮
實際場景中常常需要對集群進行伸縮,如果訪問量增大時,集群的擴容操作。Redis集群可以在不影響對外服務(wù)的情況下對集群進行伸縮;其核心是槽遷移:修改槽與節(jié)點之間的關(guān)系,實現(xiàn)槽在節(jié)點中的遷移。例如,如果槽均勻分配在三個節(jié)點中,現(xiàn)需要新增一個節(jié)點,則需要從3個節(jié)點中取出一部分槽分配給新的節(jié)點,從而實現(xiàn)槽的重新分配。
新增節(jié)點
- 啟動節(jié)點
- 節(jié)點握手
- 遷移槽,使用redis-trib.rb 的reshard(重新分區(qū))工具實現(xiàn),reshard自動化程度很高,只需要輸入redis-trib.rb reshard ip:port即可自動實現(xiàn)槽遷移。
- 指定主從關(guān)系
減少節(jié)點
- 遷移槽,使用reshard將需要刪除的節(jié)點的槽均勻遷移到其他節(jié)點上
- 下線節(jié)點:使用redis-trib.rb del-node工具,先下線從節(jié)點再下線主節(jié)點。
ASK錯誤
當(dāng)客戶端向源節(jié)點發(fā)送一個與數(shù)據(jù)庫有關(guān)的命令,并且命令要處理的數(shù)據(jù)庫鍵剛好就屬于正在被遷移的槽時:
- 源節(jié)點會現(xiàn)在自己的數(shù)據(jù)庫中查找指定的鍵,如果找到就執(zhí)行客戶端發(fā)送的命令。
- 相反,如果源節(jié)點沒有在數(shù)據(jù)庫中找到指定的鍵,則這個鍵有可能已經(jīng)被遷移到了其他節(jié)點,此時源節(jié)點將向客戶端返回一個ASK錯誤,指引客戶端轉(zhuǎn)向正在導(dǎo)入槽的目標(biāo)節(jié)點,并且再次發(fā)送之前要執(zhí)行的命令。
客戶端收到ASK錯誤后,從中讀取目標(biāo)節(jié)點的地址信息,并向目標(biāo)節(jié)點重新發(fā)送請求,就像收到MOVED錯誤時一樣。但是二者有很大區(qū)別:ASK錯誤說明數(shù)據(jù)正在遷移,不知道何時遷移完成,因此重定向是臨時的,SMART客戶端不會刷新slots緩存;MOVED錯誤重定向則是(相對)永久的,SMART客戶端會刷新slots緩存。
參考文獻:
《Redis設(shè)計與實現(xiàn)》
《Redis開發(fā)與運維》
以上。
創(chuàng)作不易,如果文章對你有幫助,留個三連再走吧。
如果不足或錯誤歡迎評論指正。
總結(jié)
- 上一篇: Java实现堆排序及详细图解
- 下一篇: Dubbo暴露服务过程