一篇详文带你入门 Redis
作者:QQ 音樂(lè)前端團(tuán)隊(duì)
本文將會(huì)從:Redis 使用場(chǎng)景與介紹 -> 數(shù)據(jù)結(jié)構(gòu)與簡(jiǎn)單使用 -> 小功能大用處 -> 持久化、主從同步與緩存設(shè)計(jì) -> 知識(shí)拓展 來(lái)書(shū)寫(xiě),初學(xué)的童鞋只要能記住 Redis 是用來(lái)干嘛,各功能的使用場(chǎng)景有哪些,然后對(duì) Redis 有個(gè)大概的認(rèn)識(shí)就好啦,剩下的以后有需要的時(shí)候再來(lái)查看和實(shí)踐吧~
文章真的有億點(diǎn)長(zhǎng),下面是目錄,建議先收藏再看~
目錄
Redis 介紹
Redis 是什么?
Redis 特性
Redis 典型使用場(chǎng)景
Redis 高并發(fā)原理
Redis 安裝、啟動(dòng)
redis conf 配置文件
Redis 數(shù)據(jù)結(jié)構(gòu)與命令使用
通用全局命令
常用全局命令
字符串使用
哈希 hash
列表(lists)
set 集合和 zset 有序集合
小功能大用處
慢查詢分析
Pipeline(流水線)機(jī)制
事務(wù)與 Lua
Bitmaps
HyperLogLog
發(fā)布訂閱
GEO
Redis 客戶端
持久化、主從同步與緩存設(shè)計(jì)
持久化
主從同步
緩存
知識(shí)拓展
緩存與數(shù)據(jù)庫(kù)同步策略
分布式鎖
關(guān)于集群
Redis 介紹
Redis 是什么?
Redis 是一個(gè)開(kāi)源(BSD 許可)的,內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)系統(tǒng),它可以用作數(shù)據(jù)庫(kù)、緩存和消息中間件;
Redis 支持多種類型的數(shù)據(jù)結(jié)構(gòu),如 字符串(strings),散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) ,范圍查詢, bitmaps, hyperloglogs 和 地理空間(geospatial) 索引半徑查詢;
Redis 內(nèi)置了復(fù)制(replication),LUA 腳本(Lua scripting),LRU 驅(qū)動(dòng)事件(LRU eviction),事務(wù)(transactions)和不同級(jí)別的 磁盤(pán)持久化(persistence);
Redis 通過(guò) 哨兵(Sentinel) 和自動(dòng)分區(qū)(Cluster)提供高可用性(high availability)。
Redis 特性
速度快
單節(jié)點(diǎn)讀110000次/s,寫(xiě)81000次/s
數(shù)據(jù)存放內(nèi)存中
用 C 語(yǔ)言實(shí)現(xiàn),離操作系統(tǒng)更近
單線程架構(gòu),6.0 開(kāi)始支持多線程(CPU、IO 讀寫(xiě)負(fù)荷)
持久化
數(shù)據(jù)的更新將異步地保存到硬盤(pán)(RDB 和 AOF)
多種數(shù)據(jù)結(jié)構(gòu) - 不僅僅支持簡(jiǎn)單的 key-value 類型數(shù)據(jù),還支持:字符串、hash、列表、集合、有序集合,
支持多種編程語(yǔ)言
功能豐富
HyperLogLog、GEO、發(fā)布訂閱、Lua腳本、事務(wù)、Pipeline、Bitmaps,key 過(guò)期
簡(jiǎn)單穩(wěn)定
源碼少、單線程模型
主從復(fù)制
Redis 支持?jǐn)?shù)據(jù)的備份(master-slave)與集群(分片存儲(chǔ)),以及擁有哨兵監(jiān)控機(jī)制。
Redis 的所有操作都是原子性的,同時(shí) Redis 還支持對(duì)幾個(gè)操作合并后的原子性執(zhí)行。
Redis 典型使用場(chǎng)景
緩存:
計(jì)數(shù)器:
消息隊(duì)列:
排行榜:
社交網(wǎng)絡(luò):
Redis 高并發(fā)原理
Redis 是純內(nèi)存數(shù)據(jù)庫(kù),一般都是簡(jiǎn)單的存取操作,線程占用的時(shí)間很多,時(shí)間的花費(fèi)主要集中在 IO 上,所以讀取速度快
Redis 使用的是非阻塞 IO,IO 多路復(fù)用,使用了單線程來(lái)輪詢描述符,將數(shù)據(jù)庫(kù)的開(kāi)、關(guān)、讀、寫(xiě)都轉(zhuǎn)換成了事件,減少了線程切換時(shí)上下文的切換和競(jìng)爭(zhēng)。
Redis 采用了單線程的模型,保證了每個(gè)操作的原子性,也減少了線程的上下文切換和競(jìng)爭(zhēng)。
Redis 存儲(chǔ)結(jié)構(gòu)多樣化,不同的數(shù)據(jù)結(jié)構(gòu)對(duì)數(shù)據(jù)存儲(chǔ)進(jìn)行了優(yōu)化,如壓縮表,對(duì)短數(shù)據(jù)進(jìn)行壓縮存儲(chǔ),再如,跳表,使用有序的數(shù)據(jù)結(jié)構(gòu)加快讀取的速度。
Redis 采用自己實(shí)現(xiàn)的事件分離器,效率比較高,內(nèi)部采用非阻塞的執(zhí)行方式,吞吐能力比較大。
Redis 安裝
這里只提供 linux 版本的安裝部署
下載 Redis
進(jìn)入官網(wǎng)找到下載地址:https://redis.io/download
右鍵 Download 按鈕,選擇復(fù)制鏈接地址,然后進(jìn)入 linux 的 shell 控制臺(tái):輸入 wget 將上面復(fù)制的下載鏈接粘貼上,如下命令:
wget?https://download.redis.io/releases/redis-6.2.4.tar.gz回車后等待下載完畢。
解壓并安裝 Redis
下載完成后需要將壓縮文件解壓,輸入以下命令解壓到當(dāng)前目錄:
tar?-zvxf?redis-6.2.4.tar.gz解壓后在根目錄上輸入 ls 列出所有目錄會(huì)發(fā)現(xiàn)與下載 redis 之前多了一個(gè) redis-6.2.4.tar.gz 文件和 redis-6.2.4 的目錄。
移動(dòng) Redis 目錄(可選)
若你不想在下載的目錄安裝 Redis,可以將 Redis 移動(dòng)到特定目錄安裝,我習(xí)慣放在 ‘/usr/local/’ 目錄下,所以我這里輸入命令將目前在 ‘/root’ 目錄下的 'redis-6.2.4' 文件夾更改目錄,同時(shí)修改其名字為 redis:
mv?/root/rredis-6.2.4?/usr/local/rediscd 到 '/usr/local' 目錄下輸入 ls 命令可以查詢到當(dāng)前目錄已經(jīng)多了一個(gè) redis 子目錄,同時(shí) '/root' 目錄下已經(jīng)沒(méi)有 'redis-6.2.4' 文件:
編譯
cd 到 '/usr/local/redis' 目錄,輸入命令 make 執(zhí)行編譯命令,接下來(lái)控制臺(tái)會(huì)輸出各種編譯過(guò)程中輸出的內(nèi)容:
make最終運(yùn)行結(jié)果如下:
安裝
輸入以下命令:
make?PREFIX=/usr/local/redis?install這里多了一個(gè)關(guān)鍵字 'PREFIX=' 這個(gè)關(guān)鍵字的作用是編譯的時(shí)候用于指定程序存放的路徑。比如我們現(xiàn)在就是指定了 redis 必須存放在 '/usr/local/redis' 目錄。假設(shè)不添加該關(guān)鍵字 linux 會(huì)將可執(zhí)行文件存放在 '/usr/local/bin' 目錄,庫(kù)文件會(huì)存放在 '/usr/local/lib' 目錄。配置文件會(huì)存放在 '/usr/local/etc 目錄。其他的資源文件會(huì)存放在 'usr/local/share' 目錄。這里指定好目錄也方便后續(xù)的卸載,后續(xù)直接 rm -rf /usr/local/redis 即可刪除 Redis。
執(zhí)行結(jié)果如下圖:
到此為止,Redis 已經(jīng)安裝完畢,可以開(kāi)始使用了~
Redis 啟動(dòng)
根據(jù)上面的操作已經(jīng)將 redis 安裝完成了。在目錄 ‘/usr/local/redis’ 輸入下面命令啟動(dòng) redis:
./bin/redis-server&?./redis.conf上面的啟動(dòng)方式是采取后臺(tái)進(jìn)程方式,下面是采取顯示啟動(dòng)方式(如在配置文件設(shè)置了 daemonize 屬性為 yes 則跟后臺(tái)進(jìn)程方式啟動(dòng)其實(shí)一樣):
./bin/redis-server?./redis.conf兩種方式區(qū)別無(wú)非是有無(wú)帶符號(hào)&的區(qū)別。redis-server 后面是配置文件,目的是根據(jù)該配置文件的配置啟動(dòng) redis 服務(wù)。redis.conf 配置文件允許自定義多個(gè)配置文件,通過(guò)啟動(dòng)時(shí)指定讀取哪個(gè)即可。
啟動(dòng)可以概括為:
最簡(jiǎn)默認(rèn)啟動(dòng)
- 安裝后在 bin 目錄下直接執(zhí)行 redis-server驗(yàn)證(ps –aux | grep redis)
動(dòng)態(tài)參數(shù)啟動(dòng)(可配置一下參數(shù),例如指定端口)
- ./bin/redis-server –port 6380配置文件啟動(dòng)
- ./bin/redis-server& ./redis.conf生產(chǎn)環(huán)境一般選擇配置啟動(dòng)
單機(jī)多實(shí)例配置文件可以用端口區(qū)分開(kāi)
注:若在進(jìn)行 redis 命令操作,直接在 redis 中的 bin 目錄下運(yùn)行 redis-cli 命令即可,若開(kāi)啟了多個(gè)則需要加上對(duì)應(yīng)的端口參數(shù):
若運(yùn)行 redis-cli 提示不未安裝,則安裝一下即可:
redis.conf 配置文件
在目錄 '/usr/local/redis' 下有一個(gè) redis.conf 的配置文件。我們上面啟動(dòng)方式就是執(zhí)行了該配置文件的配置運(yùn)行的。我們可以通過(guò) cat、vim、less 等 linux 內(nèi)置的讀取命令讀取該文件。
這里列舉下比較重要的配置項(xiàng):
這里我要將 daemonize 改為 yes,不然我每次啟動(dòng)都得在 redis-server 命令后面加符號(hào) &,不這樣操作則只要回到 linux 控制臺(tái)則 redis 服務(wù)會(huì)自動(dòng)關(guān)閉,同時(shí)也將 bind 注釋,將 p rotected-mode 設(shè)置為 no。這樣啟動(dòng)后我就可以在外網(wǎng)訪問(wèn)了。修改方式通過(guò) vim 或者你喜歡的方式即可:
vim?/usr/local/redis/redis.conf通過(guò) /daemonize 查找到屬性,默認(rèn)是 no,更改為 yes 即可。(通過(guò)/關(guān)鍵字查找出現(xiàn)多個(gè)結(jié)果則使用 n 字符切換到下一個(gè)即可,按 i 可以開(kāi)始編輯,ESC 退出編輯模式,輸入 :wq 命令保存并退出),如下圖:
其他屬性也是同樣方式查找和編輯即可。
安裝部署部分參考:https://www.cnblogs.com/hunanzp/p/12304622.html
Redis 數(shù)據(jù)結(jié)構(gòu)與命令使用
Redis 的數(shù)據(jù)結(jié)構(gòu)有:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集 合)。但這些只是 Redis 對(duì)外的數(shù)據(jù)結(jié)構(gòu),實(shí)際上每種數(shù)據(jù)結(jié)構(gòu)都有自己底層的內(nèi)部編碼實(shí)現(xiàn),而且是多種實(shí)現(xiàn), 這樣 Redis 會(huì)在合適的場(chǎng)景選擇合適的內(nèi)部編碼。
可以看到每種數(shù)據(jù)結(jié)構(gòu)都有兩種以上的內(nèi)部編碼實(shí)現(xiàn),例如 list 數(shù)據(jù)結(jié) 構(gòu)包含了 linkedlist 和 ziplist 兩種內(nèi)部編碼。同時(shí),有些內(nèi)部編碼,例如 ziplist, 可以作為多種外部數(shù)據(jù)結(jié)構(gòu)的內(nèi)部實(shí)現(xiàn),可以通過(guò) object encoding 命令查詢內(nèi)部編碼。
object?encoding?xxx??#?xxx?為鍵名Redis 所有的數(shù)據(jù)結(jié)構(gòu)都是以唯一的 key 字符串作為名稱,然后通過(guò)這個(gè)唯一 key 值來(lái)獲取相應(yīng)的 value 數(shù)據(jù)。不同類型的數(shù)據(jù)結(jié) 構(gòu)的差異就在于 value 的結(jié)構(gòu)不一樣。
通用全局命令
常用全局命令
keys:查看所有鍵
dbsize:鍵總數(shù)
exists key:檢查鍵是否存在
del key [key ...]:刪除鍵
expire key seconds:鍵過(guò)期
ttl key: 通過(guò) ttl 命令觀察鍵鍵的剩余過(guò)期時(shí)間
type key:鍵的數(shù)據(jù)結(jié)構(gòu)類型
簡(jiǎn)單使用截圖
根據(jù)上面的命令解釋,大家應(yīng)該比較容易看懂截圖里面的所有命令含義,這里就不過(guò)多解釋了。
字符串使用
字符串 string 是 Redis 最簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)。Redis 的字符串是動(dòng)態(tài)字符串,是可以修改的字符串,內(nèi)部結(jié)構(gòu)實(shí)現(xiàn)上類似于 Java 的 ArrayList,采用預(yù)分配冗余空間的方式來(lái)減少內(nèi)存的頻繁分配。
字符串結(jié)構(gòu)使用非常廣泛,一個(gè)常見(jiàn)的用途就是緩存用戶信息。我們將用戶信息結(jié)構(gòu)體 使用 JSON 序列化成字符串,然后將序列化后的字符串塞進(jìn) Redis 來(lái)緩存。同樣,取用戶 信息會(huì)經(jīng)過(guò)一次反序列化的過(guò)程。
常用字符串命令
set key value [ex seconds][px milliseconds] [nx|xx]: 設(shè)置值,返回 ok 表示成功
ex seconds:為鍵設(shè)置秒級(jí)過(guò)期時(shí)間。
px milliseconds:為鍵設(shè)置毫秒級(jí)過(guò)期時(shí)間。
nx:鍵必須不存在,才可以設(shè)置成功,用于添加??蓡为?dú)用 setnx 命令替代
xx:與 nx 相反,鍵必須存在,才可以設(shè)置成功,用于更新??蓡为?dú)用 setxx 命令替代
get key:獲取值
mset key value [key value ...]:批量設(shè)置值,批量操作命令可以有效提高業(yè)務(wù)處理效率
mget key [key ...]:批量獲取值,批量操作命令可以有效提高業(yè)務(wù)處理效率
incr key:計(jì)數(shù),返回結(jié)果分 3 種情況:
值不是整數(shù),返回錯(cuò)誤。
值是整數(shù),返回自增后的結(jié)果。
鍵不存在,按照值為 0 自增,返回結(jié)果為 1。
decr(自減)、incrby(自增指定數(shù)字)、 decrby(自減指定數(shù)字)
字符串簡(jiǎn)單使用截圖
根據(jù)上面的命令解釋,大家應(yīng)該比較容易看懂截圖里面的所有命令含義,這里就不過(guò)多解釋了。
字符串使用場(chǎng)景
緩存數(shù)據(jù),提高查詢性能。比如存儲(chǔ)登錄用戶信息、電商中存儲(chǔ)商品信息
可以做計(jì)數(shù)器(想知道什么時(shí)候封鎖一個(gè) IP 地址(訪問(wèn)超過(guò)幾次)),短信限流
共享 Session,例如:一個(gè)分布式 Web 服務(wù)將用戶的 Session 信息(例如用戶登錄信息)保存在各自服務(wù)器中,這樣會(huì)造成一個(gè)問(wèn)題,出于負(fù)載均衡的考慮,分布式服務(wù)會(huì)將用戶的訪問(wèn)均衡到不同服務(wù)器上,用戶刷新一次訪問(wèn)可 能會(huì)發(fā)現(xiàn)需要重新登錄,為了解決這個(gè)問(wèn)題,可以使用 Redis 將用戶的 Session 進(jìn)行集中管理,在這種模式下只要保證 Redis 是高可用和擴(kuò)展性的,每次用戶 更新或者查詢登錄信息都直接從 Redis 中集中獲取,如圖:
哈希 hash
哈希相當(dāng)于 Java 中的 HashMap,以及 Js 中的 Map,內(nèi)部是無(wú)序字典。實(shí)現(xiàn)原理跟 HashMap 一致。一個(gè)哈希表有多個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)保存一個(gè)鍵值對(duì)。
與 Java 中的 HashMap 不同的是,rehash 的方式不一樣,因?yàn)?Java 的 HashMap 在字典很大時(shí),rehash 是個(gè)耗時(shí)的操作,需要一次性全部 rehash。
Redis 為了高性能,不能堵塞服務(wù),所以采用了漸進(jìn)式 rehash 策略。
漸進(jìn)式 rehash 會(huì)在 rehash 的同時(shí),保留新舊兩個(gè) hash 結(jié)構(gòu),查詢時(shí)會(huì)同時(shí)查詢兩個(gè) hash 結(jié)構(gòu),然后在后續(xù)的定時(shí)任務(wù)中以及 hash 操作指令中,循序漸進(jìn)地將舊 hash 的內(nèi)容一點(diǎn)點(diǎn)遷移到新的 hash 結(jié)構(gòu)中。當(dāng)搬遷完成了,就會(huì)使用新的 hash 結(jié)構(gòu)取而代之。
當(dāng) hash 移除了最后一個(gè)元素之后,該數(shù)據(jù)結(jié)構(gòu)自動(dòng)被刪除,內(nèi)存被回收。
常用哈希命令
hset key field value:設(shè)置值
hget key field:獲取值
hdel key field [field ...]:刪除 field
hlen key:計(jì)算 field 個(gè)數(shù)
hmset key field value [field value ...]:批量設(shè)置 field-value
hmget key field [field ...]:批量獲取 field-value
hexists key field:判斷 field 是否存在
hkeys key:獲取所有 field
hvals key:獲取所有 value
hgetall key:獲取所有的 field-value
incrbyfloat 和 hincrbyfloat:就像 incrby 和 incrbyfloat 命令一樣,但是它們的作 用域是 filed
哈希簡(jiǎn)單使用截圖
根據(jù)上面的命令解釋,大家應(yīng)該比較容易看懂截圖里面的所有命令含義,這里同樣不過(guò)多解釋了
哈希使用場(chǎng)景
Hash 也可以同于對(duì)象存儲(chǔ),比如存儲(chǔ)用戶信息,與字符串不一樣的是,字符串是需要將對(duì)象進(jìn)行序列化(比如 json 序列化)之后才能保存,而 Hash 則可以講用戶對(duì)象的每個(gè)字段單獨(dú)存儲(chǔ),這樣就能節(jié)省序列化和反序列的時(shí)間。如下:
此外還可以保存用戶的購(gòu)買(mǎi)記錄,比如 key 為用戶 id,field 為商品 i d,value 為商品數(shù)量。同樣還可以用于購(gòu)物車數(shù)據(jù)的存儲(chǔ),比如 key 為用戶 id,field 為商品 id,value 為購(gòu)買(mǎi)數(shù)量等等:
列表(lists)
Redis 中的 lists 相當(dāng)于 Java 中的 LinkedList,實(shí)現(xiàn)原理是一個(gè)雙向鏈表(其底層是一個(gè)快速列表),即可以支持反向查找和遍歷,更方便操作。插入和刪除操作非???#xff0c;時(shí)間復(fù)雜度為 O(1),但是索引定位很慢,時(shí)間復(fù)雜度為 O(n)。
常用列表命令
rpush key value [value ...]:從右邊插入元素
lpush key value [value ...]:從左邊插入元素
linsert key before|after pivot value:向某個(gè)元素前或者后插入元素
lrange key start end:獲取指定范圍內(nèi)的元素列表,lrange key 0 -1可以從左到右獲取列表的所有元素
lindex key index:獲取列表指定索引下標(biāo)的元素
llen key:獲取列表長(zhǎng)度
lpop key:從列表左側(cè)彈出元素
rpop key:從列表右側(cè)彈出
lrem key count value:刪除指定元素,lrem 命令會(huì)從列表中找到等于 value 的元素進(jìn)行刪除,根據(jù) count 的不同 分為三種情況:
·count>0,從左到右,刪除最多 count 個(gè)元素。
count<0,從右到左,刪除最多 count 絕對(duì)值個(gè)元素。
count=0,刪除所有。
ltrim key start end:按照索引范圍修剪列表
lset key index newValue:修改指定索引下標(biāo)的元素
blpop key [key ...] timeout 和 brpop key [key ...] timeout:阻塞式彈出
列表簡(jiǎn)單使用截圖
根據(jù)上面的命令解釋,大家應(yīng)該比較容易看懂截圖里面的所有命令含義,這里同樣不過(guò)多解釋了
列表使用場(chǎng)景
熱銷榜,文章列表
實(shí)現(xiàn)工作隊(duì)列(利用 lists 的 push 操作,將任務(wù)存在 lists 中,然后工作線程再用 pop 操作將任務(wù)取出進(jìn)行執(zhí)行 ),例如消息隊(duì)列
最新列表,比如最新評(píng)論
使用參考:
lpush+lpop=Stack(棧)
lpush+rpop=Queue(隊(duì)列)
lpsh+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息隊(duì)列)
set 集合和 zset 有序集合
Redis 的集合相當(dāng)于 Java 語(yǔ)言里面的 HashSet 和 JS 里面的 Set,它內(nèi)部的鍵值對(duì)是無(wú)序的唯一的。Set 集合中最后一個(gè) value 被移除后,數(shù)據(jù)結(jié)構(gòu)自動(dòng)刪除,內(nèi)存被回收。
zset 可能是 Redis 提供的最為特色的數(shù)據(jù)結(jié)構(gòu),它也是在面試中面試官最愛(ài)問(wèn)的數(shù)據(jù)結(jié)構(gòu)。它類似于 Java 的 SortedSet 和 HashMap 的結(jié)合體,一方面它是一個(gè) set,保證了內(nèi)部 value 的唯一性,另一方面它可以給每個(gè) value 賦予一個(gè) score,代表這個(gè) value 的排序權(quán)重。它的內(nèi)部實(shí)現(xiàn)用的是一種叫著「跳躍列表」(后面會(huì)簡(jiǎn)單介紹)的數(shù)據(jù)結(jié)構(gòu)。
常用集合命令
sadd key element [element ...]:添加元素,返回結(jié)果為添加成功的元素個(gè)數(shù)
srem key element [element ...]:刪除元素,返回結(jié)果為成功刪除元素個(gè)數(shù)
smembers key:獲取所有元素
sismember key element:判斷元素是否在集合中,如果給定元素 element 在集合內(nèi)返回 1,反之返回 0
scard key:計(jì)算元素個(gè)數(shù),scard 的時(shí)間復(fù)雜度為 O(1),它不會(huì)遍歷集合所有元素
spop key:從集合隨機(jī)彈出元素,從 3.2 版本開(kāi)始,spop 也支持[count]參數(shù)。
srandmember key [count]:隨機(jī)從集合返回指定個(gè)數(shù)元素,[count]是可選參數(shù),如果不寫(xiě)默認(rèn)為 1
sinter key [key ...]:求多個(gè)集合的交集
suinon key [key ...]:求多個(gè)集合的并集
sdiff key [key ...]:求多個(gè)集合的差集
集合簡(jiǎn)單使用截圖
常用有序集合命令
zadd key score member [score member ...]:添加成員,返回結(jié)果代表成功添加成員的個(gè)數(shù)。Redis3.2 為 zadd 命令添加了 nx、xx、ch、incr 四個(gè)選項(xiàng):
nx:member 必須不存在,才可以設(shè)置成功,用于添加
xx:member 必須存在,才可以設(shè)置成功,用于更新
ch:返回此次操作后,有序集合元素和分?jǐn)?shù)發(fā)生變化的個(gè)數(shù)
incr:對(duì) score 做增加,相當(dāng)于后面介紹的 zincrby
zcard key:計(jì)算成員個(gè)數(shù)
zscore key member:計(jì)算某個(gè)成員的分?jǐn)?shù)
zrank key member 和 zrevrank key member:計(jì)算成員的排名,zrank 是從分?jǐn)?shù)從低到高返回排名,zrevrank 反之
zrem key member [member ...]:刪除成員
zincrby key increment member:增加成員的分?jǐn)?shù)
zrange key start end [withscores] 和 zrevrange key start end [withscores]:返回指定排名范圍的成員,zrange 是從低到高返回,zrevrange 反之。
zrangebyscore key min max [withscores][limit offset count] 和 zrevrangebyscore key max min [withscores][limit offset count] 返回指定分?jǐn)?shù)范圍的成員,其中 zrangebyscore 按照分?jǐn)?shù)從低到高返回,zrevrangebyscore 反之
zcount key min max:返回指定分?jǐn)?shù)范圍成員個(gè)數(shù)
zremrangebyrank key start end:刪除指定排名內(nèi)的升序元素
zremrangebyscore key min max:刪除指定分?jǐn)?shù)范圍的成員
zinterstore 和 zunionstore 命令求集合的交集和并集,可用參數(shù)比較多,可用到再查文檔
有序集合相比集合提供了排序字段,但是也產(chǎn)生了代價(jià),zadd 的時(shí)間 復(fù)雜度為 O(log(n)),sadd 的時(shí)間復(fù)雜度為 O(1)。
有序集合簡(jiǎn)單使用截圖
集合和有序集合使用場(chǎng)景
給用戶添加標(biāo)簽
給標(biāo)簽添加用戶
根據(jù)某個(gè)權(quán)重進(jìn)行排序的隊(duì)列的場(chǎng)景,比如游戲積分排行榜,設(shè)置優(yōu)先級(jí)的任務(wù)列表,學(xué)生成績(jī)表等
關(guān)于跳躍列表
跳躍列表就是一種層級(jí)制,最下面一層所有的元素都會(huì)串起來(lái)。然后每隔幾個(gè)元素挑選出一個(gè)代表來(lái),再將這幾個(gè)代表使用另外一級(jí)指針串起來(lái)。然后在這些代表里再挑出二級(jí)代表,再串起來(lái)。最終就形成了金字塔結(jié)構(gòu),如圖:
更多可以看:https://www.jianshu.com/p/09c3b0835ba6
列表、集合和有序集合異同
小功能大用處
慢查詢分析
許多存儲(chǔ)系統(tǒng)(例如 MySQL)提供慢查詢?nèi)罩編椭_(kāi)發(fā)和運(yùn)維人員定位系統(tǒng)存在的慢操作。
所謂慢查詢?nèi)罩揪褪窍到y(tǒng)在命令執(zhí)行前后計(jì)算每條命令的執(zhí)行時(shí)間,當(dāng)超過(guò)預(yù)設(shè)閾值,就將這條命令的相關(guān)信息(例如:發(fā)生時(shí)間,耗時(shí),命令的詳細(xì)信息)記錄下來(lái),Redis 也提供了類似的功能。這里可以順帶了解一下 Redis 客戶端執(zhí)行一條命令的過(guò)程,分為如下 4 個(gè)部分:
對(duì)于慢查詢功能,需要明確 3 件事:
1、預(yù)設(shè)閾值怎么設(shè)置?
在 redis 配置文件中修改配置 ‘slowlog-log-slower-than’ 的值,單位是微妙(1 秒 = 1000 毫秒 = 1000000 微秒),默認(rèn)是 10000 微秒,如果把 slowlog-log-slower-than 設(shè)置為 0,將會(huì)記錄所有命令到日志中。如果把 slowlog-log-slower-than 設(shè)置小于 0,將會(huì)不記錄任何命令到日志中。
2、慢查詢記錄存放在哪?
在 redis 配置文件中修改配置 ‘slowlog-max-len’ 的值。slowlog-max-len 的作用是指定慢查詢?nèi)罩咀疃啻鎯?chǔ)的條數(shù)。實(shí)際上,Redis 使用了一個(gè)列表存放慢查詢?nèi)罩?#xff0c;slowlog-max-len 就是這個(gè)列表的最大長(zhǎng)度。當(dāng)一個(gè)新的命令滿足滿足慢查詢條件時(shí),被插入這個(gè)列表中。當(dāng)慢查詢?nèi)罩玖斜硪呀?jīng)達(dá)到最大長(zhǎng)度時(shí),最早插入的那條命令將被從列表中移出。比如,slowlog-max-len 被設(shè)置為 10,當(dāng)有第 11 條命令插入時(shí),在列表中的第 1 條命令先被移出,然后再把第 11 條命令放入列表。
記錄慢查詢指 Redis 會(huì)對(duì)長(zhǎng)命令進(jìn)行截?cái)?#xff0c;不會(huì)大量占用大量?jī)?nèi)存。在實(shí)際的生產(chǎn)環(huán)境中,為了減緩慢查詢被移出的可能和更方便地定位慢查詢,建議將慢查詢?nèi)罩镜拈L(zhǎng)度調(diào)整的大一些。比如可以設(shè)置為 1000 以上。
除了去配置文件中修改,也可以通過(guò) config set 命令動(dòng)態(tài)修改配置
>?config?set?slowlog-log-slower-than?1000 OK >?config?set?slowlog-max-len?1200 OK >?config?rewrite OK3、如何獲取慢查詢?nèi)罩?#xff1f;
可以使用 slowlog get 命令獲取慢查詢?nèi)罩?#xff0c;在 slowlog get 后面還可以加一個(gè)數(shù)字,用于指定獲取慢查詢?nèi)罩镜臈l數(shù),比如,獲取 2 條慢查詢?nèi)罩?#xff1a;
>?slowlog?get?3 1)?1)?(integer)?61072)?(integer)?16163989303)?(integer)?31094)?1)?"config"2)?"rewrite" 2)?1)?(integer)?61062)?(integer)?16137017883)?(integer)?360044)?1)?"flushall"可以看出每一條慢查詢?nèi)罩径加?4 個(gè)屬性組成:
唯一標(biāo)識(shí) ID
命令執(zhí)行的時(shí)間戳
命令執(zhí)行時(shí)長(zhǎng)
執(zhí)行的命名和參數(shù)
此外,可以通過(guò) slowlog len 命令獲取慢查詢?nèi)罩镜拈L(zhǎng)度;通過(guò) slowlog reset 命令清理慢查詢?nèi)罩尽?/p>
Pipeline(流水線)機(jī)制
Redis 提供了批量操作命令(例如 mget、mset 等),有效地節(jié)約 RTT。但大部分命令是不支持批量操作的,例如要執(zhí)行 n 次 hgetall 命令,并沒(méi)有 mhgetall 命令存在,需要消耗 n 次 RTT。
Redis 的客戶端和服務(wù)端可能部署在不同的機(jī)器上。例如客戶端在北京,Redis 服務(wù)端在上海,兩地直線距離約為 1300 公里,那么 1 次 RTT 時(shí)間 = 1300×2/(300000×2/3) = 13 毫秒(光在真空中 傳輸速度為每秒 30 萬(wàn)公里,這里假設(shè)光纖為光速的 2/3),那么客戶端在 1 秒 內(nèi)大約只能執(zhí)行 80 次左右的命令,這個(gè)和 Redis 的高并發(fā)高吞吐特性背道而馳。
Pipeline(流水線)機(jī)制能改善上面這類問(wèn)題,它能將一組 Redis 命令進(jìn) 行組裝,通過(guò)一次 RTT 傳輸給 Redis,再將這組 Redis 命令的執(zhí)行結(jié)果按順序返回給客戶端。
不使用 Pipeline 的命令執(zhí)行流程:
使用 Pipeline 的命令執(zhí)行流程:
Redis 的流水線是一種通信協(xié)議,沒(méi)有辦法通過(guò)客戶端演示給大家,這里以 Jedis 為例,通過(guò) Java API 或者使用 Spring 操作它(代碼來(lái)源于互聯(lián)網(wǎng)):
/***?測(cè)試Redis流水線*?@author?liu*/ publicclass?TestPipelined?{/***?使用Java?API測(cè)試流水線的性能*/@SuppressWarnings({?"unused",?"resource"?})@Testpublic?void?testPipelinedByJavaAPI()?{JedisPoolConfig?jedisPoolConfig?=?new?JedisPoolConfig();jedisPoolConfig.setMaxIdle(20);jedisPoolConfig.setMaxTotal(10);jedisPoolConfig.setMaxWaitMillis(20000);JedisPool?jedisPool?=?new?JedisPool(jedisPoolConfig,"localhost",6379);Jedis?jedis?=?jedisPool.getResource();long?start?=?System.currentTimeMillis();//?開(kāi)啟流水線Pipeline?pipeline?=?jedis.pipelined();//?測(cè)試10w條數(shù)據(jù)讀寫(xiě)for(int?i?=?0;?i?<?100000;?i++)?{int?j?=?i?+?1;pipeline.set("key"?+?j,?"value"?+?j);pipeline.get("key"?+?j);}//?只執(zhí)行同步但不返回結(jié)果//pipeline.sync();//?以list的形式返回執(zhí)行過(guò)的命令的結(jié)果List<Object>?result?=?pipeline.syncAndReturnAll();long?end?=?System.currentTimeMillis();//?計(jì)算耗時(shí)System.out.println("耗時(shí)"?+?(end?-?start)?+?"毫秒");}/***?使用RedisTemplate測(cè)試流水線*/@SuppressWarnings({?"resource",?"rawtypes",?"unchecked",?"unused"?})@Testpublic?void?testPipelineBySpring()?{ApplicationContext?applicationContext?=?new?ClassPathXmlApplicationContext("spring.xml");RedisTemplate?rt?=?(RedisTemplate)applicationContext.getBean("redisTemplate");SessionCallback?callback?=?(SessionCallback)(RedisOperations?ops)->{for(int?i?=?0;?i?<?100000;?i++)?{int?j?=?i?+?1;ops.boundValueOps("key"?+?j).set("value"?+?j);ops.boundValueOps("key"?+?j).get();}returnnull;};long?start?=?System.currentTimeMillis();//?執(zhí)行Redis的流水線命令List?result?=?rt.executePipelined(callback);long?end?=?System.currentTimeMillis();System.out.println(end?-?start);} }網(wǎng)上寫(xiě)的測(cè)試結(jié)果為:使用 Java API 耗時(shí)在 550ms 到 700ms 之間,也就是不到 1s 就完成了 10 萬(wàn)次讀寫(xiě),使用 Spring 耗時(shí)在 1100ms 到 1300ms 之間。這個(gè)與之前一條一條命令使用,1s 內(nèi)就發(fā)送幾十幾百條(客戶端和服務(wù)端距離導(dǎo)致)命令的差距不是一般的大了。
注意,這里只是為了測(cè)試性能而已,當(dāng)你要執(zhí)行很多的命令并返回結(jié)果的時(shí)候,需要考慮 List 對(duì)象的大小,因?yàn)樗鼤?huì)“吃掉”服務(wù)器上許多的內(nèi)存空間,嚴(yán)重時(shí)會(huì)導(dǎo)致內(nèi)存不足,引發(fā) JVM 溢出異常,所以在工作環(huán)境中,是需要讀者自己去評(píng)估的,可以考慮使用迭代的方式去處理。
事務(wù)與 Lua
multi 和 exec 命令
很多情況下我們需要一次執(zhí)行不止一個(gè)命令,而且需要其同時(shí)成功或者失敗。為了保證多條命令組合的原子性,Redis 提供了簡(jiǎn)單的事務(wù)功能以及集成 Lua 腳本來(lái)解決這個(gè)問(wèn)題。
Redis 提供了簡(jiǎn)單的事務(wù)功能,將一組需要一起執(zhí)行的命令放到 multi 和 exec 兩個(gè)命令之間。Multi 命令代表事務(wù)開(kāi)始,exec 命令代表事務(wù)結(jié)束,它們之間的命令是原子順序執(zhí)行的。使用案例:
127.0.0.1:6379>?multi OK 127.0.0.1:6379>?SET?msg?"hello?chrootliu" QUEUED 127.0.0.1:6379>?GET?msg QUEUED 127.0.0.1:6379>?EXEC 1)?OK 1)?hello?chrootliuRedis 提供了簡(jiǎn)單的事務(wù),之所以說(shuō)它簡(jiǎn)單,主要是因?yàn)樗恢С质聞?wù)中的回滾特性,同時(shí)無(wú)法實(shí)現(xiàn)命令之間的邏輯關(guān)系計(jì)算,主要有以下幾點(diǎn):
不夠滿足原子性。一個(gè)事務(wù)執(zhí)行過(guò)程中,其他事務(wù)或 client 是可以對(duì)相應(yīng)的 key 進(jìn)行修改的(并發(fā)情況下,例如電商常見(jiàn)的超賣(mài)問(wèn)題),想要避免這樣的并發(fā)性問(wèn)題就需要使用 WATCH 命令,但是通常來(lái)說(shuō),必須經(jīng)過(guò)仔細(xì)考慮才能決定究竟需要對(duì)哪些 key 進(jìn)行 WATCH 加鎖。然而,額外的 WATCH 會(huì)增加事務(wù)失敗的可能,而缺少必要的 WATCH 又會(huì)讓我們的程序產(chǎn)生競(jìng)爭(zhēng)條件。
后執(zhí)行的命令無(wú)法依賴先執(zhí)行命令的結(jié)果。由于事務(wù)中的所有命令都是互相獨(dú)立的,在遇到 exec 命令之前并沒(méi)有真正的執(zhí)行,所以我們無(wú)法在事務(wù)中的命令中使用前面命令的查詢結(jié)果。我們唯一可以做的就是通過(guò) watch 保證在我們進(jìn)行修改時(shí),如果其它事務(wù)剛好進(jìn)行了修改,則我們的修改停止,然后應(yīng)用層做相應(yīng)的處理。
事務(wù)中的每條命令都會(huì)與 Redis 服務(wù)器進(jìn)行網(wǎng)絡(luò)交互。Redis 事務(wù)開(kāi)啟之后,每執(zhí)行一個(gè)操作返回的都是 queued,這里就涉及到客戶端與服務(wù)器端的多次交互,明明是需要一次批量執(zhí)行的 n 條命令,還需要通過(guò)多次網(wǎng)絡(luò)交互,顯然非常浪費(fèi)(這個(gè)就是為什么會(huì)有 pipeline 的原因,減少 RTT 的時(shí)間)。
Redis 事務(wù)缺陷的解決 – Lua
Lua 是一個(gè)小巧的腳本語(yǔ)言,用標(biāo)準(zhǔn) C 編寫(xiě),幾乎在所有操作系統(tǒng)和平臺(tái)上都可以編譯運(yùn)行。一個(gè)完整的 Lua 解釋器不過(guò) 200k,在目前所有腳本引擎中,Lua 的速度是最快的,這一切都決定了 Lua 是作為嵌入式腳本的最佳選擇。
Redis 2.6 版本之后內(nèi)嵌了一個(gè) Lua 解釋器,可以用于一些簡(jiǎn)單的事務(wù)與邏輯運(yùn)算,也可幫助開(kāi)發(fā)者定制自己的 Redis 命令(例如:一次性的執(zhí)行復(fù)雜的操作,和帶有邏輯判斷的操作),在這之前,必須修改源碼。
在 Redis 中執(zhí)行 Lua 腳本有兩種方法:eval 和 evalsha,這里以 eval 做為案例介紹:
eval 語(yǔ)法:
eval?script?numkeys?key?[key?...]?arg?[arg?...]其中:
script 一段 Lua 腳本或 Lua 腳本文件所在路徑及文件名
numkeys Lua 腳本對(duì)應(yīng)參數(shù)數(shù)量
key [key …] Lua 中通過(guò)全局變量 KEYS 數(shù)組存儲(chǔ)的傳入?yún)?shù)
arg [arg …] Lua 中通過(guò)全局變量 ARGV 數(shù)組存儲(chǔ)的傳入附加參數(shù)
Lua 執(zhí)行流程圖:
SCRIPT LOAD 與 EVALSHA 命令
對(duì)于不立即執(zhí)行的 Lua 腳本,或需要重用的 Lua 腳本,可以通過(guò) SCRIPT LOAD 提前載入 Lua 腳本,這個(gè)命令會(huì)立即返回對(duì)應(yīng)的 SHA1 校驗(yàn)碼
當(dāng)需要執(zhí)行函數(shù)時(shí),通過(guò) EVALSHA 調(diào)用 SCRIPT LOAD 返回的 SHA1 即可
SCRIPT?LOAD?"return?{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" "232fd51614574cf0867b83d384a5e898cfd24e5a"EVALSHA?"232fd51614574cf0867b83d384a5e898cfd24e5a"?2?key1?key2?first?second 1)?"key1" 2)?"key2" 3)?"first" 4)?"second"通過(guò) Lua 腳本執(zhí)行 Redis 命令
在 Lua 腳本中,只要使用 redis.call() 或 redis.pcall() 傳入 Redis 命令就可以直接執(zhí)行:
eval?"return?redis.call('set',KEYS[1],'bar')"?1?foo?????--等同于在服務(wù)端執(zhí)行?set?foo?bar案例,使用 Lua 腳本實(shí)現(xiàn)訪問(wèn)頻率限制:
-- --?KEYS[1]?要限制的ip --?ARGV[1]?限制的訪問(wèn)次數(shù) --?ARGV[2]?限制的時(shí)間 --local?key?=?"rate.limit:"?..?KEYS[1] local?limit?=?tonumber(ARGV[1]) local?expire_time?=?ARGV[2]local?is_exists?=?redis.call("EXISTS",?key) if?is_exists?==?1thenif?redis.call("INCR",?key)?>?limit?thenreturn0elsereturn1end elseredis.call("SET",?key,?1)redis.call("EXPIRE",?key,?expire_time)return1 end使用方法,通過(guò):
eval(file_get_contents(storage_path("limit.lua")),?3,?"127.0.0.1",?"3",?"100");redis 的事務(wù)與 Lua,就先介紹到這里了,更多的用法大家請(qǐng)查看 Lua 官方文檔
Bitmaps
許多開(kāi)發(fā)語(yǔ)言都提供了操作位的功能,合理地使用位能夠有效地提高內(nèi)存使用率和開(kāi)發(fā)效率。Redis 提供了 Bitmaps 這個(gè)“數(shù)據(jù)結(jié)構(gòu)”可以實(shí)現(xiàn)對(duì)位的操作。把數(shù)據(jù)結(jié)構(gòu)加上引號(hào)主要因?yàn)?#xff1a;
Bitmaps 本身不是一種數(shù)據(jù)結(jié)構(gòu),實(shí)際上它就是字符串,但是它可以對(duì)字符串的位進(jìn)行操作。
Bitmaps 單獨(dú)提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同??梢园?Bitmaps 想象成一個(gè)以位為單位的數(shù)組,數(shù)組的每個(gè)單元只能存儲(chǔ) 0 和 1,數(shù)組的下標(biāo)在 Bitmaps 中叫做偏移量。
在我們平時(shí)開(kāi)發(fā)過(guò)程中,會(huì)有一些 bool 型數(shù)據(jù)需要存取,比如用戶一年的簽到記錄, 簽了是 1,沒(méi)簽是 0,要記錄 365 天。如果使用普通的 key/value,每個(gè)用戶要記錄 365 個(gè),當(dāng)用戶上億的時(shí)候,需要的存儲(chǔ)空間是驚人的。為了解決這個(gè)問(wèn)題,Redis 提供了位圖數(shù)據(jù)結(jié)構(gòu),這樣每天的簽到記錄只占據(jù)一個(gè)位, 365 天就是 365 個(gè)位,46 個(gè)字節(jié) (一個(gè)稍長(zhǎng)一點(diǎn)的字符串) 就可以完全容納下,這就大大節(jié)約了存儲(chǔ)空間。
語(yǔ)法:
setbit?key?offset?value??#?設(shè)置或者清空?key?的?value(字符串)在?offset?處的?bit?值 getbit?key?offset??#?返回?key?對(duì)應(yīng)的?string?在?offset?處的?bit?值 bitcount?key?[start?end]?#?start?end?范圍內(nèi)被設(shè)置為1的數(shù)量,不傳遞?start?end?默認(rèn)全范圍使用案例,統(tǒng)計(jì)用戶登錄(活躍)情況
127.0.0.1:6379>?setbit?userLogin:2021-04-10?66666?1?#userId=66666的用戶登錄,這是今天登錄的第一個(gè)用戶。 (integer)?0 127.0.0.1:6379>?setbit?userLogin:2021-04-10?999999?1?#userId=999999的用戶登錄,這是今天第二個(gè)登錄、的用戶。 (integer)?0 127.0.0.1:6379>?setbit?userLogin:2021-04-10?3333?1 (integer)?0 127.0.0.1:6379>?setbit?userLogin:2021-04-10?8888?1 (integer)?0 127.0.0.1:6379>?setbit?userLogin:2021-04-10?100000?1 (integer)?0127.0.0.1:6379>?getbit?active:2021-04-10?66666 (integer)?1 127.0.0.1:6379>?getbit?active:2021-04-10?55555 (integer)127.0.0.1:6379>?bitcount?active:2021-04-10 (integer)?5由于 bit 數(shù)組的每個(gè)位置只能存儲(chǔ) 0 或者 1 這兩個(gè)狀態(tài);所以對(duì)于實(shí)際生活中,處理兩個(gè)狀態(tài)的業(yè)務(wù)場(chǎng)景就可以考慮使用 bitmaps。如用戶登錄/未登錄,簽到/未簽到,關(guān)注/未關(guān)注,打卡/未打卡等。同時(shí) bitmap 還通過(guò)了相關(guān)的統(tǒng)計(jì)方法進(jìn)行快速統(tǒng)計(jì)。
HyperLogLog
HyperLogLog 并不是一種新的數(shù)據(jù)結(jié)構(gòu)(實(shí)際類型為字符串類型),而 是一種基數(shù)算法,通過(guò) HyperLogLog 可以利用極小的內(nèi)存空間完成獨(dú)立總數(shù)的統(tǒng)計(jì),數(shù)據(jù)集可以是 IP、Email、ID 等。
HyperLogLog 提供了 3 個(gè)命令:pfadd、pfcount、pfmerge。
#?用于向?HyperLogLog?添加元素 #?如果?HyperLogLog?估計(jì)的近似基數(shù)在?PFADD?命令執(zhí)行之后出現(xiàn)了變化,?那么命令返回?1?,?否則返回?0 #?如果命令執(zhí)行時(shí)給定的鍵不存在,?那么程序?qū)⑾葎?chuàng)建一個(gè)空的?HyperLogLog?結(jié)構(gòu),?然后再執(zhí)行命令 pfadd?key?value1?[value2?value3]#?PFCOUNT?命令會(huì)給出?HyperLogLog?包含的近似基數(shù) #?在計(jì)算出基數(shù)后, PFCOUNT 會(huì)將值存儲(chǔ)在 HyperLogLog 中進(jìn)行緩存,知道下次 PFADD 執(zhí)行成功前,就都不需要再次進(jìn)行基數(shù)的計(jì)算。 pfcount?key# PFMERGE 將多個(gè) HyperLogLog 合并為一個(gè) HyperLogLog ,?合并后的 HyperLogLog 的基數(shù)接近于所有輸入 HyperLogLog 的并集基數(shù)。 pfmerge?destkey?key1?key2?[...keyn] 127.0.0.1:6379>?pfadd?totaluv?user1 (integer)?1 127.0.0.1:6379>?pfcount?totaluv (integer)?1 127.0.0.1:6379>?pfadd?totaluv?user2 (integer)?1 127.0.0.1:6379>?pfcount?totaluv (integer)?2 127.0.0.1:6379>?pfadd?totaluv?user3 (integer)?1 127.0.0.1:6379>?pfcount?totaluv (integer)?3 127.0.0.1:6379>?pfadd?totaluv?user4 (integer)?1 127.0.0.1:6379>?pfcount?totaluv (integer)?4 127.0.0.1:6379>?pfadd?totaluv?user5 (integer)?1 127.0.0.1:6379>?pfcount?totaluv (integer)?5 127.0.0.1:6379>?pfadd?totaluv?user6?user7?user8?user9?user10 (integer)?1 127.0.0.1:6379>?pfcount?totaluv (integer)?10HyperLogLog 內(nèi)存占用量非常小,但是存在錯(cuò)誤率,開(kāi)發(fā)者在進(jìn)行數(shù)據(jù) 229 結(jié)構(gòu)選型時(shí)只需要確認(rèn)如下兩條即可:
只為了計(jì)算獨(dú)立總數(shù),不需要獲取單條數(shù)據(jù)。
可以容忍一定誤差率,畢竟 HyperLogLog 在內(nèi)存的占用量上有很大的優(yōu)勢(shì)。
例如:如果你負(fù)責(zé)開(kāi)發(fā)維護(hù)一個(gè)大型的網(wǎng)站,有一天老板找產(chǎn)品經(jīng)理要網(wǎng)站每個(gè)網(wǎng)頁(yè)每天的 UV 數(shù)據(jù),然后讓你來(lái)開(kāi)發(fā)這個(gè)統(tǒng)計(jì)模塊,你會(huì)如何實(shí)現(xiàn)?
如果統(tǒng)計(jì) PV 那非常好辦,給每個(gè)網(wǎng)頁(yè)一個(gè)獨(dú)立的 Redis 計(jì)數(shù)器就可以了,這個(gè)計(jì)數(shù)器 的 key 后綴加上當(dāng)天的日期。這樣來(lái)一個(gè)請(qǐng)求,incrby 一次,最終就可以統(tǒng)計(jì)出所有的 PV 數(shù)據(jù)。
但是 UV 不一樣,它要去重,同一個(gè)用戶一天之內(nèi)的多次訪問(wèn)請(qǐng)求只能計(jì)數(shù)一次。這就 要求每一個(gè)網(wǎng)頁(yè)請(qǐng)求都需要帶上用戶的 ID,無(wú)論是登錄用戶還是未登錄用戶都需要一個(gè)唯一 ID 來(lái)標(biāo)識(shí)。
你也許已經(jīng)想到了一個(gè)簡(jiǎn)單的方案,那就是為每一個(gè)頁(yè)面一個(gè)獨(dú)立的 set 集合來(lái)存儲(chǔ)所 有當(dāng)天訪問(wèn)過(guò)此頁(yè)面的用戶 ID。當(dāng)一個(gè)請(qǐng)求過(guò)來(lái)時(shí),我們使用 sadd 將用戶 ID 塞進(jìn)去就可 以了。通過(guò) scard 可以取出這個(gè)集合的大小,這個(gè)數(shù)字就是這個(gè)頁(yè)面的 UV 數(shù)據(jù)。沒(méi)錯(cuò),這是一個(gè)非常簡(jiǎn)單的方案。
但是,如果你的頁(yè)面訪問(wèn)量非常大,比如一個(gè)爆款頁(yè)面幾千萬(wàn)的 UV,你需要一個(gè)很大 的 set 集合來(lái)統(tǒng)計(jì),這就非常浪費(fèi)空間。如果這樣的頁(yè)面很多,那所需要的存儲(chǔ)空間是驚人 的。為這樣一個(gè)去重功能就耗費(fèi)這樣多的存儲(chǔ)空間,值得么?其實(shí)老板需要的數(shù)據(jù)又不需要 太精確,105w 和 106w 這兩個(gè)數(shù)字對(duì)于老板們來(lái)說(shuō)并沒(méi)有多大區(qū)別,So,有沒(méi)有更好的解 決方案呢?
Redis 提供了 HyperLogLog 數(shù)據(jù)結(jié)構(gòu)就是用來(lái)解決 這種統(tǒng)計(jì)問(wèn)題的。HyperLogLog 提供不精確的去重計(jì)數(shù)方案,雖然不精確但是也不是非常不精確,標(biāo)準(zhǔn)誤差是 0.81%,這樣的精確度已經(jīng)可以滿足上面的 UV 統(tǒng)計(jì)需求了。
對(duì)于上面的場(chǎng)景,同學(xué)們可能有疑問(wèn),我或許同樣可以使用 HashMap、BitMap 和 HyperLogLog 來(lái)解決。對(duì)于這三種解決方案,這邊做下對(duì)比:
HashMap:算法簡(jiǎn)單,統(tǒng)計(jì)精度高,對(duì)于少量數(shù)據(jù)建議使用,但是對(duì)于大量的數(shù)據(jù)會(huì)占用很大內(nèi)存空間;
BitMap:位圖算法,具體內(nèi)容可以參考我的這篇文章,統(tǒng)計(jì)精度高,雖然內(nèi)存占用要比 HashMap 少,但是對(duì)于大量數(shù)據(jù)還是會(huì)占用較大內(nèi)存;
HyperLogLog:存在一定誤差,占用內(nèi)存少,穩(wěn)定占用 12k 左右內(nèi)存,可以統(tǒng)計(jì) 2^64 個(gè)元素,對(duì)于上面舉例的應(yīng)用場(chǎng)景,建議使用。
發(fā)布訂閱
Redis 提供了基于“發(fā)布/訂閱”模式的消息機(jī)制,此種模式下,消息發(fā)布者和訂閱者不進(jìn)行直接通信,發(fā)布者客戶端向指定的頻道(channel)發(fā)布消 息,訂閱該頻道的每個(gè)客戶端都可以收到該消息:
主要對(duì)應(yīng)的 Redis 命令為:
subscribe?channel?[channel?...]?#?訂閱一個(gè)或多個(gè)頻道 unsubscribe?channel?#?退訂指定頻道 publish?channel?message?#?發(fā)送消息 psubscribe?pattern?#?訂閱指定模式 punsubscribe?pattern?#?退訂指定模式使用案例:
打開(kāi)一個(gè) Redis 客戶端,如向 TestChanne 說(shuō)一聲 hello:
127.0.0.1:6379>?publish?TestChanne?hello (integer)?1?#?返回的是接收這條消息的訂閱者數(shù)量這樣消息就發(fā)出去了。發(fā)出去的消息不會(huì)被持久化,也就是有客戶端訂閱 TestChanne 后只能接收到后續(xù)發(fā)布到該頻道的消息,之前的就接收不到了。
打開(kāi)另一 Redis 個(gè)客戶端,這里假設(shè)發(fā)送消息之前就打開(kāi)并且訂閱了 TestChanne 頻道:
127.0.0.1:6379>?subscribe?TestChanne?#?執(zhí)行上面命令客戶端會(huì)進(jìn)入訂閱狀態(tài) Reading?messages...?(press?Ctrl-C?to?quit) 1)?"subscribe"?//?消息類型 2)?"TestChanne"?//?頻道 3)?"hello"?//?消息內(nèi)容我們可以利用 Redis 發(fā)布訂閱功能,實(shí)現(xiàn)的簡(jiǎn)單 MQ 功能,實(shí)現(xiàn)上下游的解耦。不過(guò)需要注意了,由于 Redis 發(fā)布的消息不會(huì)被持久化,這就會(huì)導(dǎo)致新訂閱的客戶端將不會(huì)收到歷史消息。所以,如果當(dāng)前的業(yè)務(wù)場(chǎng)景不能容忍這些缺點(diǎn),那還是用專業(yè) MQ 吧。
GEO
Redis3.2 版本提供了 GEO(地理信息定位)功能,支持存儲(chǔ)地理位置信 息用來(lái)實(shí)現(xiàn)諸如附近位置、搖一搖這類依賴于地理位置信息的功能,對(duì)于需 要實(shí)現(xiàn)這些功能的開(kāi)發(fā)者來(lái)說(shuō)是一大福音。GEO 功能是 Redis 的另一位作者 Matt Stancliff 借鑒 NoSQL 數(shù)據(jù)庫(kù) Ardb 實(shí)現(xiàn)的,Ardb 的作者來(lái)自中國(guó),它提供了優(yōu)秀的 GEO 功能。
Redis GEO 相關(guān)的命令如下:
#?添加一個(gè)空間元素,longitude、latitude、member分別是該地理位置的經(jīng)度、緯度、成員 #?這里的成員就是指代具體的業(yè)務(wù)數(shù)據(jù),比如說(shuō)用戶的ID等 #?需要注意的是Redis的緯度有效范圍不是[-90,90]而是[-85,85] #?如果在添加一個(gè)空間元素時(shí),這個(gè)元素中的menber已經(jīng)存在key中,那么GEOADD命令會(huì)返回0,相當(dāng)于更新了這個(gè)menber的位置信息 GEOADD?key?longitude?latitude?member?[longitude?latitude?member] #?用于添加城市的坐標(biāo)信息 geoadd?cities:locations?117.12?39.08?tianjin?114.29?38.02?shijiazhuang?118.01?39.38?tangshan?115.29?38.51?baoding#?獲取地理位置信息 geopos?key?member?[member?...] #?獲取天津的坐標(biāo) geopos?cities:locations?tianjin#?獲取兩個(gè)坐標(biāo)之間的距離 #?unit代表單位,有4個(gè)單位值-?m?(meter)?代表米-?km?(kilometer)代表千米-?mi?(miles)代表英里-?ft?(ft)代表尺 geodist?key?member1?member2?[unit] #?獲取天津和保定之間的距離 GEODIST?cities:locations?tianjin?baoding?km#?獲取指定位置范圍內(nèi)的地理信息位置集合,此命令可以用于實(shí)現(xiàn)附近的人的功能 # georadius和georadiusbymember兩個(gè)命令的作用是一樣的,都是以一個(gè)地理位置為中心算出指定半徑內(nèi)的其他地理信息位置,不同的是georadius命令的中心位置給出了具體的經(jīng)緯度,georadiusbymember只需給出成員即可。其中radiusm|km|ft|mi是必需參數(shù),指定了半徑(帶單位),這兩個(gè)命令有很多可選參數(shù),參數(shù)含義如下: #?- withcoord:返回結(jié)果中包含經(jīng)緯度。 #?- withdist:返回結(jié)果中包含離中心節(jié)點(diǎn)位置的距離。 #?- withhash:返回結(jié)果中包含geohash,有關(guān)geohash后面介紹。 #?- COUNT count:指定返回結(jié)果的數(shù)量。 #?- asc|desc:返回結(jié)果按照離中心節(jié)點(diǎn)的距離做升序或者降序。 #?- store key:將返回結(jié)果的地理位置信息保存到指定鍵。 #?- storedist key:將返回結(jié)果離中心節(jié)點(diǎn)的距離保存到指定鍵。 georadius?key?longitude?latitude?radiusm|km|ft|mi?[withcoord]?[withdist]?[withhash]?[COUNT?count]?[asc|desc]?[store?key]?[storedist?key]georadiusbymember?key?member?radiusm|km|ft|mi?[withcoord]?[withdist]?[withhash]?[COUNT?count]?[asc|desc]?[store?key]?[storedist?key]#?獲取geo?hash # Redis使用geohash將二維經(jīng)緯度轉(zhuǎn)換為一維字符串,geohash有如下特點(diǎn): #?- GEO的數(shù)據(jù)類型為zset,Redis將所有地理位置信息的geohash存放在zset中。 #?-?字符串越長(zhǎng),表示的位置更精確,表3-8給出了字符串長(zhǎng)度對(duì)應(yīng)的精度,例如geohash長(zhǎng)度為9時(shí),精度在2米左右。長(zhǎng)度和精度的對(duì)應(yīng)關(guān)系,請(qǐng)參考:https://easyreadfs.nosdn.127.net/9F42_CKRFsfc8SUALbHKog==/8796093023252281390 #?-?兩個(gè)字符串越相似,它們之間的距離越近,Redis利用字符串前綴匹配算法實(shí)現(xiàn)相關(guān)的命令。 #?- geohash編碼和經(jīng)緯度是可以相互轉(zhuǎn)換的。 #?- Redis正是使用有序集合并結(jié)合geohash的特性實(shí)現(xiàn)了GEO的若干命令。 geohash?key?member?[member?...]#?刪除操作,GEO沒(méi)有提供刪除成員的命令,但是因?yàn)镚EO的底層實(shí)現(xiàn)是zset,所以可以借用zrem命令實(shí)現(xiàn)對(duì)地理位置信息的刪除。 zrem?key?member使用案例,例如咋部門(mén)是做直播的,那直播業(yè)務(wù)一般會(huì)有一個(gè)“附近的直播”功能,這里就可以考慮用 Redis 的 GEO 技術(shù)來(lái)完成這個(gè)功能。
數(shù)據(jù)操作主要有兩個(gè):一是主播開(kāi)播的時(shí)候?qū)懭胫鞑?Id 的經(jīng)緯度,二是主播關(guān)播的時(shí)候刪除主播 Id 元素。這樣就維護(hù)了一個(gè)具有位置信息的在線主播集合提供給線上檢索。
大家具體使用的時(shí)候,可以去了解一下 Redis GEO 原理,主要用到了空間索引的算法 GEOHASH 的相關(guān)知識(shí),針對(duì)索引我們?nèi)粘K?jiàn)都是一維的字符,那么如何對(duì)三維空間里面的坐標(biāo)點(diǎn)建立索引呢,直接點(diǎn)就是三維變二維,二維變一維。這里就不再詳細(xì)闡述了。
Redis 客戶端
主流編程語(yǔ)言都有對(duì)應(yīng)的常用 Redis 客戶端,例如:
java -> Jedis
python -> redis-py
node -> ioredis
具體使用語(yǔ)法,大家可以根據(jù)自己的需要查找對(duì)應(yīng)的官方文檔:
Jedis 文檔:https://github.com/redis/jedis
redis-py 文檔:https://github.com/redis/redis-py
ioredis 文檔:https://github.com/luin/ioredis
持久化、主從同步與緩存設(shè)計(jì)
持久化
Redis 支持 RDB 和 AOF 兩種持久化機(jī)制,持久化功能有效地避免因進(jìn)程 退出造成的數(shù)據(jù)丟失問(wèn)題,當(dāng)下次重啟時(shí)利用之前持久化的文件即可實(shí)現(xiàn)數(shù)據(jù)恢復(fù)。
RDB 是一次全量備份,AOF 日志是連續(xù)的增量備份, RDB 是內(nèi)存數(shù)據(jù)的二進(jìn)制序列化形式,在存儲(chǔ)上非常緊湊,而 AOF 日志記錄的是內(nèi)存數(shù)據(jù)修改的指令記錄文本。
AOF 以獨(dú)立日志的方式記錄每次寫(xiě)命令, 重啟時(shí)再重新執(zhí)行 AOF 文件中的命令達(dá)到恢復(fù)數(shù)據(jù)的目的。AOF 的主要作用 是解決了數(shù)據(jù)持久化的實(shí)時(shí)性,目前已經(jīng)是 Redis 持久化的主流方式。
AOF 日志在長(zhǎng)期的運(yùn)行過(guò)程中會(huì)變的無(wú)比龐大,數(shù)據(jù)庫(kù)重啟時(shí)需要加載 AOF 日志進(jìn)行指令重放,這個(gè)時(shí)間就會(huì)無(wú)比漫長(zhǎng)。所以需要定期進(jìn)行 AOF 重寫(xiě),給 AOF 日志進(jìn)行瘦身。
RDB
我們知道 Redis 是單線程程序,這個(gè)線程要同時(shí)負(fù)責(zé)多個(gè)客戶端套接字的并發(fā)讀寫(xiě)操作和內(nèi)存數(shù)據(jù)結(jié)構(gòu)的邏輯讀寫(xiě)。
在服務(wù)線上請(qǐng)求的同時(shí),Redis 還需要進(jìn)行內(nèi)存 RDB,內(nèi)存 RDB 要求 Redis 必須進(jìn)行文件 IO 操作,可文件 IO 操作是不能使用多路復(fù)用 API。這意味著單線程同時(shí)在服務(wù)線上的請(qǐng)求還要進(jìn)行文件 IO 操作,文件 IO 操作會(huì)嚴(yán)重拖垮服務(wù)器請(qǐng)求的性能。還有個(gè)重要的問(wèn)題是為了不阻塞線上的業(yè)務(wù),就需要邊持久化邊響應(yīng)客戶端請(qǐng)求。持久化的同時(shí),內(nèi)存數(shù)據(jù)結(jié)構(gòu)還在改變,比如一個(gè)大型的 hash 字典正在持久化,結(jié)果一個(gè)請(qǐng)求過(guò)來(lái)把它給刪掉了,還沒(méi)持久化完呢,這可怎么辦?
那該怎么辦呢? Redis 使用操作系統(tǒng)的多進(jìn)程 COW(Copy On Write) 機(jī)制來(lái)實(shí)現(xiàn) RDB 持久化,以下為 RDB 備份流程:
執(zhí)行 bgsave 命令,Redis 父進(jìn)程判斷當(dāng)前是否存在正在執(zhí)行的子進(jìn) 程,如 RDB/AOF 子進(jìn)程,如果存在 bgsave 命令直接返回。
父進(jìn)程執(zhí)行 fork 操作創(chuàng)建子進(jìn)程,fork 操作過(guò)程中父進(jìn)程會(huì)阻塞,通 過(guò) info stats 命令查看 latest_fork_usec 選項(xiàng),可以獲取最近一個(gè) fork 操作的耗 時(shí),單位為微秒。
父進(jìn)程 fork 完成后,bgsave 命令返回 “Background saving started” 信息 并不再阻塞父進(jìn)程,可以繼續(xù)響應(yīng)其他命令。
子進(jìn)程創(chuàng)建 RDB 文件,根據(jù)父進(jìn)程內(nèi)存生成臨時(shí)快照文件,完成后 對(duì)原有文件進(jìn)行原子替換。執(zhí)行 lastsave 命令可以獲取最后一次生成 RDB 的 時(shí)間,對(duì)應(yīng) info 統(tǒng)計(jì)的 rdb_last_save_time 選項(xiàng)。
進(jìn)程發(fā)送信號(hào)給父進(jìn)程表示完成,父進(jìn)程更新統(tǒng)計(jì)信息,具體見(jiàn) info Persistence 下的 rdb_* 相關(guān)選項(xiàng)。
AOF
AOF 日志存儲(chǔ)的是 Redis 服務(wù)器的順序指令序列,AOF 日志只記錄對(duì)內(nèi)存進(jìn)行修改的 指令記錄。
假設(shè) AOF 日志記錄了自 Redis 實(shí)例創(chuàng)建以來(lái)所有的修改性指令序列,那么就可以通過(guò) 對(duì)一個(gè)空的 Redis 實(shí)例順序執(zhí)行所有的指令,也就是「重放」,來(lái)恢復(fù) Redis 當(dāng)前實(shí)例的內(nèi) 存數(shù)據(jù)結(jié)構(gòu)的狀態(tài)。
Redis 會(huì)在收到客戶端修改指令后,先進(jìn)行參數(shù)校驗(yàn),如果沒(méi)問(wèn)題,就立即將該指令文本存儲(chǔ)到 AOF 日志中,也就是先存到磁盤(pán),然后再執(zhí)行指令。這樣即使遇到突發(fā)宕機(jī),已經(jīng)存儲(chǔ)到 AOF 日志的指令進(jìn)行重放一下就可以恢復(fù)到宕機(jī)前的狀態(tài)。通過(guò) appendfsync 參數(shù)可以控制實(shí)時(shí)/秒級(jí)持久化 。
AOF 流程:
所有的寫(xiě)入命令會(huì)追加到 aof_buf(緩沖區(qū))中。
AOF 緩沖區(qū)根據(jù)對(duì)應(yīng)的策略向硬盤(pán)做同步操作。
隨著 AOF 文件越來(lái)越大,需要定期對(duì) AOF 文件進(jìn)行重寫(xiě),達(dá)到壓縮的目的。
當(dāng) Redis 服務(wù)器重啟時(shí),可以加載 AOF 文件進(jìn)行數(shù)據(jù)恢復(fù)。
Redis 在長(zhǎng)期運(yùn)行的過(guò)程中,AOF 的日志會(huì)越變?cè)介L(zhǎng)。如果實(shí)例宕機(jī)重啟,重放整個(gè) AOF 日志會(huì)非常耗時(shí),導(dǎo)致長(zhǎng)時(shí)間 Redis 無(wú)法對(duì)外提供服務(wù)。所以需要對(duì) AOF 日志瘦身。
Redis 提供了 bgrewriteaof 指令用于對(duì) AOF 日志進(jìn)行瘦身。其原理就是開(kāi)辟一個(gè)子進(jìn)程對(duì)內(nèi)存進(jìn)行遍歷轉(zhuǎn)換成一系列 Redis 的操作指令,序列化到一個(gè)新的 AOF 日志文件中。序列化完畢后再將操作期間發(fā)生的增量 AOF 日志追加到這個(gè)新的 AOF 日志文件中,追加完畢后就立即替代舊的 AOF 日志文件了,瘦身工作就完成了。
AOF 瘦身重寫(xiě)流程:
AOF 重寫(xiě)可以通過(guò) auto-aof-rewrite-min-siz e 和 auto-aof-rewrite- percentage 參數(shù)控制自動(dòng)觸發(fā),也可以使用 bgrewriteaof 命令手動(dòng)觸發(fā)。
子進(jìn)程執(zhí)行期間使用 copy-on-write 機(jī)制與父進(jìn)程共享內(nèi)存,避免內(nèi) 存消耗翻倍。AOF 重寫(xiě)期間還需要維護(hù)重寫(xiě)緩沖區(qū),保存新的寫(xiě)入命令避免 數(shù)據(jù)丟失。
單機(jī)下部署多個(gè)實(shí)例時(shí),為了防止出現(xiàn)多個(gè)子進(jìn)程執(zhí)行重寫(xiě)操作, 建議做隔離控制,避免 CPU 和 IO 資源競(jìng)爭(zhēng)。
Redis 4.0 混合持久化
重啟 Redis 時(shí),我們很少使用 RDB 來(lái)恢復(fù)內(nèi)存狀態(tài),因?yàn)闀?huì)丟失大量數(shù)據(jù)。我們通常 使用 AOF 日志重放,但是重放 AOF 日志性能相對(duì) rdb 來(lái)說(shuō)要慢很多,這樣在 Redis 實(shí) 例很大的情況下,啟動(dòng)需要花費(fèi)很長(zhǎng)的時(shí)間。
Redis 4.0 為了解決這個(gè)問(wèn)題,帶來(lái)了一個(gè)新的持久化選項(xiàng)——混合持久化。將 RDB 文 件的內(nèi)容和增量的 AOF 日志文件存在一起。這里的 AOF 日志不再是全量的日志,而是自 持久化開(kāi)始到持久化結(jié)束的這段時(shí)間發(fā)生的增量 AOF 日志,通常這部分 AOF 日志很小。
于是在 Redis 重啟的時(shí)候,可以先加載 RDB 的內(nèi)容,然后再重放增量 AOF 日志就可 以完全替代之前的 AOF 全量文件重放,重啟效率因此大幅得到提升。
主從同步—簡(jiǎn)單了解
很多企業(yè)都沒(méi)有使用到 Redis 的集群,但是至少都做了主從。有了主從,當(dāng) master 掛 掉的時(shí)候,運(yùn)維讓從庫(kù)過(guò)來(lái)接管,服務(wù)就可以繼續(xù),否則 master 需要經(jīng)過(guò)數(shù)據(jù)恢復(fù)和重啟的過(guò)程,這就可能會(huì)拖很長(zhǎng)的時(shí)間,影響線上業(yè)務(wù)的持續(xù)服務(wù)。
Redis 通過(guò)主從同步功能實(shí)現(xiàn)主節(jié)點(diǎn)的多個(gè)副本。從節(jié)點(diǎn)可靈活地通過(guò) slaveof 命令建立或斷開(kāi)同步流程。同步復(fù)制分為:全量復(fù)制和部分增量復(fù)制主從節(jié)點(diǎn)之間維護(hù)心跳和偏移量檢查機(jī)制,保證主從節(jié)點(diǎn)通信正常和數(shù)據(jù)一致。
Redis 為了保證高性能復(fù)制過(guò)程是異步的,寫(xiě)命令處理完后直接返回給客戶端,不等待從節(jié)點(diǎn)復(fù)制完成。因此從節(jié)點(diǎn)數(shù)據(jù)集會(huì)有延遲情況。即當(dāng)使用從節(jié)點(diǎn)用于讀寫(xiě)分離時(shí)會(huì)存在數(shù)據(jù)延遲、過(guò)期數(shù)據(jù)、從節(jié)點(diǎn)可用性等問(wèn)題,需要根據(jù)自身業(yè)務(wù)提前作出規(guī)避。
注意:在運(yùn)維過(guò)程中,主節(jié)點(diǎn)存在多個(gè)從節(jié)點(diǎn)或者一臺(tái)機(jī)器上部署大量主節(jié)點(diǎn)的情況下,會(huì)有復(fù)制風(fēng)暴的風(fēng)險(xiǎn)。
Redis Sentinel(哨兵)
主從復(fù)制是 Redis 分布式的基礎(chǔ),Redis 的高可用離開(kāi)了主從復(fù)制將無(wú)從進(jìn)行。后面的我們會(huì)講到 Redis 的集群模式,集群模式都依賴于本節(jié)所講的主從復(fù)制。
不過(guò)復(fù)制功能也不是必須的,如果你將 Redis 只用來(lái)做緩存,也就無(wú)需要從庫(kù)做備份,掛掉了重新啟動(dòng)一下就行。但是只要你使用了 Redis 的持久化 功能,就必須認(rèn)真對(duì)待主從復(fù)制,它是系統(tǒng)數(shù)據(jù)安全的基礎(chǔ)保障。
舉例:如果主節(jié)點(diǎn)凌晨 3 點(diǎn)突發(fā)宕機(jī)怎么辦?就坐等運(yùn)維從床上爬起來(lái),然后手工進(jìn)行從主切換,再通知所有的程 序把地址統(tǒng)統(tǒng)改一遍重新上線么?毫無(wú)疑問(wèn),這樣的人工運(yùn)維效率太低,事故發(fā)生時(shí)估計(jì)得 至少 1 個(gè)小時(shí)才能緩過(guò)來(lái)。
Sentinel 負(fù)責(zé)持續(xù)監(jiān)控主從節(jié)點(diǎn)的健康,當(dāng)主節(jié)點(diǎn)掛掉時(shí),自動(dòng)選擇一個(gè)最優(yōu)的從節(jié)點(diǎn)切換為主節(jié)點(diǎn)。客戶端來(lái)連接集群時(shí),會(huì)首先連接 sentinel,通過(guò) sentinel 來(lái)查詢主節(jié)點(diǎn)的地址, 然后再去連接主節(jié)點(diǎn)進(jìn)行數(shù)據(jù)交互。當(dāng)主節(jié)點(diǎn)發(fā)生故障時(shí),客戶端會(huì)重新向 sentinel 要地址,sentinel 會(huì)將最新的主節(jié)點(diǎn)地址告訴客戶端。如此應(yīng)用程序?qū)o(wú)需重啟即可自動(dòng)完成節(jié)點(diǎn)切換。如圖:
消息丟失
Redis 主從采用異步復(fù)制,意味著當(dāng)主節(jié)點(diǎn)掛掉時(shí),從節(jié)點(diǎn)可能沒(méi)有收到全部的同步消息,這部分未同步的消息就丟失了。如果主從延遲特別大,那么丟失的數(shù)據(jù)就可能會(huì)特別 多。Sentinel 無(wú)法保證消息完全不丟失,但是也盡可能保證消息少丟失。它有兩個(gè)選項(xiàng)可以 限制主從延遲過(guò)大:
min-slaves-to-write 1
min-slaves-max-lag 10
第一個(gè)參數(shù)表示主節(jié)點(diǎn)必須至少有一個(gè)從節(jié)點(diǎn)在進(jìn)行正常復(fù)制,否則就停止對(duì)外寫(xiě)服務(wù),喪失可用性。
何為正常復(fù)制,何為異常復(fù)制?這個(gè)就是由第二個(gè)參數(shù)控制的,它的單位是秒,表示如果 10s 沒(méi)有收到從節(jié)點(diǎn)的反饋,就意味著從節(jié)點(diǎn)同步不正常,要么網(wǎng)絡(luò)斷開(kāi)了,要么一直沒(méi)有給反饋。
Redis 最終一致
Redis 的主從數(shù)據(jù)是異步同步的,所以分布式的 Redis 系統(tǒng)并不滿足「一致性」要求。當(dāng)客戶端在 Redis 的主節(jié)點(diǎn)修改了數(shù)據(jù)后,立即返回,即使在主從網(wǎng)絡(luò)斷開(kāi)的情況下,主節(jié) 點(diǎn)依舊可以正常對(duì)外提供修改服務(wù),所以 Redis 滿足「可用性」。
Redis 保證「最終一致性」,從節(jié)點(diǎn)會(huì)努力追趕主節(jié)點(diǎn),最終從節(jié)點(diǎn)的狀態(tài)會(huì)和主節(jié)點(diǎn) 的狀態(tài)將保持一致。如果網(wǎng)絡(luò)斷開(kāi)了,主從節(jié)點(diǎn)的數(shù)據(jù)將會(huì)出現(xiàn)大量不一致,一旦網(wǎng)絡(luò)恢 復(fù),從節(jié)點(diǎn)會(huì)采用多種策略努力追趕上落后的數(shù)據(jù),繼續(xù)盡力保持和主節(jié)點(diǎn)一致。
緩存
緩存的收益與成本
收益:
加速讀寫(xiě):CPU L1/L2/L3 Cache、瀏覽器緩存等。因?yàn)榫彺嫱ǔ6际侨珒?nèi)存的(例如 Redis、Memcache),而 存儲(chǔ)層通常讀寫(xiě)性能不夠強(qiáng)悍(例如 MySQL),通過(guò)緩存的使用可以有效 地加速讀寫(xiě),優(yōu)化用戶體驗(yàn)。
降低后端負(fù)載:幫助后端減少訪問(wèn)量和復(fù)雜計(jì)算,在很大程度降低了后端的負(fù)載。成本:
數(shù)據(jù)不一致:緩存層和數(shù)據(jù)層有時(shí)間窗口不一致,和更新策略有關(guān)。
代碼維護(hù)成本:加入緩存后,需要同時(shí)處理緩存層和存儲(chǔ)層的邏輯, 增大了開(kāi)發(fā)者維護(hù)代碼的成本。
運(yùn)維成本:以 Redis Cluster 為例,加入后無(wú)形中增加了運(yùn)維成本。使用場(chǎng)景:
降低后端負(fù)載:對(duì)高消耗的 SQL:join 結(jié)果集/分組統(tǒng)計(jì)結(jié)果緩存。
加速請(qǐng)求響應(yīng):利用 Redis/Memcache 優(yōu)化 IO 響應(yīng)時(shí)間。
大量寫(xiě)合并為批量寫(xiě):比如計(jì)數(shù)器先 Redis 累加再批量寫(xiě)入 DB。
緩存更新策略—算法剔除
LRU:Least Recently Used,最近最少使用。
LFU:Least Frequently Used,最不經(jīng)常使用。
FIFO:First In First Out,先進(jìn)先出。
使用場(chǎng)景:剔除算法通常用于緩存使用量超過(guò)了預(yù)設(shè)的最大值時(shí)候,如何對(duì)現(xiàn)有的數(shù)據(jù)進(jìn)行剔除。例如 Redis 使用 maxmemory-policy 這個(gè)配置作為內(nèi)存最大值后對(duì)于數(shù)據(jù)的剔除策略。
一致性:要清理哪些數(shù)據(jù)是由具體算法決定,開(kāi)發(fā)人員只能決定使用哪種算法,所以數(shù)據(jù)的一致性是最差的。
維護(hù)成本:算法不需要開(kāi)發(fā)人員自己來(lái)實(shí)現(xiàn),通常只需要配置最大 maxmemory 和對(duì)應(yīng)的策略即可。
緩存更新策略—超時(shí)剔除
使用場(chǎng)景:超時(shí)剔除通過(guò)給緩存數(shù)據(jù)設(shè)置過(guò)期時(shí)間,讓其在過(guò)期時(shí)間后自動(dòng)刪除,例如 Redis 提供的 expire 命令。如果業(yè)務(wù)可以容忍一段時(shí)間內(nèi),緩存層數(shù)據(jù)和存儲(chǔ)層數(shù)據(jù)不一致,那么可以為其設(shè)置過(guò)期時(shí)間。在數(shù)據(jù)過(guò)期后,再?gòu)恼鎸?shí)數(shù)據(jù)源獲取數(shù)據(jù),重新放到緩存并設(shè)置過(guò)期時(shí)間。
一致性:一段時(shí)間窗口內(nèi)(取決于過(guò)期時(shí)間長(zhǎng)短)存在一致性問(wèn)題,即緩存數(shù)據(jù)和真實(shí)數(shù)據(jù)源的數(shù)據(jù)不一致。
維護(hù)成本:維護(hù)成本不是很高,只需設(shè)置 expire 過(guò)期時(shí)間即可,當(dāng)然前提是應(yīng)用方允許這段時(shí)間可能發(fā)生的數(shù)據(jù)不一致。
緩存更新策略—主動(dòng)更新
使用場(chǎng)景:應(yīng)用方對(duì)于數(shù)據(jù)的一致性要求高,需要在真實(shí)數(shù)據(jù)更新后, 立即更新緩存數(shù)據(jù)。例如可以利用消息系統(tǒng)或者其他方式通知緩存更新。
一致性:一致性最高,但如果主動(dòng)更新發(fā)生了問(wèn)題,那么這條數(shù)據(jù)很可能很長(zhǎng)時(shí)間不會(huì)更新,所以建議結(jié)合超時(shí)剔除一起使用效果會(huì)更好。
維護(hù)成本:維護(hù)成本會(huì)比較高,開(kāi)發(fā)者需要自己來(lái)完成更新,并保證更新操作的正確性。
緩存更新策略—總結(jié)
低一致性業(yè)務(wù):建議配置最大內(nèi)存和淘汰策略的方式使用。
高一致性業(yè)務(wù):可以結(jié)合使用超時(shí)剔除和主動(dòng)更新,這樣即使主動(dòng)更新出了問(wèn)題,也能保證數(shù)據(jù)過(guò)期時(shí)間后刪除臟數(shù)據(jù)。
緩存可能會(huì)遇到的問(wèn)題
緩存穿透:指查詢一個(gè)一定不存在的數(shù)據(jù),由于緩存是不命中時(shí)被動(dòng)寫(xiě)的,并且出于容錯(cuò)考慮,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫(xiě)入緩存,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到存儲(chǔ)層去查詢,失去了緩存的意義。在流量大時(shí),可能 DB 就掛掉了,要是有人利用不存在的 key 頻繁攻擊我們的應(yīng)用,這就是漏洞。解決方法:
布隆過(guò)濾器,將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的 bitmap 中,一個(gè)一定不存在的數(shù)據(jù)會(huì)被 這個(gè) bitmap 攔截掉,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的查詢壓力。
另外也有一個(gè)更為簡(jiǎn)單粗暴的方法(我們采用的就是這種),如果一個(gè)查詢返回的數(shù)據(jù)為空(不管是數(shù) 據(jù)不存在,還是系統(tǒng)故障),我們?nèi)匀话堰@個(gè)空結(jié)果進(jìn)行緩存,但它的過(guò)期時(shí)間會(huì)很短,最長(zhǎng)不超過(guò)五分鐘。
緩存雪崩:指在我們?cè)O(shè)置緩存時(shí)采用了相同的過(guò)期時(shí)間,導(dǎo)致緩存在某一時(shí)刻同時(shí)失效,請(qǐng)求全部轉(zhuǎn)發(fā)到 DB,DB 瞬時(shí)壓力過(guò)重雪崩。解決方法:我們可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值,比如 1-5 分鐘隨機(jī),這樣每一個(gè)緩存的過(guò)期時(shí)間的重復(fù)率就會(huì)降低,就很難引發(fā)集體失效的事件。
緩存擊穿:對(duì)于一些設(shè)置了過(guò)期時(shí)間的 key,如果這些 key 可能會(huì)在某些時(shí)間點(diǎn)被超高并發(fā)地訪問(wèn),是一種非常“熱點(diǎn)”的數(shù)據(jù)。這個(gè)時(shí)候,需要考慮一個(gè)問(wèn)題:緩存被“擊穿”的問(wèn)題,這個(gè)和緩存雪崩的區(qū)別在于這里針對(duì)某一 key 緩存,前者則是很多 key。緩存在某個(gè)時(shí)間點(diǎn)過(guò)期的時(shí)候,恰好在這個(gè)時(shí)間點(diǎn)對(duì)這個(gè) Key 有大量的并發(fā)請(qǐng)求過(guò)來(lái),這些請(qǐng)求發(fā)現(xiàn)緩存過(guò)期一般都會(huì)從后端 DB 加載數(shù)據(jù)并回設(shè)到緩存,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端 DB 壓垮。解決方法:互斥鎖、永遠(yuǎn)不過(guò)期設(shè)置、資源保護(hù)等等。
緩存無(wú)底洞問(wèn)題:Facebook 的工作人員反應(yīng) 2010 年已達(dá)到 3000 個(gè) memcached 節(jié)點(diǎn),儲(chǔ)存數(shù)千 G 的緩存。他們發(fā)現(xiàn)一個(gè)問(wèn)題– memcached 的連接效率下降了,于是添加 memcached 節(jié)點(diǎn),添加完之后,并沒(méi)有好轉(zhuǎn)。稱為“無(wú)底洞”現(xiàn)象。原因:客戶端一次批量操作會(huì)涉及多次網(wǎng)絡(luò)操作,也就意味著批量操作會(huì)隨著實(shí)例的增多,耗時(shí)會(huì)不斷增大。服務(wù)端網(wǎng)絡(luò)連接次數(shù)變多,對(duì)實(shí)例的性能也有一定影響。即:更多的機(jī)器不代表更多的性能,所謂“無(wú)底洞”就是說(shuō)投入越多不一定產(chǎn)出越多。解決方案有:串行 mget、串行 IO、并行 IO、Hash tag 實(shí)現(xiàn)等,更多請(qǐng)看:緩存無(wú)底洞問(wèn)題(http://ifeve.com/redis-multiget-hole/)
知識(shí)拓展
緩存與數(shù)據(jù)庫(kù)同步策略(如何保證緩存(Redis)與數(shù)據(jù)庫(kù)(MySQL)的一致性?)
對(duì)于熱點(diǎn)數(shù)據(jù)(經(jīng)常被查詢,但不經(jīng)常被修改的數(shù)據(jù)),我們一般會(huì)將其放入 Redis 緩存中,以增加查詢效率,但需要保證從 Redis 中讀取的數(shù)據(jù)與數(shù)據(jù)庫(kù)中存儲(chǔ)的數(shù)據(jù)最終是一致的,這就是經(jīng)典的緩存與數(shù)據(jù)庫(kù)同步問(wèn)題。
那么,如何保證緩存(Redis)與數(shù)據(jù)庫(kù)(MySQL)的一致性呢?根據(jù)緩存是刪除還是更新,以及操作順序大概是可以分為下面四種情況:
先更新數(shù)據(jù)庫(kù),再更新緩存
先更新緩存,再更新數(shù)據(jù)庫(kù)
先刪除緩存,再更新數(shù)據(jù)庫(kù)
先更新數(shù)據(jù)庫(kù),再刪除緩存
刪除緩存對(duì)比更新緩存
刪除緩存: 數(shù)據(jù)只會(huì)寫(xiě)入數(shù)據(jù)庫(kù),不會(huì)寫(xiě)入緩存,只會(huì)刪除緩存
更新緩存: 數(shù)據(jù)不但寫(xiě)入數(shù)據(jù)庫(kù),還會(huì)寫(xiě)入緩存
刪除緩存
優(yōu)點(diǎn):操作簡(jiǎn)單,無(wú)論更新操作是否復(fù)雜,直接刪除,并且能防止更新出現(xiàn)的線程安全問(wèn)題
缺點(diǎn):刪除后,下一次查詢無(wú)法在 cache 中查到,會(huì)有一次 Cache Miss,這時(shí)需要重新讀取數(shù)據(jù)庫(kù),高并發(fā)下可能會(huì)出現(xiàn)上面說(shuō)的緩存問(wèn)題
更新緩存
優(yōu)點(diǎn):命中率高,直接更新緩存,不會(huì)有 Cache Miss 的情況
缺點(diǎn):更新緩存消耗較大,尤其在復(fù)雜的操作流程中
那到底是選擇更新緩存還是刪除緩存呢,主要取決于更新緩存的復(fù)雜度
更新緩存的代價(jià)很小,此時(shí)我們應(yīng)該更傾向于更新緩存,以保證更高的緩存命中率
更新緩存的代價(jià)很大,此時(shí)我們應(yīng)該更傾向于刪除緩存
例如:只是簡(jiǎn)單的更新一下用戶積分,只操作一個(gè)字段,那就可以采用更新緩存,還有類似秒殺下商品庫(kù)存數(shù)量這種并發(fā)下查詢頻繁的數(shù)據(jù),也可以使用更新緩存,不過(guò)也要注意線程安全的問(wèn)題,防止產(chǎn)生臟數(shù)據(jù)。但是當(dāng)更新操作的邏輯較復(fù)雜時(shí),需要涉及到其它數(shù)據(jù),如用戶購(gòu)買(mǎi)商品付款時(shí),需要考慮打折、優(yōu)惠券、紅包等多種因素,這樣需要緩存與數(shù)據(jù)庫(kù)進(jìn)行多次交互,將打折等信息傳入緩存,再與緩存中的其它值進(jìn)行計(jì)算才能得到最終結(jié)果,此時(shí)更新緩存的消耗要大于直接淘汰緩存。
所以還是要根據(jù)業(yè)務(wù)場(chǎng)景來(lái)進(jìn)行選擇,不過(guò)大部分場(chǎng)景下刪除緩存操作簡(jiǎn)單,并且?guī)?lái)的副作用只是增加了一次 Cache Miss,建議作為通用的處理方式。
先更新數(shù)據(jù)庫(kù),再更新緩存
這種方式就適合更新緩存的代價(jià)很小的數(shù)據(jù),例如上面說(shuō)的用戶積分,庫(kù)存數(shù)量這類數(shù)據(jù),同樣還是要注意線程安全的問(wèn)題。
線程安全角度
同時(shí)有請(qǐng)求 A 和請(qǐng)求 B 進(jìn)行更新操作,那么會(huì)出現(xiàn)
線程 A 更新了數(shù)據(jù)庫(kù)
線程 B 更新了數(shù)據(jù)庫(kù)
線程 B 更新了緩存
線程 A 更新了緩存
這就出現(xiàn)請(qǐng)求 A 更新緩存應(yīng)該比請(qǐng)求 B 更新緩存早才對(duì),但是因?yàn)榫W(wǎng)絡(luò)等原因,B 卻比 A 更早更新了緩存,這就導(dǎo)致了臟數(shù)據(jù)。
業(yè)務(wù)場(chǎng)景角度
有如下兩種不適合場(chǎng)景:
如果你是一個(gè)寫(xiě)數(shù)據(jù)庫(kù)場(chǎng)景比較多,而讀數(shù)據(jù)場(chǎng)景比較少的業(yè)務(wù)需求,采用這種方案就會(huì)導(dǎo)致,數(shù)據(jù)壓根還沒(méi)讀到,緩存就被頻繁的更新,浪費(fèi)性能
如果你寫(xiě)入數(shù)據(jù)庫(kù)的值,并不是直接寫(xiě)入緩存的,而是要經(jīng)過(guò)一系列復(fù)雜的計(jì)算再寫(xiě)入緩存。那么,每次寫(xiě)入數(shù)據(jù)庫(kù)后,都再次計(jì)算寫(xiě)入緩存的值,無(wú)疑是也浪費(fèi)性能的
先更新緩存,再更新數(shù)據(jù)庫(kù)
這種情況應(yīng)該是和第一種情況一樣會(huì)存在線程安全問(wèn)題的,但是這種情況是有人使用過(guò)的,根據(jù)書(shū)籍《淘寶技術(shù)這十年》里,多隆把商品詳情頁(yè)放入緩存,采取的正是先更新緩存,再將緩存中的數(shù)據(jù)異步更新到數(shù)據(jù)庫(kù)這種方式,有興趣了解的可以查看這篇博客: https://www.cnblogs.com/rjzheng/p/9240611.html
還有現(xiàn)在互聯(lián)網(wǎng)常見(jiàn)的點(diǎn)贊功能,也可以采用這種方式,有興趣了解的可以查看這篇文章: https://juejin.im/post/5bdc257e6fb9a049ba410098
先刪除緩存,再更新數(shù)據(jù)庫(kù)
簡(jiǎn)單的想一下,好像這種方式不錯(cuò),就算是第一步刪除緩存成功,第二步寫(xiě)數(shù)據(jù)庫(kù)失敗,則只會(huì)引發(fā)一次 Cache Miss,對(duì)數(shù)據(jù)沒(méi)有影響,其實(shí)仔細(xì)一想并發(fā)下也很容易導(dǎo)致了臟數(shù)據(jù),例如
請(qǐng)求 A 進(jìn)行寫(xiě)操作,刪除緩存
請(qǐng)求 B 查詢發(fā)現(xiàn)緩存不存在
請(qǐng)求 B 去數(shù)據(jù)庫(kù)查詢得到舊值
請(qǐng)求 B 將舊值寫(xiě)入緩存
請(qǐng)求 A 將新值寫(xiě)入數(shù)據(jù)庫(kù)
那怎么解決呢,先看第四種情況(先更新數(shù)據(jù)庫(kù),再刪除緩存),后面再統(tǒng)一說(shuō)第三種和第四種的解決方案。
先更新數(shù)據(jù)庫(kù),再刪除緩存
先說(shuō)一下,國(guó)外有人提出了一個(gè)緩存更新套路,名為 Cache-Aside Pattern:https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside
失效:應(yīng)用程序先從 cache 取數(shù)據(jù),沒(méi)有得到,則從數(shù)據(jù)庫(kù)中取數(shù)據(jù),成功后,放到緩存中
命中:應(yīng)用程序從 cache 中取數(shù)據(jù),渠道后返回
更新:先把數(shù)據(jù)存到數(shù)據(jù)庫(kù)中,成功后再讓緩存失效
更新操作就是先更新數(shù)據(jù)庫(kù),再刪除緩存;讀取操作先從緩存取數(shù)據(jù),沒(méi)有,則從數(shù)據(jù)庫(kù)中取數(shù)據(jù),成功后,放到緩存中;這是標(biāo)準(zhǔn)的設(shè)計(jì)方案,包括 Facebook 的論文 Scaling Memcache at Facebook:chrome-extension://ikhdkkncnoglghljlkmcimlnlhkeamad/pdf-viewer/web/viewer.html? file=https%3A%2F%2Fwww.usenix.org%2Fsystem%2Ffiles%2Fconference%2Fnsdi13%2Fnsdi13-final170_update.pdf 也使用了這個(gè)策略。
為什么他們都用這種方式呢,這種情況不存在并發(fā)問(wèn)題么?
答案是也存在,但是出現(xiàn)概率比第三種低,例如:
請(qǐng)求緩存剛好失效
請(qǐng)求 A 查詢數(shù)據(jù)庫(kù),得一個(gè)舊值
請(qǐng)求 B 將新值寫(xiě)入數(shù)據(jù)庫(kù)
請(qǐng)求 B 刪除緩存
請(qǐng)求 A 將查到的舊值寫(xiě)入緩存
這樣就出現(xiàn)臟數(shù)據(jù)了,然而,實(shí)際上出現(xiàn)的概率可能非常低,因?yàn)檫@個(gè)條件需要發(fā)生在讀緩存時(shí)緩存失效,而且并發(fā)著有一個(gè)寫(xiě)操作。而實(shí)際上數(shù)據(jù)庫(kù)的寫(xiě)操作會(huì)比讀操作慢得多,而且還要鎖表,而讀操作必需在寫(xiě)操作前進(jìn)入數(shù)據(jù)庫(kù)操作,而又要晚于寫(xiě)操作刪除緩存,所有的這些條件都具備的概率基本并不大,但是還是會(huì)有出現(xiàn)的概率。
并且假如第一步寫(xiě)數(shù)據(jù)庫(kù)成功,第二步刪除緩存失敗,這樣也導(dǎo)致臟數(shù)據(jù),請(qǐng)看解決方案。
方案三四臟數(shù)據(jù)解決方案
那怎么解決呢,可以采用延時(shí)雙刪策略(緩存雙淘汰法),可以將前面所造成的緩存臟數(shù)據(jù),再次刪除:
先刪除(淘汰)緩存
再寫(xiě)數(shù)據(jù)庫(kù)(這兩步和原來(lái)一樣)
休眠 1 秒,再次刪除(淘汰)緩存
或者是:
先寫(xiě)數(shù)據(jù)庫(kù)
再刪除(淘汰)緩存(這兩步和原來(lái)一樣)
休眠 1 秒,再次刪除(淘汰)緩存
這個(gè) 1 秒應(yīng)該看你的業(yè)務(wù)場(chǎng)景,應(yīng)該自行評(píng)估自己的項(xiàng)目的讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時(shí),然后寫(xiě)數(shù)據(jù)的休眠時(shí)間則在讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時(shí)基礎(chǔ)上,加幾百毫秒即可,這么做確保讀請(qǐng)求結(jié)束,寫(xiě)請(qǐng)求可以刪除讀請(qǐng)求造成的緩存臟數(shù)據(jù)。
如果你用了 MySql 的讀寫(xiě)分離架構(gòu)怎么辦?,例如:
請(qǐng)求 A 進(jìn)行寫(xiě)操作,刪除緩存
請(qǐng)求 A 將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)了,(或者是先更新數(shù)據(jù)庫(kù),后刪除緩存)
請(qǐng)求 B 查詢緩存發(fā)現(xiàn),緩存沒(méi)有值
請(qǐng)求 B 去從庫(kù)查詢,這時(shí),還沒(méi)有完成主從同步,因此查詢到的是舊值
請(qǐng)求 B 將舊值寫(xiě)入緩存
數(shù)據(jù)庫(kù)完成主從同步,從庫(kù)變?yōu)樾轮?/p>
這種情景,就是數(shù)據(jù)不一致的原因,還是采用延時(shí)雙刪策略(緩存雙淘汰法),只是,休眠時(shí)間修改為在主從同步的延時(shí)時(shí)間基礎(chǔ)上,加幾百毫秒
并且為了性能更快,可以把第二次刪除緩存可以做成異步的,這樣不會(huì)阻塞請(qǐng)求了,如果再嚴(yán)謹(jǐn)點(diǎn),防止第二次刪除緩存失敗,這個(gè)異步刪除緩存可以加上重試機(jī)制,失敗一直重試,直到成功。
這里給出兩種重試機(jī)制參考
方案一
更新數(shù)據(jù)庫(kù)數(shù)據(jù)
緩存因?yàn)榉N種問(wèn)題刪除失敗
將需要?jiǎng)h除的 key 發(fā)送至消息隊(duì)列
自己消費(fèi)消息,獲得需要?jiǎng)h除的 key
繼續(xù)重試刪除操作,直到成功
然而,該方案有一個(gè)缺點(diǎn),對(duì)業(yè)務(wù)線代碼造成大量的侵入,于是有了方案二,啟動(dòng)一個(gè)訂閱程序去訂閱數(shù)據(jù)庫(kù)的 Binlog,獲得需要操作的數(shù)據(jù)。在應(yīng)用程序中,另起一段程序,獲得這個(gè)訂閱程序傳來(lái)的信息,進(jìn)行刪除緩存操作
方案二:
更新數(shù)據(jù)庫(kù)數(shù)據(jù)
數(shù)據(jù)庫(kù)會(huì)將操作信息寫(xiě)入 binlog 日志當(dāng)中
訂閱程序提取出所需要的數(shù)據(jù)以及 key
另起一段非業(yè)務(wù)代碼,獲得該信息
嘗試刪除緩存操作,發(fā)現(xiàn)刪除失敗
將這些信息發(fā)送至消息隊(duì)列
重新從消息隊(duì)列中獲得該數(shù)據(jù),重試操作
上述的訂閱 Binlog 程序在 MySql 中有現(xiàn)成的中間件叫 Canal,可以完成訂閱 Binlog 日志的功能,另外,重試機(jī)制,這里采用的是消息隊(duì)列的方式。如果對(duì)一致性要求不是很高,直接在程序中另起一個(gè)線程,每隔一段時(shí)間去重試即可,這些大家可以靈活自由發(fā)揮,只是提供一個(gè)思路。
總結(jié): 大部分應(yīng)該使用的都是第三種或第四種方式,如果都是采用延時(shí)雙刪策略(緩存雙淘汰法),可能區(qū)別不會(huì)很大,不過(guò)第四種方式出現(xiàn)臟數(shù)據(jù)概率是更小點(diǎn),更多的話還是要結(jié)合自身業(yè)務(wù)場(chǎng)景使用,靈活變通。
分布式鎖
例如一個(gè)操作要修改用戶的狀態(tài),修改狀態(tài)需要先讀出用戶的狀態(tài),在內(nèi)存里進(jìn)行修 改,改完了再存回去。如果這樣的操作同時(shí)進(jìn)行了,就會(huì)出現(xiàn)并發(fā)問(wèn)題,因?yàn)樽x取和保存狀 態(tài)這兩個(gè)操作不是原子的。(Wiki 解釋:所謂原子操作是指不會(huì)被線程調(diào)度機(jī)制打斷的操作;這種操作一旦開(kāi)始,就一直運(yùn)行到結(jié)束,中間不會(huì)有任何 context switch 線程切換。)如圖:
這個(gè)時(shí)候就要使用到分布式鎖來(lái)限制程序的并發(fā)執(zhí)行。
分布式鎖本質(zhì)上要實(shí)現(xiàn)的目標(biāo)就是在 Redis 里面占一個(gè)“茅坑”,當(dāng)別的進(jìn)程也要來(lái)占 時(shí),發(fā)現(xiàn)已經(jīng)有人蹲在那里了,就只好放棄或者稍后再試。占坑一般是使用 setnx(set if not exists) 指令,只允許被一個(gè)客戶端占坑。先來(lái)先占, 用 完了,再調(diào)用 del 指令釋放茅坑。
setnx?lock:codehole?true OK...?do?something?critical?... del?lock:codehole (integer)?1但是有個(gè)問(wèn)題,如果邏輯執(zhí)行到中間出現(xiàn)異常了,可能會(huì)導(dǎo)致 del 指令沒(méi)有被調(diào)用,這樣 就會(huì)陷入死鎖,鎖永遠(yuǎn)得不到釋放。于是我們?cè)谀玫芥i之后,再給鎖加上一個(gè)過(guò)期時(shí)間,比如 5s,這樣即使中間出現(xiàn)異常也 可以保證 5 秒之后鎖會(huì)自動(dòng)釋放。
setnx?lock:codehole?true OK >?expire?lock:codehole?5?... do?something?critical?... >?del?lock:codehole(integer)?1如果在 setnx 和 expire 之間服務(wù)器進(jìn)程突然掛掉了,可能是因?yàn)闄C(jī)器掉電或者是被人為殺掉的,就會(huì)導(dǎo)致 expire 得不到執(zhí)行,也會(huì)造成死鎖。
這種問(wèn)題的根源就在于 setnx 和 expire 是兩條指令而不是原子指令。如果這兩條指令可 以一起執(zhí)行就不會(huì)出現(xiàn)問(wèn)題。也許你會(huì)想到用 Redis 事務(wù)來(lái)解決。但是這里不行,因?yàn)?expire 是依賴于 setnx 的執(zhí)行結(jié)果的,如果 setnx 沒(méi)搶到鎖,expire 是不應(yīng)該執(zhí)行的。事務(wù)里沒(méi)有 if else 分支邏輯,事務(wù)的特點(diǎn)是一口氣執(zhí)行,要么全部執(zhí)行要么一個(gè)都不執(zhí)行。
Redis 2.8 版本中作者加入了 set 指令的擴(kuò)展參數(shù),使得 setnx 和 expire 指令可以一起執(zhí)行:
set?lock:codehole?trueex?5?nx OK ...?do?something?critical?... del?lock:codehole上面這個(gè)指令就是 setnx 和 expire 組合在一起的原子指令,它就是分布式鎖的奧義所在。
分布式鎖存在的問(wèn)題
超時(shí)問(wèn)題:如果在加鎖和釋放鎖之間的邏輯執(zhí)行的太長(zhǎng),以至于超出了鎖的超時(shí)限制,就會(huì)出現(xiàn)問(wèn)題。因?yàn)檫@時(shí)候鎖過(guò)期了,第二個(gè)線程重新持有了這把鎖,但是緊接著第一個(gè)線程執(zhí)行完了業(yè)務(wù)邏輯,就把鎖給釋放了,第三個(gè)線程就會(huì)在第二個(gè)線程邏輯執(zhí)行完之間拿到了鎖。
單節(jié)點(diǎn)的分布式鎖問(wèn)題:在單 Matste 的主從 Matster-Slave Redis 系統(tǒng)中,正常情況下 Client 向 Master 獲取鎖之后同步給 Slave,如果 Client 獲取鎖成功之后 Master 節(jié)點(diǎn)掛掉,并且未將該鎖同步到 Slave,之后在 Sentinel 的幫助下 Slave 升級(jí)為 Master 但是并沒(méi)有之前未同步的鎖的信息,此時(shí)如果有新的 Client 要在新 Master 獲取鎖,那么將可能出現(xiàn)兩個(gè) Client 持有同一把鎖的問(wèn)題,來(lái)看個(gè)圖來(lái)想下這個(gè)過(guò)程:
所以,為了保證自己的鎖只能自己釋放需要增加唯一性的校驗(yàn),綜上基于單 Redis 節(jié)點(diǎn)的獲取鎖和釋放鎖的簡(jiǎn)單過(guò)程如下:
//?獲取鎖?unique_value作為唯一性的校驗(yàn) SET?resource_name?unique_value?NX?PX?30000//?釋放鎖?比較unique_value是否相等?避免誤釋放 if?redis.call("get",KEYS[1])?==?ARGV[1]?thenreturn?redis.call("del",KEYS[1]) elsereturn?0 end關(guān)于分布式鎖的 Redlock 算法
Redis 性能好并且實(shí)現(xiàn)方便,但是單節(jié)點(diǎn)的分布式鎖在故障遷移時(shí)產(chǎn)生安全問(wèn)題,Redlock 算法是 Redis 的作者 Antirez 提出的集群模式分布式鎖,基于 N 個(gè)完全獨(dú)立的 Redis 節(jié)點(diǎn)實(shí)現(xiàn)分布式鎖的高可用。
在 Redis 的分布式環(huán)境中,我們假設(shè)有 N 個(gè)完全互相獨(dú)立的 Redis 節(jié)點(diǎn),在 N 個(gè) Redis 實(shí)例上使用與在 Redis 單實(shí)例下相同方法獲取鎖和釋放鎖。
現(xiàn)在假設(shè)有 5 個(gè) Redis 主節(jié)點(diǎn)(大于 3 的奇數(shù)個(gè)),這樣基本保證他們不會(huì)同時(shí)都宕掉,獲取鎖和釋放鎖的過(guò)程中,客戶端會(huì)執(zhí)行以下操作:
獲取當(dāng)前 Unix 時(shí)間,以毫秒為單位
依次嘗試從 5 個(gè)實(shí)例,使用相同的 key 和具有唯一性的 value 獲取鎖 當(dāng)向 Redis 請(qǐng)求獲取鎖時(shí),客戶端應(yīng)該設(shè)置一個(gè)網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間,這樣可以避免客戶端死等
客戶端使用當(dāng)前時(shí)間減去開(kāi)始獲取鎖時(shí)間就得到獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)從半數(shù)以上的 Redis 節(jié)點(diǎn)取到鎖,并且使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功
如果取到了鎖,key 的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間,這個(gè)很重要
如果因?yàn)槟承┰?#xff0c;獲取鎖失敗(沒(méi)有在半數(shù)以上實(shí)例取到鎖或者取鎖時(shí)間已經(jīng)超過(guò)了有效時(shí)間),客戶端應(yīng)該在所有的 Redis 實(shí)例上進(jìn)行解鎖,無(wú)論 Redis 實(shí)例是否加鎖成功,因?yàn)榭赡芊?wù)端響應(yīng)消息丟失了但是實(shí)際成功了,畢竟多釋放一次也不會(huì)有問(wèn)題
關(guān)于集群
在大數(shù)據(jù)高并發(fā)場(chǎng)景下,單個(gè) Redis 實(shí)例往往會(huì)顯得捉襟見(jiàn)肘。首先體現(xiàn)在內(nèi)存上,單個(gè) Redis 的內(nèi)存不宜過(guò)大,內(nèi)存太大會(huì)導(dǎo)致 rdb 文件過(guò)大,進(jìn)一步導(dǎo)致主從同步時(shí)全量同步時(shí)間過(guò)長(zhǎng),在實(shí)例重啟恢復(fù)時(shí)也會(huì)消耗很長(zhǎng)的數(shù)據(jù)加載時(shí)間,特別是在云環(huán)境下,單個(gè)實(shí)例內(nèi)存往往都是受限的。其次體現(xiàn)在 CPU 的利用率上,單個(gè) Redis 實(shí)例只能利用單個(gè)核心,這單個(gè)核心要完成海量數(shù)據(jù)的存取和管理工作壓力會(huì)非常大。所以孕育而生了 Redis 集群,集群方案主要有以下幾種:
Sentinel:Sentinel(哨兵)模式,基于主從復(fù)制模式,只是引入了哨兵來(lái)監(jiān)控與自動(dòng)處理故障
Codis:Codis 是 Redis 集群方案之一,令我們感到驕傲的是,它是中國(guó)人開(kāi)發(fā)并開(kāi)源的,來(lái)自前豌豆莢中間件團(tuán)隊(duì)。
Cluster:Redis Cluster 是 Redis 的親兒子,它是 Redis 作者自己提供的 Redis 集群化方案。
感謝閱讀,部分圖片來(lái)源于互聯(lián)網(wǎng),暫未備注來(lái)源~
本文參考:
Redis 開(kāi)發(fā)與運(yùn)維:https://book.douban.com/subject/26971561
事務(wù)和 Lua 腳本:https://whiteccinn.github.io/2020/06/02/Redis/redis%E4%BA%8B%E5%8A%A1%E5%92%8Clua
Redis GEO 功能使用場(chǎng)景:https://www.cnblogs.com/54chensongxia/p/13813533.html
Redis 與數(shù)據(jù)庫(kù)一致性:https://note.dolyw.com/cache/00-DataBaseConsistency.html
總結(jié)
以上是生活随笔為你收集整理的一篇详文带你入门 Redis的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 2021 大前端技术回顾及未来展望
- 下一篇: Redis基本概念