Redis基本介紹
Remote Dictionary Server 高性能key-value數(shù)據(jù)庫,支持BSD協(xié)議
Redis基本使用
數(shù)據(jù)類型
String
Redis使用SDS(Simple Dynamic String)動態(tài)字符串保存,可以根據(jù)不同的字符串長度使用不同的結(jié)構(gòu)體
使用場景:
- 保存
Session實現(xiàn)單點登錄 - 計數(shù)器,記錄網(wǎng)站的瀏覽量或者點贊數(shù)等信息,然后按照一定的規(guī)則持久化到數(shù)據(jù)庫中
- 緩存數(shù)據(jù),常用的是把緩存對象轉(zhuǎn)為
Json格式的字符串保存,讀取的時候再反序列化
List 有序列表
Redis底層是使用QuickList保存,相當(dāng)于Java中的LinkedList,每個節(jié)點都是ZipList的雙向鏈表保存,所以從列表的兩端取數(shù)據(jù)效率很高,查詢數(shù)據(jù)的時間復(fù)雜度為O(n)
使用場景:
- 粉絲列表
- 文章評論列表等,可以使用lrange命令,進行分頁查詢,提高應(yīng)用分頁查詢的速度
- 消息隊列,使用LPush和BRPop命令可以實現(xiàn)簡單的消息隊列功能
常用命令
| 命令 | 作用 |
|---|---|
| LPUSH | 從列表頭部插入一條數(shù)據(jù) |
| BRPOP | 從列表尾部移除一條數(shù)據(jù),如果沒有數(shù)據(jù),則會一直阻塞等待到超時或者有元素為止 |
| BRPOPLPUSH | 從列表中彈出一個值,并且將他插入到另外一個列表的頭部,如果列表中沒有元素,則會一直阻塞等待到超時或者有元素為止 |
| RPOP | 從列表中取出指定下標(biāo)的元素 |
Set 集合
內(nèi)部使用value為空的Hashtable實現(xiàn),查詢的時間復(fù)雜度為O(1)
使用場景:
- 去重
SortedSet 有序集合
基于跳躍表實現(xiàn),具體查看此文章,他給每一個元素設(shè)置一個score分數(shù),然后根據(jù)分數(shù)進行排序。
使用場景:
- 各種排行榜
- 帶權(quán)重的隊列,可以讓線程根據(jù)權(quán)重優(yōu)先執(zhí)行某些任務(wù)。
Hash
內(nèi)部使用ziplist或者Hashtable實現(xiàn),相當(dāng)于Java中的HashMap,使用數(shù)據(jù)+鏈表的方式解決Hash沖突的問題
使用場景:
- 一般可以把
java對象緩存到hash里面,然后通過key-value的方式取值,但是實際開發(fā)中的對象一般比較復(fù)雜,嵌套類型,所以使用hash較少
過期時間
Expire key seconds 給key設(shè)置過期時間
刪除策略
- 消極方法
- 當(dāng)用戶get獲取值的時候,判斷當(dāng)前key是否過期,如果過期就刪除,不返回給用戶
- 積極方法
- 周期性的從設(shè)置了過期時間的key中隨機的選擇20個進行檢查
- 刪除已經(jīng)過期的鍵
- 如果有25%的key過期,則重復(fù)一次該操作
pub/sub
publish channel message 發(fā)布一條消息到channel通道
subscribe channel[channel...] 訂閱一個或者多個通道的消息
發(fā)布消息后就刪除了,不能實現(xiàn)消息的持久化以及重發(fā)等功能,需要專門的消息隊列中間件來實現(xiàn)(Kafka、RocketMQ、RabbitMQ)
自增
使用incr命令可以進行原子遞增
getset
設(shè)置一個key的value并獲取設(shè)置前的值,
Redis內(nèi)存回收策略
- noeviction 不淘汰任何鍵值對,如果進行讀操作則正常工作,進行寫操作返回錯誤。Redis默認策略
- allkeys-lru 淘汰最近最少使用的鍵值對
- allkeys-random 對所有的鍵采用隨機刪除策略
- volatile-lru 在設(shè)置了過期時間的鍵中采用最近最少使用策略刪除鍵
- volatile-random 在設(shè)置了過期時間的鍵中采用隨機刪除的策略刪除鍵值對
- volatile-ttl 在設(shè)置了過期時間的鍵中,具有更早過期時間的key優(yōu)先移除
JoinGroup過程
RDB方式:當(dāng)一定的條件觸發(fā)后,Redis會fork一個子進程來進行持久化操作,會把內(nèi)存中的數(shù)據(jù)集以快照形式寫入磁盤,采用二進制壓縮存儲,將所有的數(shù)據(jù)寫入到一個臨時文件中,等寫入完成后會替換上次持久化的文件。
觸發(fā)條件
- 用戶配置(默認下面save seconds 操作次數(shù))
- save 900 1
- save 300 10
- save 60 10000
-
bgsave、save調(diào)用save方法或者bgsave方法 -
flushall清空數(shù)據(jù) -
replication主從同步數(shù)據(jù)
AOF: 每隔一秒或者每次更改redis數(shù)據(jù)就將命令追加到AOF文件中
- 默認不開啟,使用配置中
appendonly yes打開AOF -
AOF文件體積過大時,會自動的在后臺對AOF進行重寫,重寫后新的AOF文件包含了恢復(fù)當(dāng)前數(shù)據(jù)集所需要的最小命令集;主進程會fork一個子進程進行重寫,類似于RDB快照的方式。 -
auto-aof-rewrite-percentage表示表示當(dāng)前的AOF文件大小 超過上一次重寫時的AOF文件大小的百分之多少時會再次進行重寫,如果之前沒有重寫過,則以啟動時AOF文件大小為依據(jù)。 -
Auto-aof-rewrite-min-size表示限制了允許重寫的最小AOF文件大小
RDB和AOF的優(yōu)缺點:
- 當(dāng)服務(wù)發(fā)生故障,
RDB方式會丟失上次備份之后的所有數(shù)據(jù),AOF最多丟失1秒內(nèi)的數(shù)據(jù) -
Redis重啟時,首先通過RDB加載數(shù)據(jù)到內(nèi)存中,速度比較快,然后通過AOF文件中的近期操作指令,將數(shù)據(jù)恢復(fù)到重啟之前的狀態(tài) -
AOF文件可讀性比較好,但是相同內(nèi)容的數(shù)據(jù),會比RDB大很多
Redis 單線程
Redis為什么這么塊
- Redis是在內(nèi)存中操作數(shù)據(jù),而且存儲的數(shù)據(jù)結(jié)構(gòu)類似于
Java中的HashMap,查詢的時間復(fù)雜度為O(1); - 單線程,不需要在線程間切換,減少了上下文切換的資源消耗,單線程也不需要考慮鎖的使用,減少了獲取鎖和釋放鎖的資源消耗
- 使用I/O多路復(fù)用,同步非阻塞IO
Lua腳本
Redis是單線程的,在內(nèi)部不會存在線程安全的問題,但是如果有多個客戶端同時訪問,就相當(dāng)于多線程,多個客戶端之間沒有請求的同步,實際順序不一樣就可能產(chǎn)生線程安全問題。使用Lua腳本可以滿足原子性。
基本使用
eval "redis.call(’set’,’hello’,’world')" 0不帶參數(shù)
eval “redis.call(’set’,KEYS[1],ARGV[1])” 1 hello world 帶參數(shù)
Lua腳本的好處
- 減少網(wǎng)絡(luò)開銷,在Lua腳本中可以把多個命令放在同一個腳本中運行
- 原子操作,Redis會將整個腳本作為一個整體執(zhí)行,中間不會被其他命令插入
- 復(fù)用性,客戶端發(fā)送的腳本會存儲在Redis中,其他客戶端可以復(fù)用這一腳本完成同樣的邏輯。
Pinelining 管道
Redis是一種基于客戶端-服務(wù)端模型以及請求/響應(yīng)協(xié)議的TCP服務(wù),通常會遵循以下步驟
- 客戶端發(fā)送一個查詢請求,并監(jiān)聽
socket返回,通常是阻塞模式,等待服務(wù)端響應(yīng) - 服務(wù)端處理命令,然后將結(jié)果返回給客戶端
如果客戶端需要發(fā)送批量的命令的時候,往返時間就會變的很長(RTT Round Trip Time),使用pipeline可以減少RTT的時間,服務(wù)端也可以見減少I/O的調(diào)用次數(shù)(用戶態(tài)->內(nèi)核態(tài))
分布式鎖的實現(xiàn)
setnx命令可以設(shè)置一個key-value鍵值對,如果當(dāng)前Redis中已經(jīng)有該key,會返回失敗。多個進程同時進行setnx操作的時候,只會有一個可以設(shè)置成功。
-
setnx需要設(shè)置一個超時時間,防止獲取鎖的進程掛掉后導(dǎo)致死鎖 - 存在一種情況,當(dāng)一個客戶端A獲取鎖成功后,由于某種原因阻塞了,然后超時時間到了,自動釋放鎖了,然后另一個客戶端B獲取了鎖,此時客戶端A阻塞結(jié)束并且運行結(jié)束后,會嘗試釋放鎖,這時候可能會把客戶端A的鎖釋放了。解決辦法:
setnx key randomValue設(shè)置一個隨機值,每個客戶端不同,釋放鎖的時候,首先判斷一下當(dāng)前value是否與自己客戶端相同,如果相同才能釋放鎖。
Redis集群
1.如何配置
master節(jié)點不用做任何修改,只需要在slave節(jié)點中,修改redis.conf文件中添加slaveof master-ip master-port
2.數(shù)據(jù)如何同步
-
slave初始化階段,Redis會觸發(fā)全量復(fù)制,slave需要將master節(jié)點上的所有數(shù)據(jù) - Master服務(wù)器執(zhí)行
bgsave命令(子線程),生成快照,同時記錄在此期間的寫命令,快照發(fā)送到Slave節(jié)點并載入后,再把緩存的寫命令發(fā)送過來執(zhí)行命令 -
min-slaves-to-write 3表示只有當(dāng)3個slave同步完成,master才是可寫的 -
min-slaves-max-lag 10表示允許slave最長失去連接的時間是10秒,10秒還沒收到slave響應(yīng),master就認為slave已經(jīng)斷開 -
master node會在內(nèi)存中創(chuàng)建一個backlog,master和slave都會保存要給replica offset還有一個master id,如果slave斷開連接,重連后slave會讓master從上次的replica offset開始繼續(xù)復(fù)制數(shù)據(jù),如果沒有對應(yīng)的offset,則會進行一次全量同步

