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