Redis哨兵(Sentinel)
Redis集群之后,如果master節(jié)點掛了,需要從slave節(jié)點中選舉master,Redis沒有提供相關(guān)的功能,需要哨兵來進行監(jiān)控。
哨兵是一個單獨的進程,一般使用三個哨兵集群,保證哨兵的高可用。此時哨兵不僅會監(jiān)控master和slave,還會互相監(jiān)控。
配置:在redis-sentinel.conf文件中配置 sentinel monitor name ip port quorum,只需要配置master節(jié)點即可
哨兵之間相互感知
- 所有的
sentinel向他們的監(jiān)視的master節(jié)點訂閱channelsentinel - 新加入的
sentinel節(jié)點向master的這個節(jié)點發(fā)布一條消息,其他訂閱了這個channel的sentinel會發(fā)現(xiàn)這個新的sentinel - 新加入的
sentinel和其他sentinel節(jié)點建立長連接
故障發(fā)現(xiàn)
sentinel節(jié)點定期向master節(jié)點發(fā)送心跳包判斷是否存活,一旦發(fā)現(xiàn)master沒有正確響應(yīng),sentinel就會把master設(shè)為主觀不可用狀態(tài),然后把狀態(tài)發(fā)送給其他sentinel確認,當(dāng)確認的sentinel超過quorum時,就認為master節(jié)點客觀不可用,接著就進入選舉新的master的流程,這里使用到了Raft算法,基于投票的算法,只要保證過半數(shù)節(jié)點通過提議即可。
哨兵的主要功能
- 集群監(jiān)控 哨兵可以監(jiān)控master節(jié)點和slave節(jié)點的運行情況
- 消息通知 當(dāng)集群中有某一個節(jié)點掛了之后,可以通知管理員
- 故障轉(zhuǎn)移 當(dāng)master節(jié)點掛了之后,哨兵會從slave節(jié)點中重新選舉一個做為master節(jié)點
- 配置中心 如果發(fā)生故障轉(zhuǎn)移,通知客戶端新的master節(jié)點地址
Redis分片(Cluster)

主從同步的集群中,每個節(jié)點都存有集群中的所有數(shù)據(jù),存在著單機存儲量的瓶頸問題,形成了木桶效應(yīng)。對Redis進行分片集群,可以提高Redis的性能和存儲能力。
集群結(jié)構(gòu)如上圖,一個Redis Cluster由多個Redis節(jié)點構(gòu)成,不同節(jié)點組服務(wù)沒有交集,也就是每一個節(jié)點組對應(yīng)數(shù)據(jù)sharding的一個分片。節(jié)點組內(nèi)部分為主備兩類節(jié)點,對應(yīng)master和slave節(jié)點。兩者數(shù)據(jù)準(zhǔn)實時一直,通過異步化的主備復(fù)制機制來保證。一個節(jié)點組有且只有一個master節(jié)點,同時可以有0到n個slave節(jié)點,在這個節(jié)點組中,只有master節(jié)點可以提供讀寫服務(wù),slave只能提供讀服務(wù)。
集群的數(shù)據(jù)分片
Redis 集群沒有使用一致性hash, 而是引入了 哈希槽的概念.
Redis 集群有16384個哈希槽,每個key通過CRC16校驗后對16384取模來決定放置哪個槽.集群的每個節(jié)點負責(zé)一部分hash槽,舉個例子,比如當(dāng)前集群有3個節(jié)點,那么:
- 節(jié)點 A 包含 0 到 5500號哈希槽.
- 節(jié)點 B 包含5501 到 11000 號哈希槽.
- 節(jié)點 C 包含11001 到 16384號哈希槽.
這種結(jié)構(gòu)很容易添加或者刪除節(jié)點. 比如如果我想新添加個節(jié)點D, 我需要從節(jié)點 A, B, C中得部分槽到D上. 如果我想移除節(jié)點A,需要將A中的槽移到B和C節(jié)點上,然后將沒有任何槽的A節(jié)點從集群中移除即可. 由于從一個節(jié)點將哈希槽移動到另一個節(jié)點并不會停止服務(wù),所以無論添加刪除或者改變某個節(jié)點的哈希槽的數(shù)量都不會造成集群不可用的狀態(tài).
如何把一次操作的所有值保存到一個節(jié)點中
Redis中引入了HashTag的概念,可以是得數(shù)據(jù)分布算法可以根據(jù)key的某一個部分進行計算,
舉個簡單的例子,加入對于用戶的信息進行存儲, user:user1:id、user:user1:name/ 那么通過hashtag的方式, user:{user1}:id、user:{user1}.name; 表示
當(dāng)一個key包含 {} 的時候,就不對整個key做hash,而僅對 {} 包括的字符串做hash。
擴容
- 槽位遷移
- 數(shù)據(jù)遷移
Redis緩存問題(緩存擊穿、緩存穿透、緩存雪崩、緩存不一致)
緩存擊穿
當(dāng)Redis緩存中的某個熱點key失效,導(dǎo)致大量的請求穿過緩存,到達DB
緩存穿透
大量的無效請求到達數(shù)據(jù)庫,比如請求參數(shù)id = -1,redis和數(shù)據(jù)庫中都沒有數(shù)據(jù),但是會一直查詢數(shù)據(jù)庫。
解決辦法
- 熱點數(shù)據(jù)設(shè)置永不失效
- 多級緩存
- 熔斷降級
- nginx拉黑多次請求的同一ip
- 參數(shù)校驗,永遠不要相信用戶
- 當(dāng)查詢到數(shù)據(jù)庫中沒有的值,可以設(shè)置一個為null或其他提示的數(shù)據(jù)到緩存中,下次請求就不會到數(shù)據(jù)庫中了,緩存的失效時間設(shè)置短一點如30秒
- 使用互斥鎖
- 布隆過濾器
緩存雪崩
同一時間Redis緩存的key大面積失效,導(dǎo)致所有的請求都到達了數(shù)據(jù)庫
- 一般這種情況是redis的過期時間到了,可以設(shè)置一個隨機值,讓key在一定范圍內(nèi)失效
- Redis分片,將key保存到不同的節(jié)點上,防止出現(xiàn)大面積失效的情況
緩存更新方式
這是決定在使用緩存時就該考慮的問題。
緩存的數(shù)據(jù)在數(shù)據(jù)源發(fā)生變更時需要對緩存進行更新,數(shù)據(jù)源可能是 DB,也可能是遠程服務(wù)。更新的方式可以是主動更新。數(shù)據(jù)源是 DB 時,可以在更新完 DB 后就直接更新緩存。
當(dāng)數(shù)據(jù)源不是 DB 而是其他遠程服務(wù),可能無法及時主動感知數(shù)據(jù)變更,這種情況下一般會選擇對緩存數(shù)據(jù)設(shè)置失效期,也就是數(shù)據(jù)不一致的最大容忍時間。
這種場景下,可以選擇失效更新,key 不存在或失效時先請求數(shù)據(jù)源獲取最新數(shù)據(jù),然后再次緩存,并更新失效期。
但這樣做有個問題,如果依賴的遠程服務(wù)在更新時出現(xiàn)異常,則會導(dǎo)致數(shù)據(jù)不可用。改進的辦法是異步更新,就是當(dāng)失效時先不清除數(shù)據(jù),繼續(xù)使用舊的數(shù)據(jù),然后由異步線程去執(zhí)行更新任務(wù)。這樣就避免了失效瞬間的空窗期。另外還有一種純異步更新方式,定時對數(shù)據(jù)進行分批更新。實際使用時可以根據(jù)業(yè)務(wù)場景選擇更新方式。
數(shù)據(jù)不一致
第二個問題是數(shù)據(jù)不一致的問題,可以說只要使用緩存,就要考慮如何面對這個問題。緩存不一致產(chǎn)生的原因一般是主動更新失敗,例如更新 DB 后,更新 Redis 因為網(wǎng)絡(luò)原因請求超時;或者是異步更新失敗導(dǎo)致。
解決的辦法是,如果服務(wù)對耗時不是特別敏感可以增加重試;如果服務(wù)對耗時敏感可以通過異步補償任務(wù)來處理失敗的更新,或者短期的數(shù)據(jù)不一致不會影響業(yè)務(wù),那么只要下次更新時可以成功,能保證最終一致性就可以。