手機企業(yè)網(wǎng)站制作企業(yè)網(wǎng)頁設(shè)計公司
目錄
Redis 是單線程的!為什么
Redis-Key(操作redis的key命令)
String
擴展字符串操作命令
數(shù)字增長命令
字符串范圍range命令
設(shè)置過期時間命令
批量設(shè)置值
string設(shè)置對象,但最好使用hash來存儲對象
組合命令getset,先get然后在set
Hash
hash命令:
hash基礎(chǔ)的增刪查
hash擴展命令
總結(jié)
List
Set
ZSet
Redis事務(wù)
redis的事務(wù)命令:
正常執(zhí)行事務(wù) exec
放棄事務(wù) discard
命令異常,事務(wù)回滾
1.類似于java編譯型異常(語法錯誤)
2.類似于java運行時異常(如1/0,除數(shù)不能為0錯誤) (邏輯錯誤)
監(jiān)控 Watch(相當(dāng)于java加鎖) (面試: redis的watch命令監(jiān)控實現(xiàn)秒殺系統(tǒng))
悲觀鎖
樂觀鎖
Redis用watch做樂觀鎖實現(xiàn)步驟
redis監(jiān)視watch測試案例
取錢正常執(zhí)行成功的流程
取錢出現(xiàn)并發(fā)問題的流程
Redis的Java客戶端
測試jedis
Springboot整合Redis
自定義RedisTemplate
緩存
緩存穿透
解決方案一:緩存空數(shù)據(jù)
解決方案二:布隆過濾器
緩存擊穿
解決方案一:互斥鎖(分布式鎖)
解決方案二:邏輯過期
緩存雪崩
持久化
1.RDB
RDB執(zhí)行原理
2.AOF
RDB與AOF對比
雙寫一致性
1.一致性要求高
1.延遲雙刪
1.無論第一步是先刪除緩存還是先修改數(shù)據(jù)庫都會導(dǎo)致臟數(shù)據(jù)的出現(xiàn)
2.刪除兩次緩存的原因
3.延時刪除的原因
總結(jié)
2.加讀寫鎖
2.允許延遲一致(較為主流)
1.異步通知保證數(shù)據(jù)的最終一致性
2.基于Canal的異步通知
數(shù)據(jù)過期策略
方案一惰性刪除:
方案二定期刪除:
總結(jié)
數(shù)據(jù)淘汰策略
八種不同策略
數(shù)據(jù)淘汰策略--使用建議
分布式鎖
場景
引入與基本介紹
redis分布式鎖實現(xiàn)原理
基本介紹
設(shè)置超時失效時間的原因(避免死鎖):
總結(jié)
缺陷
Redisson實現(xiàn)的分布式鎖
1.redisson實現(xiàn)的分布式鎖的執(zhí)行流程/合理地控制鎖的有效時長(失效時間)
2.可重入
3.主從一致性
Redis發(fā)布訂閱
訂閱/發(fā)布消息圖
命令
測試
Redis發(fā)布訂閱原理
總結(jié)
使用場景
Redis消息隊列
概念
基于List結(jié)構(gòu)模擬消息隊列(可實現(xiàn)阻塞隊列的效果)
基于PubSub(發(fā)布訂閱)的消息隊列
基于Stream的消息隊列
基本知識
消費者組
Redis集群(分布式緩存)
單點Redis的問題
主從復(fù)制
主從數(shù)據(jù)同步原理
1.主從全量同步
2.主從增量同步(slave重啟或后期數(shù)據(jù)變化)
3.總結(jié)
哨兵模式
哨兵模式的結(jié)構(gòu)與作用
服務(wù)狀態(tài)監(jiān)控
哨兵選主規(guī)則(主節(jié)點宕機后,選從節(jié)點為主節(jié)點的規(guī)則)
Redis集群(哨兵模式)的腦裂問題
分片集群
redis集群環(huán)境部署(環(huán)境配置)
一主二從集群搭建(命令或文件配置)(這種方式的redis集群實際工作用不到,僅供基礎(chǔ)學(xué)習(xí))
命令方式配置
文件方式配置(一主二從,持久化的,對于哨兵模式,不建議使用這種)
一主兩從的第二種搭建方式(層層鏈路)哨兵模式的手動版
I/O多路復(fù)用模型
redis為什么這么快
用戶空間和內(nèi)核空間
阻塞IO
非阻塞IO
IO多路復(fù)用
Redis網(wǎng)絡(luò)模型
Redis 是單線程的!為什么
Redis是基于內(nèi)存實現(xiàn)的,使用Redis時,CPU幾乎不會成為Redis性能瓶頸,Redis的瓶頸是機器的內(nèi)存和網(wǎng)絡(luò)帶寬(網(wǎng)絡(luò)),既然可以使用單線程來實現(xiàn),就使用單線程了!所有就使用了單線程了!
內(nèi)存訪問速度:由于Redis將數(shù)據(jù)存儲在內(nèi)存中,數(shù)據(jù)訪問速度非???/strong>,通常接近于CPU的緩存訪問速度。這意味著CPU在讀取或?qū)懭霐?shù)據(jù)時很少需要等待,從而減少了CPU的空閑時間。
計算密集度:Redis的操作通常是簡單的數(shù)據(jù)查找、插入、刪除和計算集合操作(如交集、并集等)。這些操作在CPU層面上的計算復(fù)雜度相對較低,因此不太可能使CPU成為瓶頸。
并發(fā)處理:Redis使用單線程模型來處理客戶端請求(盡管有IO多路復(fù)用技術(shù)來同時處理多個連接),但這并不意味著Redis不能利用多核CPU。通過部署多個Redis實例或使用Redis集群,可以水平擴展以利用多核CPU的并行處理能力。
Redis 是C 語言寫的,官方提供的數(shù)據(jù)為 100000+ 的QPS,完全不比同樣是使用 key-vale的Memecache差!
Redis 為什么單線程還這么快?
1、高性能的服務(wù)器不一定是多線程的
2、多線程不一定比單線程效率高!(多線程CPU上下文會切換消耗資源!)
速度:CPU>內(nèi)存>硬盤
核心:redis 是將所有的數(shù)據(jù)全部放在內(nèi)存中的,所以說使用單線程去操作效率就是最高的,多線程(CPU上下文會切換:耗時的操作!)
對于內(nèi)存系統(tǒng)來說,如果沒有上下文切換效率就是最高的!
多次讀寫都是在一個CPU上的,在內(nèi)存情況下,這個就是最佳的方案!
Redis-Key(操作redis的key命令)
keys * # 查看所有的key?
set name123 kuangshen # set key
EXISTS name123 # 判斷當(dāng)前的key是否存在
move name123 # 移除當(dāng)前的key
EXPIRE name123 10 # 設(shè)置key的過期時間,單位是秒
ttl name123 # 查看當(dāng)前key的剩余時間
type name123? ??# 查看當(dāng)前key的一個類型
String
擴展字符串操作命令
- APPEND key123 "hello" # 追加字符串,如果當(dāng)前key不存在,就相當(dāng)于setkey?
- STRLEN key123 # 獲取字符串的長度!
數(shù)字增長命令
- set views123 0 # 初始瀏覽量為0?
- incr views123 # 自增1 瀏覽量變?yōu)??
- decr views123 # 自減1 瀏覽量-1?
- INCRBY views123 10 # 可以設(shè)置步長,指定增量10!
字符串范圍range命令
- GETRANGE key123 0 3 # 截取字符串 [0,3]
- GETRANGE key123 0 -1 # 獲取全部的字符串 和 get key是一樣的
- SETRANGE key123 1 xx # 替換指定位置1開始的字符串!
設(shè)置過期時間命令
setex (set with expire) //設(shè)置過期時間
例:setex key123 30 "hello" //設(shè)置key123 的值為hello,30秒后過期
setnx (set if not exist) //key不存在在設(shè)置,key存在則回滾設(shè)置失敗(在分布式鎖中會常常使用)?
例:setnx mykey123 "MongoDB" //如果mykey不存在創(chuàng)建成功,存在,創(chuàng)建失敗不會替換值
批量設(shè)置值
- ?mset k1 v1 k2 v2 k3 v3 # 同時設(shè)置多個key和value值(k1:v1, k2:v2, k3:v3)
- mget k1 k2 k3 # 根據(jù)多個key同時獲取多個值
- msetnx k1 v1 k4 v4 # msetnx 是一個原子性的操作,要么一起成功,要么一起失敗!
-
- 由于k1已經(jīng)存在,所以setnx一定會失敗,由于是原子性操作k4也會跟著失敗
string設(shè)置對象,但最好使用hash來存儲對象
- set user:1 {name:zhangsan,age:3} # 設(shè)置一個key為user:1的對象,值為 json字符來保存一個對象!?
-
- key值為user:{id}, value值為json
- mset user:1:name zhangsan user:1:age 2 #批量創(chuàng)建對象
-
- 這里的key是一個巧妙的設(shè)計: user:{id}:{filed} , 如此設(shè)計在Redis中是完全OK了!
- mget user:1:name user:1:age #批量獲取對象中的值
組合命令getset,先get然后在set
- getset db redis # 如果不存在值,則返回 nil?,但同時值被設(shè)置成了redis
- getset db mongodb # 如果存在值,獲取原來的值,并設(shè)置新的值
Hash
相當(dāng)于Map集合,key-value!,只是value存的是map,也就是key-map,值是map集合
- hash本質(zhì)和String類型沒有太大區(qū)別,還是一個簡單的key-value
- hset myhash field codeyuaiiao
-
- 命令含義:hset key mapkey mapvalue
hash命令:
hash基礎(chǔ)的增刪查
- hset myhash field1 yuaiiao # 添加或修改一個具體的值
- hget myhash field1 # 獲取一個字段值
- hmset myhash field4 hello field5 byebye # 添加多個字段值進map集合
- hmget myhash field3 field4 # 獲取多個指定字段值
- hgetall myhash # 獲取hash全部字段值(包含了key和value)
- hdel myhash field1 field2 # 刪除一個或多個指定字段值
hash擴展命令
- hlen myhash # 獲取hash表的字段數(shù)量
- hexists myhash field1 # 判斷hash表中指定字段是否存在
- hkeys myhash # 獲取所有field(相當(dāng)于key)
- hvals myhash # 獲取所有value
- hincrby myhash field3 2 # 指定增量
- hincrby myhash field3 -2 # 指定減量
- hsetnx myhash field4 yuaiiao # 如果不存在可以設(shè)置,如果存在則不可設(shè)置
總結(jié)
hash變更的用戶數(shù)據(jù)user表,name,age字段 ,尤其是用戶信息之類的,經(jīng)常變動的信息!
hash更適合于對象的存儲,String更加適合字符串存儲
List
Set
Redis的Set結(jié)構(gòu)與java中的HashSet類似,可以看做是一個value為null的HashMap.因為也是一個hash表,因此具備與hashset類似的特征:
無序
元素不可重復(fù)
查找快
支持交集,并集,差集等功能
ZSet
Redis事務(wù)
mysql事務(wù)本質(zhì)
- ACID特性,要么同時成功,要么同時失敗,保證原子性!
Redis事務(wù)本質(zhì)
- 一組命令的集合! 一個事務(wù)中的所有命令都會被序列化,在事務(wù)執(zhí)行過程中,會按照順序執(zhí)行!
- 一次性,順序性,排他性! 執(zhí)行一系列的命令!
------隊列 set get set 執(zhí)行-----
- Redis 事務(wù)沒有隔離級別的概念!
-
- 不會出現(xiàn)幻度,臟讀,不可重復(fù)讀等問題
- 所有的命令在事務(wù)中,并沒有直接被執(zhí)行,只有發(fā)起執(zhí)行命令的時候才會執(zhí)行! Exec
- Redis單條命令是保證原子性的,但是redis事務(wù)是一組命令的集合,所以不保證原子性!
redis的事務(wù)命令:
- 開啟事務(wù)(multi)
- 命令入隊(要執(zhí)行的命令) (事務(wù)隊列)
- 執(zhí)行事務(wù)(exec)
正常執(zhí)行事務(wù) exec
127.0.0.1:6379> multi # 開啟事務(wù)
OK
127.0.0.1:6379> set key hello # 執(zhí)行命令
QUEUED
127.0.0.1:6379> set key1 yuaiiao
QUEUED
127.0.0.1:6379> get key
QUEUED
127.0.0.1:6379> get key1
QUEUED
127.0.0.1:6379> exec # 執(zhí)行事務(wù)
1) OK
2) OK
3) "hello"
4) "yuaiiao"
127.0.0.1:6379>
放棄事務(wù) discard
127.0.0.1:6379> multi # 開啟事務(wù)
OK
127.0.0.1:6379> set key 1
QUEUED
127.0.0.1:6379> set key2 3
QUEUED
127.0.0.1:6379> discard # 放棄事務(wù)
OK
127.0.0.1:6379> get key2 #事務(wù)隊列中的命令都不會被執(zhí)行
(nil)
127.0.0.1:6379>
命令異常,事務(wù)回滾
1.類似于java編譯型異常(語法錯誤)
命令語法導(dǎo)致執(zhí)行錯誤,事務(wù)中所有的命令都不會被執(zhí)行。
127.0.0.1:6379> multi # 開啟事務(wù)
OK
127.0.0.1:6379> set k1 v1 # 執(zhí)行命令
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> getset k3 v3
QUEUED
127.0.0.1:6379> getset k3 # 錯誤的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec # 執(zhí)行事務(wù)報錯
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k2 # 事務(wù)執(zhí)行失敗,得不到值,所有的命令都不會被執(zhí)行
(nil)
127.0.0.1:6379>
2.類似于java運行時異常(如1/0,除數(shù)不能為0錯誤) (邏輯錯誤)
命令邏輯執(zhí)行錯誤 , 那么執(zhí)行命令的時候,其他的命令是可以正常執(zhí)行的,只是錯誤命令拋出異常!
證明事務(wù)不保證原子性
127.0.0.1:6379> set k1 "v1" # 字符串
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 # 對字符串進行 自增1 運行時異常錯誤
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec # 錯誤的命令報錯但是其余命令都能執(zhí)行
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) "v2"
127.0.0.1:6379> get k2 # 其余命令正常執(zhí)行
"v2"
127.0.0.1:6379> get k3
"v3"
127.0.0.1:6379>
監(jiān)控 Watch(相當(dāng)于java加鎖) (面試: redis的watch命令監(jiān)控實現(xiàn)秒殺系統(tǒng))
悲觀鎖
很悲觀,認為什么時候都會出問題,無論做什么都會加鎖,效率低下
樂觀鎖
很樂觀,認為什么時候都不會出現(xiàn)問題,所以不會上鎖,更新數(shù)據(jù)的時候去判斷一下,在此期間是否有人修改過這個數(shù)據(jù),使用version字段比較。
Redis用watch做樂觀鎖實現(xiàn)步驟
1.獲取最新version
2.更新的的時候比較version,version沒變更新成功,version改變進入自旋。
redis的樂觀鎖watch
- watch加鎖,記得用完需要unwatch解鎖
redis監(jiān)視watch測試案例
取錢正常執(zhí)行成功的流程
127.0.0.1:6379> set money 100 #存錢100
OK
127.0.0.1:6379> set out 0 #取錢0
OK
127.0.0.1:6379> watch money # 監(jiān)視money對象
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec # 事務(wù)正常結(jié)束 , 期間數(shù)據(jù)沒有發(fā)生變動 ,這個時候就正常執(zhí)行成功了!
1) (integer) 80
2) (integer) 20
127.0.0.1:6379>
取錢出現(xiàn)并發(fā)問題的流程
- 測試多線程修改值,使用watch當(dāng)做redis 的樂觀鎖操作
-
- 在開一個redis-client客戶端,一共有兩個客戶端
- 客戶端1:開啟事務(wù),money取錢20
- 客戶端2:這時候直接把money修改成1000
-
- 客戶端1:繼續(xù)執(zhí)行,out存錢20,這時候執(zhí)行事務(wù)會
-
-
- 執(zhí)行返回nil,修改失敗
-
# 客戶端1:開啟事務(wù),監(jiān)視money對象,money取錢20
127.0.0.1:6379> watch money # 監(jiān)視money對象
OK
127.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED# 客戶端1還未提交,客戶端2:這時候直接把money修改成1000
# set money 10000# 客戶端1:繼續(xù)執(zhí)行,out存錢20,然后執(zhí)行事務(wù),樂觀鎖對比money版本號改動了,執(zhí)行失敗
# 執(zhí)行之前,另一個線程,修改了我們的值,就會導(dǎo)致事務(wù)執(zhí)行失敗!# 127.0.0.1:6379> get money"80"# 127.0.0.1:6379> set money 1000# OK
127.0.0.1:6379> incrby out 20 #out存錢20
QUEUED
127.0.0.1:6379> exec # 提交事務(wù),監(jiān)視money的version是否變化,有變化事務(wù)回滾,結(jié)果返回nil
(nil)
127.0.0.1:6379>
- redis事務(wù)執(zhí)行失敗后的自旋步驟
-
- 先釋放監(jiān)控鎖watch,在重新獲取鎖重復(fù)以上步驟,進行自旋
127.0.0.1:6379> unwatch # 釋放鎖(監(jiān)控),如果發(fā)現(xiàn)事務(wù)執(zhí)行失敗,就先解鎖
OK
127.0.0.1:6379> watch money # 重新獲取鎖,獲取最新的值,再次監(jiān)視,select version
OK
127.0.0.1:6379> multi # 開啟事務(wù)執(zhí)行正常操作
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec # 在比對監(jiān)視的值是否發(fā)生了變化,如果沒有變化,可以執(zhí)行成功呢,如果變化了就執(zhí)行失敗,在重新以上步驟
1) (integer) 980
2) (integer) 40
127.0.0.1:6379>
- 如果修改失敗,獲取最新的值就好
Redis的Java客戶端
我們要使用java來操作redis
有springboot整合了,我們也要學(xué)習(xí)jedis
什么是jedis?
- jedis 是redis官方推薦的java連接開發(fā)工具! 使用java操作Redis的中間件 ! 如果你要使用java 操作redis, 那么一定要對jedis十分熟悉
測試jedis
- 導(dǎo)入對應(yīng)的依賴
<!--導(dǎo)入jedis-->
<dependencies><!-- https://mvnrepository.com/artifact/redis.clients/jedis --><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.3.0</version></dependency><!--導(dǎo)入 fastjson--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.70</version></dependency>
</dependencies>
2.編碼測試
- 連接redis數(shù)據(jù)庫
- 操作命令
- 斷開連接
package com.kuang;
import redis.clients.jedis.Jedis;public class TestPing {public static void main(String[] args) {// 1、 new Jedis 對象即可Jedis jedis = new Jedis("127.0.0.1", 6379);// jedis 所有的命令就是我們之前學(xué)習(xí)的所有指令!所以之前的指令學(xué)習(xí)很重要!System.out.println(jedis.ping());}
}
結(jié)果輸出:
jedis 所有的命令就是我們之前學(xué)習(xí)的所有指令!所以之前的指令學(xué)習(xí)很重要!
Springboot整合Redis
說明 :在 SpringBoot2.x之后, 原來使用的jedis 被替換為了 lettuce ?
-
- jedis :底層采用的直連, 多個線程操作的話 ,是不安全的, 如果想要避免不安全, 使用jedis pool 連接池 ! 更像 BIO模式,阻塞的.
- lettuce : 采用netty , 實例可以再多個線程中進行共享,不存在線程不安全的情況 ! 可以減少線程數(shù)量,不需要開連接池, 更像NIO模式非阻塞的
自定義RedisTemplate
redis關(guān)于對象的保存,對象需要序列化。
- 對象如果不序列化保存,則會報錯
分析redisTemplate源碼為什么對象需要序列化
- 分析源碼序列化配置
-
-
新建config/RedisConfig
- 不使用JDK序列化,key使用哪個string序列化,value使用json序列化
package com.kuang.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {// 這是我給大家寫好的一個固定模板,大家在企業(yè)中,拿去就可以直接使用!// 自己定義了一個 RedisTemplate @Bean @SuppressWarnings("all")public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {// 我們?yōu)榱俗约洪_發(fā)方便,一般直接使用 <String, Object>//源碼是<Object,Object>類型,可以自定義把Object轉(zhuǎn)換成String類型RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();template.setConnectionFactory(factory);// Json序列化配置Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// String 的序列化,解決redis存儲字符串是轉(zhuǎn)義字符,看著像亂碼StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();// key采用String的序列化方式template.setKeySerializer(stringRedisSerializer);// hash的key也采用String的序列化方式template.setHashKeySerializer(stringRedisSerializer);// value 序 列 化 方 式 采 用 jacksontemplate.setValueSerializer(jackson2JsonRedisSerializer);// hash 的 value 序 列 化 方 式 采 用 jacksontemplate.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet();return template;}}
StringRedisTemplate:
在使用String類型存儲自定義對象時:
存入到Redis的數(shù)據(jù)會存儲一個該類對象的位置:
比如:
"@class": "com.sky.test.User",
這種方法更麻煩一點,需要每次手動地序列化,反序列化。
緩存
緩存就是數(shù)據(jù)交換的緩沖區(qū)(稱作Cache),是存貯數(shù)據(jù)的臨時地方,一般讀寫性能較高。
緩存:緩存穿透,緩存擊穿,緩存雪崩
雙寫一致性,緩存的持久化
數(shù)據(jù)過期策略,數(shù)據(jù)淘汰策略
緩存穿透
緩存穿透:查詢一個不存在的數(shù)據(jù)(在緩存中和數(shù)據(jù)庫中都不存在),mysql查詢不到數(shù)據(jù)也不會直接寫入緩存,就會導(dǎo)致每次請求都查數(shù)據(jù)庫。
危害:如果有人惡意地讓很多線程并發(fā)地請求訪問這些不存在的數(shù)據(jù),那么這些所有的請求都會到達數(shù)據(jù)庫。而數(shù)據(jù)庫的并發(fā)不會很高,請求到達一定的量則會把數(shù)據(jù)庫搞垮,導(dǎo)致數(shù)據(jù)庫宕機。
解決方案一:緩存空數(shù)據(jù)
緩存空數(shù)據(jù),查詢返回的數(shù)據(jù)為空,仍把這個空結(jié)果進行緩存 。即{key:1,value:null}
優(yōu)點:簡單
缺點:消耗內(nèi)存,可能會發(fā)生不一致的問題
解決方案二:布隆過濾器
優(yōu)點:內(nèi)存占用較少,沒有多余key
缺點:實現(xiàn)復(fù)雜,存在誤判
首先是緩存預(yù)熱時往布隆過濾器中添加數(shù)據(jù)(存儲數(shù)據(jù)過程)。
布隆過濾器主要是用于檢索一個元素是否在一個集合中。
我們當(dāng)時使用的是redisson實現(xiàn)的布隆過濾器。
它的底層主要是先去初始化一個比較大數(shù)組(bitmap),里面存放的二進制0或1。在一開始都是0,當(dāng)一個key來了之后經(jīng)過3次hash計算,模于數(shù)組長度找到數(shù)據(jù)的下標(biāo)然后把數(shù)組中原來的0改為1,這樣的話,三個數(shù)組的位置就能標(biāo)明一個key的存在。
查找的過程也是一樣的。
缺點:
布隆過濾器有可能會產(chǎn)生一定的誤判,我們一般可以設(shè)置這個誤判率,大概不會超過5%,其實這個誤判是必然存在的,要不就得增加數(shù)組的長度,其實已經(jīng)算是很劃算了,5%以內(nèi)的誤判率一般的項目也能接受,不至于高并發(fā)下壓倒數(shù)據(jù)庫。
誤判示例如下圖:
誤判率:數(shù)組越小誤判率就越大,數(shù)組越大誤判率就越小,但是同時帶來了更多的內(nèi)存消耗。
緩存擊穿
緩存擊穿的意思是對于設(shè)置了過期時間的key,緩存在某個時間點過期的時候,恰好這時間點對這個key有大量的并發(fā)請求過來,這些請求發(fā)現(xiàn)緩存過期,一般都會從后端DB加載數(shù)據(jù)并回設(shè)到緩存,這個時候大并發(fā)的請求可能會瞬間把DB壓垮。
解釋:對于Redis中正好過期的數(shù)據(jù)(Redis不存在數(shù)據(jù)了),此時如果有請求來訪問這些數(shù)據(jù),正常來說是會去查DB,同時DB把數(shù)據(jù)更新到Redis,再把數(shù)據(jù)返回。那么Redis也就得到了刷新,后續(xù)redis也可以繼續(xù)為DB分擔(dān)壓力。
但是把DB數(shù)據(jù)更新到Redis的過程中,可能會花費過多的時間(可能是因為DB刷新到redis的數(shù)據(jù)是多表的,多表統(tǒng)計費時間),在這個時間段內(nèi)redis的數(shù)據(jù)未重建完成,大量的并發(fā)請求過來的話則會全部走DB,會瞬間把DB壓垮。
如圖所示:
解決方案一:互斥鎖(分布式鎖)
使用互斥鎖:當(dāng)緩存過期失效時,不立即去load db,先使用如redis的setnx去設(shè)置一個互斥鎖,當(dāng)操作成功返回時再進行l(wèi)oad db的操作并回設(shè)緩存,否則重試get緩存的方法。
如圖所示:
互斥鎖保證了同時只能有一個線程獲得鎖去查詢數(shù)據(jù)庫并重建redis緩存數(shù)據(jù)。保證了數(shù)據(jù)的強一致性。
缺點:性能較低。
解決方案二:邏輯過期
Redis中的熱點數(shù)據(jù)的key不設(shè)置過期時間,設(shè)置邏輯過期字段。
1、在設(shè)置key的時候,設(shè)置一個過期時間字段一塊存入緩存中,不給當(dāng)前key設(shè)置過期時間
2、當(dāng)查詢的時候,從redis取出數(shù)據(jù)后判斷時間是否過期
3、如果過期則開通另外一個線程進行數(shù)據(jù)同步,當(dāng)前線程正常返回數(shù)據(jù),這個數(shù)據(jù)不是最新
如:
key:1 value:{"id":"123", "title":"張三", "expire":153213455}
這種方案也是在查詢DB和重置邏輯過期時間時加上互斥鎖,其它線程來查詢緩存時要不就是得到還未更新的過期數(shù)據(jù),要不就得到更新后的數(shù)據(jù)。保證了在多個線程并發(fā)訪問時不把其它線程全部攔截住(就是不會讓它們一遍又一遍地重試獲取數(shù)據(jù))。
相比方案一更為高可用、性能優(yōu)。但由于可能會得到邏輯過期數(shù)據(jù),導(dǎo)致數(shù)據(jù)并不是絕對一致的。
特點:邏輯過期,更注重用戶體驗,高可用,性能優(yōu)。但不能保證數(shù)據(jù)絕對一致。
緩存雪崩
緩存雪崩是指設(shè)置緩存時采用了相同的過期時間,導(dǎo)致緩存在某一時刻同時失效,請求全部轉(zhuǎn)發(fā)到DB,DB瞬時壓力過重雪崩?;蛘呤?strong>Redis服務(wù)宏機,導(dǎo)致大量請求到達數(shù)據(jù)庫,帶來巨大壓力。
緩存雪崩與緩存擊穿的區(qū)別:
雪崩是很多key,擊穿是某一個key緩存。
第一種情況的解決方案是將緩存失效時間分散開,比如可以在原有的失效時間(TTL)基礎(chǔ)上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重復(fù)率就會降低,就很難引發(fā)集體失效的事件。
其余解決方案:
1.利用Redis集群提高服務(wù)的可用性(哨兵模式,集群模式)
2.給緩存業(yè)務(wù)添加降級限流策略(ngxin或spring cloud gateway) (降級限流策略可做為系統(tǒng)的保底策略,適用于穿透,擊穿,雪崩)
3.給業(yè)務(wù)添加多級緩存 (Guava或Caffeine)
持久化
Redis是內(nèi)存數(shù)據(jù)庫,如果不將內(nèi)存中的數(shù)據(jù)庫狀態(tài)保存到磁盤,那么一旦服務(wù)器進程退出,服務(wù)器中的數(shù)據(jù)庫狀態(tài)也會消失(斷電即失),所以redis提供了持久化功能。
1.RDB
RDB執(zhí)行原理
這里存在一個問題:就是如果子進程在讀共享的內(nèi)存數(shù)據(jù)時,主進程正在對共享的內(nèi)存數(shù)據(jù)進行更改。那么子進程就可能會得到一些臟數(shù)據(jù)。
解決方法:
主進程執(zhí)行寫操作時,共享數(shù)據(jù)會改成只讀數(shù)據(jù)。且會拷貝一份數(shù)據(jù)去執(zhí)行主進程的寫操作。
2.AOF
AOF全稱為Append Only File(追加文件)Redis處理的每一個寫命令都會記錄在AOF文件,可以看做是命令日志文件。
bgrewriteaof命令
因為是記錄命令,AOF文件會比RDB文件大的多。而且aof會記錄對同一個key的多次寫操作,但只有最后一次寫操作才有意義。通過執(zhí)行bgrewriteaof命令,可以讓AOF文件執(zhí)行重寫功能,用最少的命令達到相同效果。
RDB與AOF對比
RDB和AOF各有自己的優(yōu)缺點,如果對數(shù)據(jù)安全性要求較高,在實際開發(fā)中往往會結(jié)合兩者來使用。
雙寫一致性
redis做為緩存,mysql的數(shù)據(jù)如何與redis進行同步呢? (雙寫一致性)
這個雙寫一致性最好要根據(jù)項目實際業(yè)務(wù)背景來說,一般分為兩種情況:
1.一致性要求高
2.允許延遲一致
1.一致性要求高
1.延遲雙刪
讀操作:緩存命中,直接返回;緩存未命中查詢數(shù)據(jù)庫,寫入緩存,設(shè)定超時時間
寫操作:延遲雙刪
1.無論第一步是先刪除緩存還是先修改數(shù)據(jù)庫都會導(dǎo)致臟數(shù)據(jù)的出現(xiàn)
所以重點是進行二次刪除緩存以及第二次刪除時做到延時刪除。
2.刪除兩次緩存的原因
如果在第一次刪除緩存和修改數(shù)據(jù)庫之間的時間里,另一個線程此時來查詢緩存了(未命中,查詢數(shù)據(jù)庫),那么此時寫入緩存的則是未更新的數(shù)據(jù)庫的數(shù)據(jù),為臟數(shù)據(jù)。如下圖所示:
所以在更新完數(shù)據(jù)庫后再次刪除緩存可以將這種情況下的臟數(shù)據(jù)盡量消除。
所以對緩存進行兩次刪除可以降低臟數(shù)據(jù)的出現(xiàn),但是不能杜絕。
3.延時刪除的原因
因為數(shù)據(jù)庫一般是主從模式的,讀寫分離了,主庫的數(shù)據(jù)同步到從庫需要一定的時間,故先要延遲一會。
問題:因為延遲的時間不好控制,所以還是可能會出現(xiàn)臟數(shù)據(jù)。
總結(jié)
延遲雙刪極大地控制了臟數(shù)據(jù)的風(fēng)險,但不可杜絕臟數(shù)據(jù)的風(fēng)險。
2.加讀寫鎖
能保證強一致性,但性能低。
強一致性的,采用redisson提供的讀寫鎖
共享鎖:讀鎖readLock,加鎖之后,其他線程可以共享讀操作
排他鎖:獨占寫鎖writeLock,加鎖之后,阻塞其他線程讀寫操作
2.允許延遲一致(較為主流)
能保證最終一致性,會有短暫延遲。
1.異步通知保證數(shù)據(jù)的最終一致性
2.基于Canal的異步通知
數(shù)據(jù)過期策略
Redis對有些數(shù)據(jù)設(shè)置有效時間,數(shù)據(jù)過期以后,就需要將數(shù)據(jù)從內(nèi)存中刪除掉。可以按照不同的規(guī)則進行刪除,這種刪除規(guī)則就被稱之為數(shù)據(jù)的刪除策略(數(shù)據(jù)過期策略)。
方案一惰性刪除:
惰性刪除,在設(shè)置該key過期時間后,我們不去管它,當(dāng)需要該key時,我們在檢查其是否過期,如果過期,我們就刪掉它,反之返回該key。
優(yōu)點:對CPU友好,只會在使用該key時才會進行過期檢查,對于很多用不到的key不用浪費時間進行過期檢查
缺點:對內(nèi)存不友好,如果一個key已經(jīng)過期。但是一直沒有使用,那么該key就會一直存在內(nèi)存中,內(nèi)存永遠不會釋放
方案二定期刪除:
定期刪除,就是說每隔一段時間,我們就對一些key進行檢查,刪除里面過期的key。
?
定期清理的兩種模式:
1.SLOW模式是定時任務(wù),執(zhí)行頻率默認為10hz,每次不超過25ms,以通過修改配置文件redis.conf的hz選項來調(diào)整這個次數(shù)
2.FAST模式執(zhí)行頻率不固定,每次事件循環(huán)會嘗試執(zhí)行,但兩次間隔不低于2ms,每次耗時不超過1ms
優(yōu)點:可以通過限制刪除操作執(zhí)行的時長和頻率來減少刪除操作對CPU的影響。另外定期劇除,也能有效釋放過期鍵占用的內(nèi)存。
缺點:難以確定刪除操作執(zhí)行的時長和頻率。
定期清理控制時長和頻率--->盡量少占用主進程的操作--->減少對CPU的影響
總結(jié)
Redis的過期刪除策略:情性刪除+定期刪除兩種策略進行配合使用。
數(shù)據(jù)淘汰策略
數(shù)據(jù)的淘汰策略:當(dāng)Redis中的內(nèi)存不夠用時,此時在向Redis中添加新的key,那么Redis就會按照某一種規(guī)則將內(nèi)存中的數(shù)據(jù)刪除掉,這種數(shù)據(jù)的刪除規(guī)則被稱之為內(nèi)存的淘汰策略。
Redis支持8種不同策略來選擇要刪除的key:
八種不同策略
noeviction:不淘汰任何key,但是內(nèi)存滿時不允許寫入新數(shù)據(jù),默認就是這種策略。
volatile-ttl: 對設(shè)置了TTL的key,比較key的剩余TTL值,TTL越小越先被淘汰。 allkeys-random:對全體key,隨機進行淘汰
volatile-random:對設(shè)置了TTL的key,隨機進行淘汰
allkeys-lru:對全體key,基于LRU算法進行淘汰
volatile-lru:對設(shè)置了TTL的key,基于LRU算法進行淘汰
allkeys-lfu:對全體key,基于LFU算法進行淘汰
volatile-lfu:對設(shè)置了TTL的key,基于LFU算法進行淘汰
LRU(Least Recently Used)算法:最近最少使用。用當(dāng)前時間減去最
后一次訪問時間,這個值越大則淘汰優(yōu)先級越高。
例:key1是在6s之前訪問的,key2是在9s之前訪問的,刪除的就是key2
LFU(Least Frequently Used)算法:最少頻率使用。會統(tǒng)計每個key的
訪問頻率,值越小淘汰優(yōu)先級越高。
例:key1最近5s訪問了6次,key2最近5s訪問了9次,刪除的就是key1
數(shù)據(jù)淘汰策略--使用建議
1.優(yōu)先使用alkeys-lru策略。充分利用LRU算法的優(yōu)勢,把最近最常訪問的數(shù)據(jù)留在緩存中。如果業(yè)務(wù)有明顯的冷熱數(shù)據(jù)區(qū)分,建議使用這種策略。
2.如果業(yè)務(wù)中數(shù)據(jù)訪問頻率差別不大,沒有明顯冷熱數(shù)據(jù)區(qū)分,建議使用allkeys-random,隨機選擇淘汰。
3.如果業(yè)務(wù)中有置頂?shù)男枨?#xff0c;可以使用volatile-lru策略,同時置頂數(shù)據(jù)不設(shè)置過期時間,這些數(shù)據(jù)就一直不被刪除
會淘汰其他設(shè)置過期時間的數(shù)據(jù)。
4.如果業(yè)務(wù)中有短時高頻訪問的數(shù)據(jù),可以使用allkeys-lfu 或volatile-lfu 策略。
常見問題:
1.數(shù)據(jù)庫有1000萬數(shù)據(jù),Redis只能緩存2ow數(shù)據(jù),如何保證Redis中的數(shù)據(jù)都是熱點數(shù)據(jù)?
使用allkeys-lru(挑選最近最少使用的數(shù)據(jù)淘汰)淘汰策略,留下來的都是經(jīng)常訪問的熱點數(shù)據(jù)
2.Redis的內(nèi)存用完了會發(fā)生什么?
默認的配置(noeviction):會直接報錯
分布式鎖
場景
通常情況下,分布式鎖使用的場景:
集群情況下的定時任務(wù),搶單搶券,秒殺,冪等性場景
引入與基本介紹
如果項目是單體項目,只啟動了一臺服務(wù),那遇到這類搶單問題時(防止超賣),可以加synchronized鎖解決。(解決多線程并發(fā)環(huán)境下的問題)
但是項目服務(wù)是集群部署的話,那么synchronized鎖這種本地鎖(只能保證單個JVM內(nèi)部的多個線程之間互斥,不能讓集群下的多個JVM下的多個線程互斥)(只對本服務(wù)器有效)會失效,需要使用外部鎖,也就是分布式鎖。
例1(搶券場景):
例2:
分布式鎖:滿足分布式系統(tǒng)或集群模式下多進程可見并且互斥的鎖。
實現(xiàn)分布式鎖的方式有很多,常見的有三種:
redis分布式鎖實現(xiàn)原理
基本介紹
設(shè)置超時失效時間的原因(避免死鎖):
如果某個線程拿到鎖在執(zhí)行業(yè)務(wù)時,服務(wù)器突然宕機,此時這個線程還沒來得及釋放鎖,而如果沒有設(shè)置過期時間的話,這個鎖就沒辦法得到釋放了,別的線程怎么也獲取不到這個鎖了,就造成了死鎖。而設(shè)置了過期時間的話,鎖到時間了就會自動釋放。
總結(jié)
缺陷
以上的問題是比較小的可能出現(xiàn)的,但是我們用Redis實現(xiàn)的分布式鎖去解決又顯得尤為困難,所以我們可以去使用Redisson框架,它底層提供了以上問題的解決方案,方便了我們?nèi)ソ鉀Q問題。
Redisson實現(xiàn)的分布式鎖
redisson是Redis的一個框架。
Redisson是一個在Redis的基礎(chǔ)上實現(xiàn)的Java駐內(nèi)存數(shù)據(jù)網(wǎng)格。它不僅提供了一系列的分布式的Java常用對象,還提供了許多分布式服務(wù),其中就包含了各種分布式鎖的實現(xiàn)。
快速入門:
在Redisson中需要手動加鎖,并且可以控制鎖的失效時間和等待時間,當(dāng)鎖住的一個業(yè)務(wù)還沒有執(zhí)行完成的時候,在redisson中引入了一個看門狗機制,就是說每隔一段時間就檢查當(dāng)前業(yè)務(wù)是否還持有鎖,如果持有就增加鎖的持有時間,當(dāng)業(yè)務(wù)執(zhí)行完成之后需要釋放鎖。
在高并發(fā)下,一個業(yè)務(wù)有可能會執(zhí)行很快,先客戶1持有鎖的時候,客戶2來了以后并不會馬上拒絕,它會自旋不斷嘗試獲取鎖(while循環(huán)獲取),如果客戶1釋放之后,客戶2就可以馬上持有鎖,性能也得到了提升。
1.redisson實現(xiàn)的分布式鎖的執(zhí)行流程/合理地控制鎖的有效時長(失效時間)
原因:如果鎖的有效時間設(shè)置的不合理,可能業(yè)務(wù)還沒執(zhí)行完鎖就釋放了,那此時其它線程來也可以獲取到鎖,就破壞了業(yè)務(wù)執(zhí)行的原子性,業(yè)務(wù)數(shù)據(jù)會受到影響。
方法:根據(jù)業(yè)務(wù)所需時間實時給鎖續(xù)期。
//可重試:利用信號量和PubSub功能實現(xiàn)等待,喚醒,獲取鎖失敗的重試機制。
releaseTime默認是30s。
另外開一個線程“看門狗”來監(jiān)視持有鎖的線程并做續(xù)期任務(wù)(每隔releaseTime/3的時間做一次續(xù)期)。
public void redislock() thr throws interruptedexception {//獲取鎖(重入鎖),執(zhí)行鎖的名稱RLock lock = redissonClient.getLock("lock");//嘗試獲取鎖,//參數(shù)分別是:獲取鎖的最大等待時間(期間會重試),鎖自動釋放時間(鎖失效時間),時間單位//boolean islock = lock.tryLock(10,30,TimeUnit.SECONDS);boolean isLock = lock.tryLock(10,TimeUnit.SECONDS);//參數(shù):1.鎖的最大等待時間:鎖通過while循環(huán)來不斷嘗試獲取鎖的最大等待時間,如果這個時間內(nèi)沒有獲取到鎖則放棄獲取鎖。// 2.鎖自動釋放時間:最好不要設(shè)置或者設(shè)置為-1,否則不會啟動看門狗線程進行續(xù)期任務(wù)。// 3.時間單位//加鎖,釋放鎖,設(shè)置過期時間,給鎖續(xù)期等操作都是基于lua腳本完成。//Lua腳本可以調(diào)用Redis命令來保證多條命令執(zhí)行的原子性。//判斷是否獲取成功 if(isLock){try{System.out.println("執(zhí)行業(yè)務(wù)");} finally {//釋放鎖lock.unlock();}}
}
原子性問題:
Redis提供了Lua腳本功能,在一個腳本中編寫多條Redis命令,確保多條命令執(zhí)行時的原子性。
2.可重入
Redis實現(xiàn)的鎖是不可重入的,但redisson實現(xiàn)的鎖是可重入的。
作用:避免死鎖的產(chǎn)生。
這個重入其實在內(nèi)部就是判斷是否是當(dāng)前線程持有的鎖。如果是當(dāng)前線程持有的鎖就會計數(shù),如果釋放鎖就會在計算上減一。
存儲數(shù)據(jù)的時候采用的hash結(jié)構(gòu),大key可以按照自己的業(yè)務(wù)進行定制,其中小key是當(dāng)前線程的唯一標(biāo)識(線程id),value是當(dāng)前線程重入的次數(shù)。
public void add1(){RLock lock = redissonClient.getLock("heimalock");boolean islock = lock.tryLock();//執(zhí)行業(yè)務(wù)add2();//釋放鎖lock.unlock();
}
public void add2(){RLock lock = redissonClient.getLock("heimalock");boolean islock = lock.trylock();//執(zhí)行業(yè)務(wù)...//釋放鎖lock.unlock();
}
底層獲取鎖和釋放鎖等操作都很復(fù)雜,都是有多個步驟,所以是用Lua腳本寫確保各個操作的原子性。
3.主從一致性
redisson實現(xiàn)的分布式鎖不能解決主從一致性問題。
比如,當(dāng)線程1加鎖成功后,Master節(jié)點數(shù)據(jù)會異步復(fù)制到Slave節(jié)點,當(dāng)數(shù)據(jù)還沒來得及同步到Slave節(jié)點時,當(dāng)前持有Redis鎖的Master節(jié)點宕機,Slave節(jié)點被提升為新的Master節(jié)點。(按道理主節(jié)點和從節(jié)點的數(shù)據(jù)應(yīng)該要是一模一樣的,加鎖的信息也要一模一樣(其實就是一個setnx數(shù)據(jù)而已))
假如現(xiàn)在來了一個線程2,再次加鎖,因為Master節(jié)點數(shù)據(jù)還沒來得及同步過來(從節(jié)點已經(jīng)被這把鎖鎖住且線程一已經(jīng)拿到了這把鎖的信息還未更新過來),所以會在新的Master節(jié)點上加鎖成功,這個時候就會出現(xiàn)兩個線程同時持有一把鎖的問題。
兩個線程同時獲取一把鎖--->違背了鎖的互斥性(鎖失效了)。
紅鎖:
紅鎖算法的基本思想是,當(dāng)需要鎖定多個資源時,可以在多個Redis節(jié)點上分別獲取鎖,只有當(dāng)大多數(shù)節(jié)點上
的鎖都被成功獲取時,整個鎖才算獲取成功。這樣可以提高系統(tǒng)的容錯性和可用性。
我們可以利用Redisson提供的紅鎖來解決這個問題,它的主要作用是,不能只在一個redis實例上創(chuàng)建鎖,應(yīng)該是在多個redis實例上創(chuàng)建鎖,并且要求在大多數(shù)Redis節(jié)點上都成功創(chuàng)建鎖,紅鎖中要求是Redis的節(jié)點數(shù)量要過半。這樣就能避免線程1加鎖成功后Master節(jié)點宕機導(dǎo)致線程2成功加鎖到新的Master節(jié)點上的問題了。
意思就是線程來的時候要獲取多個Redis節(jié)點的鎖才算成功,才可以執(zhí)行代碼。
如果一個主節(jié)點宕機(主節(jié)點的數(shù)據(jù)還沒來得及同步到從節(jié)點,與以上同理),它的從節(jié)點變成主節(jié)點,那么此時另一個線程來是不可以獲取到鎖的,因為這個線程必須要獲取到所有的節(jié)點的鎖才能成功獲取到鎖,它只能拿到宕機的那個主節(jié)點的從節(jié)點的鎖(因為主節(jié)點的數(shù)據(jù)還沒來得及同步到從節(jié)點),所以會獲取鎖失敗。
只要有一個節(jié)點是存活的,其它線程就不可以拿到鎖,鎖就不會失效。
缺點
如果使用了紅鎖,因為需要同時在多個節(jié)點上都添加鎖,性能就變的很低了,并且運維維護成本也非常高,所以,我們一般在項目中也不會直接使用紅鎖,并且官方也暫時廢棄了這個紅鎖。
所以強一致性要求高的業(yè)務(wù),建議使用zookeeper實現(xiàn)的分布式鎖,它是可以保證強一致性的。
Redis發(fā)布訂閱
- Redis 發(fā)布訂閱(pub/sub)是一種消息通信模式: 發(fā)送者(pub)發(fā)送消息,訂閱者(sub)接受消息.微博,微信,關(guān)注系統(tǒng)
- Redis 客戶端可以訂閱任意數(shù)量的頻道。
訂閱/發(fā)布消息圖
- 第一個: 消息發(fā)送者, 第二個 :頻道 第三個 :消息訂閱者!
- 下圖展示了頻道 channel1 , 以及訂閱這個頻道的三個客戶端 —— client2 、 client5 和 client1 之間的關(guān)系:
- 當(dāng)有新消息通過 PUBLISH 命令發(fā)送給頻道 channel1 時, 這個消息就會被發(fā)送給訂閱它的三個客戶端:
命令
- 這些命令被廣泛用于構(gòu)建即時通信應(yīng)用,比如網(wǎng)絡(luò)聊天室和實時廣播,實時提醒
測試
- 訂閱端(消費者)
-
- 開啟客戶端1
-
127.0.0.1:6379> subscribe codeyuaiiao //訂閱一個頻道,頻道名稱:codeyuaiiao 訂閱的時候頻道就建立了
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "codeyuaiiao"
3) (integer) 1# 等待讀取推送(客戶端2發(fā)送消息,客戶端1這邊接收消息)
1) "message" # 消息
2) "codeyuaiiao" # 消息來自哪個頻道
3) "hello world" # 消息的具體內(nèi)容1) "message"
2) "codeyuaiiao"
3) "hello yuhaijiao"
- 發(fā)送端:(生產(chǎn)者)
-
- 再開啟一個客戶端2
-
127.0.0.1:6379> publish codeyuaiiao "hello world" # 發(fā)布者發(fā)布消息到頻道
(integer) 1
127.0.0.1:6379> publish codeyuaiiao "hello yuhaijiao" # 發(fā)布者發(fā)布消息到頻道
(integer) 1
127.0.0.1:6379>
Redis發(fā)布訂閱原理
- Redis是使用C實現(xiàn)的,通過分析 Redis 源碼里的 pubsub.c 文件,了解發(fā)布和訂閱機制的底層實現(xiàn),籍此加深對 Redis 的理解。
- Redis 通過 PUBLISH(發(fā)送消息) 、SUBSCRIBE(訂閱頻道) 和 PSUBSCRIBE(訂閱多個頻道) 等命令實現(xiàn)發(fā)布和訂閱功能。
例如微信訂閱公眾號:
- 通過 SUBSCRIBE 命令訂閱某頻道后
-
- Redis-server 里維護了一個字典
- 字典的鍵就是一個個頻道
- 字典的值則是一個鏈表,鏈表中保存了所有訂閱這個 channel 的客戶端
- SUBSCRIBE 命令的關(guān)鍵, 就是將客戶端添加到給定 channel 的訂閱鏈表中
- 通過publish命令向訂閱者發(fā)送消息,redis-server會使用給定的頻道作為鍵,在它所維護的channel字典中查找記錄了訂閱這個頻道的所有客戶端的鏈表,遍歷這個鏈表,將消息發(fā)布給所有訂閱者.
總結(jié)
Pub/Sub 從字面上理解就是發(fā)布(Publish)與訂閱(Subscribe),在Redis中,你可以設(shè)定對某一個key值進行消息發(fā)布及消息訂閱,當(dāng)一個key值上進行了消息發(fā)布后,所有訂閱它的客戶端都會收到相應(yīng)的消息。這一功能最明顯的用法就是用作實時消息系統(tǒng),比如普通的即時聊天,群聊等功能。
使用場景
- 實時消息系統(tǒng)
- 實時聊天! (頻道當(dāng)做聊天室,將信息回顯給所有人即可! )
- 訂閱,關(guān)注系統(tǒng)都是可以的
稍微復(fù)雜的場景我們就會使用 消息中間件MQ
Redis消息隊列
概念
消息隊列(Message Queue),字面意思就是存放消息的隊列。最簡單的消息隊列模型包括3個角色:
消息隊列:存儲和管理消息,也被稱為消息代理(message broker)
生產(chǎn)者:發(fā)送消息到消息隊列
消費者:從消息隊列獲取消息并處理消息
例(秒殺搶券業(yè)務(wù)):
生產(chǎn)者:判斷是否有資格搶券(券的剩余數(shù)量大于0且當(dāng)前用戶之前未搶到券),如果有資格則將訂單相關(guān)信息寫入消息隊列。
消費者:開啟一個獨立的線程去接收消息,完成下單(把訂單信息寫入Mysql數(shù)據(jù)庫)
這樣秒殺搶單的業(yè)務(wù)和真正寫數(shù)據(jù)庫的業(yè)務(wù)就實現(xiàn)了分離,變成了異步操作,解耦合了。
秒殺搶單的業(yè)務(wù):秒殺這里因為不用寫數(shù)據(jù)庫(比較耗時),并發(fā)能力大大提高。
寫數(shù)據(jù)庫的業(yè)務(wù):可以根據(jù)自己的節(jié)奏慢慢地去取訂單寫數(shù)據(jù)庫,不會讓數(shù)據(jù)庫有太大的壓力,保證數(shù)據(jù)庫抗得住。
Redis提供了三種不同的方式來實現(xiàn)消息隊列:
list結(jié)構(gòu):基于List結(jié)構(gòu)模擬消息隊列
PubSub:基本的點對點消息模型
Stream:比較完善的消息隊列模型
基于List結(jié)構(gòu)模擬消息隊列(可實現(xiàn)阻塞隊列的效果)
支持持久化:因為list類型redis本身是用鏈表做存儲數(shù)據(jù)的,只是我們把它當(dāng)成消息隊列來用,故對數(shù)據(jù)可以持久化
基于PubSub(發(fā)布訂閱)的消息隊列
主要內(nèi)容就是上面學(xué)習(xí)的Redis發(fā)布訂閱
優(yōu)點:
采用發(fā)布訂閱模型,支持多生產(chǎn),多消費
缺點:
不支持?jǐn)?shù)據(jù)持久化
無法避免消息丟失
消息堆積有上限,超出時數(shù)據(jù)丟失
不支持持久化:因為PubSub本身就只是用來做發(fā)布訂閱功能的,如果沒有人訂閱某個頻道,那么往這個頻道發(fā)布數(shù)據(jù)后,數(shù)據(jù)會丟失,Redis不會保存這個數(shù)據(jù)。
基于Stream的消息隊列
基本知識
Stream是Redis 5.0引入的一種新數(shù)據(jù)類型,可以實現(xiàn)一個功能非常完善的消息隊列。
例:
注意
當(dāng)我們指定起始id為$時,代表讀取最新的消息,如果我們處理一條消息的過程中,又有超過一條以上的消息到達隊列,則下次獲取時也只能獲取到最新的一條,會出現(xiàn)漏讀消息的問題。
Stream類型消息隊列的XREAD命令特點:
1.消息可回溯
2.一個消息可以被多個消費者讀取
3.可以阻塞讀取
4.有消息漏讀的風(fēng)險
消費者組
消費者組:將多個消費者劃分到一個組中,監(jiān)聽同一個隊列。
具備下列特點:
Stream類型消息隊列的XREADGROUP命令特點:
消息可回溯
可以多消費者爭搶消息,加快消費速度
可以阻塞讀取
沒有消息漏讀的風(fēng)險
有消息確認機制,保證消息至少被消費一次
Redis集群(分布式緩存)
單點Redis的問題
1.并發(fā)能力問題
解決方法:搭建主從集群,實現(xiàn)讀寫分離。實現(xiàn)高并發(fā)。
2.故障恢復(fù)問題
解決方法:利用Redis哨兵,實現(xiàn)健康檢測和自動恢復(fù)。保障高可用。
3.存儲能力問題
解決方法:搭建分片集群,利用插槽機制實現(xiàn)動態(tài)擴容。
主從復(fù)制
單節(jié)點Redis的并發(fā)能力是有上限的,要進一步提高Redis的并發(fā)能力,就需要搭建主從集群,實現(xiàn)讀寫分離。
主從數(shù)據(jù)同步原理
1.主從全量同步
注:
1.判斷是否第一次同步:
從節(jié)點的replid與主節(jié)點的不一樣則說明這個從節(jié)點是第一次同步。
2.只有第一次同步的時候主節(jié)點才會生成RDB文件,第一次之后的同步會根據(jù)偏移量利用repl_baklog日志文件進行同步數(shù)據(jù)。
2.主從增量同步(slave重啟或后期數(shù)據(jù)變化)
3.總結(jié)
全量同步:
1.從節(jié)點請求主節(jié)點同步數(shù)據(jù)(replication id, offset)
2.主節(jié)點判斷是否是第一次請求,是第一次就與從節(jié)點同步版本信息(replicationid和offset)
3.主節(jié)點執(zhí)行bgsave,生成rdb文件后,發(fā)送給從節(jié)點去執(zhí)行
4.在rdb生成執(zhí)行期間,主節(jié)點會以命令的方式記錄到緩沖區(qū)(一個日志文件)
5.把生成之后的命令日志文件發(fā)送給從節(jié)點進行同步
增量同步:
1.從節(jié)點請求主節(jié)點同步數(shù)據(jù),主節(jié)點判斷不是第一次請求,不是第一次就獲取從節(jié)點的offset值
2.主節(jié)點從命令日志中獲取offset值之后的數(shù)據(jù),發(fā)送給從節(jié)點進行數(shù)據(jù)同步
哨兵模式
redis提供了哨兵模式來實現(xiàn)主從集群的自動故障恢復(fù),從而極大地保障了Redis主從的高可用。
哨兵模式的結(jié)構(gòu)與作用
redis提供了哨兵 (Sentinel)機制來實現(xiàn)主從集群的自動故障恢復(fù)。
結(jié)構(gòu):
作用:
監(jiān)控:Sentinel會不斷檢查您的master和slave是否按預(yù)期工作
自動故障恢復(fù):如果master故障, Sentinel會將一個slave提升為master。當(dāng)故障實例恢復(fù)后也以新的master為主
通知:Sentinel充當(dāng)redis客戶端的服務(wù)發(fā)現(xiàn)來源,當(dāng)集群發(fā)生故障轉(zhuǎn)移時,會將最新信息推送給redis的客戶端
服務(wù)狀態(tài)監(jiān)控
Sentinel基于心跳機制監(jiān)測服務(wù)狀態(tài),每隔1秒向集群的每個實例發(fā)送ping命令:
主觀下線:如果某sentinel節(jié)點發(fā)現(xiàn)某實例未在規(guī)定時間響應(yīng),則認為該實例主觀下線。
客觀下線:若超過指定數(shù)量(quorum)的sentinel都認為該實例主觀下線,則該實例客觀下線。quorum值最好
超過sentinel實例數(shù)量的一半。
哨兵選主規(guī)則(主節(jié)點宕機后,選從節(jié)點為主節(jié)點的規(guī)則)
1.首先判斷主與從節(jié)點斷開時間長短,如超過指定值就排除該從節(jié)點
2.然后判斷從節(jié)點的slave-priority值,越小優(yōu)先級越高
3.如果slave-prority一樣,則判斷slave節(jié)點的offset值,越大優(yōu)先級越高.
4.最后是判斷slave節(jié)點的運行id大小,越小優(yōu)先級越高。
第三條最重要!!!
Redis集群(哨兵模式)的腦裂問題
有的時候由于網(wǎng)絡(luò)等原因可能會出現(xiàn)腦裂的情況,就是說,由于redis的master節(jié)點和redis的salve節(jié)點和sentinel處于不同的網(wǎng)絡(luò)分區(qū),使得sentinel沒有能夠心跳感知到主節(jié)點,所以通過選舉的方式提升了一個salve為master,這樣就存在了兩個master,就像大腦分裂了一樣,這樣會導(dǎo)致客戶端還在old master那里寫入數(shù)據(jù),新節(jié)點無法同步數(shù)據(jù),當(dāng)網(wǎng)絡(luò)恢復(fù)后,sentinel會將old master降為salve,這時再從新master同步數(shù)據(jù),就會導(dǎo)致old master中的大量數(shù)據(jù)丟失。
----------->
------------>
解決方法
在redis的配置中設(shè)置兩個配置參數(shù)
1.(min-replicas-to-write 1)設(shè)置最少的salve節(jié)點個數(shù)為1,設(shè)置至少要有一個從節(jié)點才能同步數(shù)據(jù)
2.(min-replicas-max-lag 5)設(shè)置主從數(shù)據(jù)復(fù)制和同步的延遲時間不能超過5秒
達不到要求就拒絕請求,就可以避免大量的數(shù)據(jù)丟失。
總結(jié):
我們可以修改redis的配置,可以設(shè)置最少的從節(jié)點數(shù)量至少為一個以及縮短主從數(shù)據(jù)同步的延遲時間(不能超過5秒),達不到要求就拒絕Redis客戶端的請求(不讓客戶端寫入數(shù)據(jù)到老的主節(jié)點),這樣就可以避免大量的數(shù)據(jù)丟失。
分片集群
主從和哨兵可以解決高可用,高并發(fā)讀的問題。但是依然有兩個問題沒有解決;
1.海量數(shù)據(jù)存儲問題
2.高并發(fā)寫的問題
使用分片集群可以解決上述問題,分片集群特征:
1.集群中有多個master,每個master保存不同數(shù)據(jù)
2.每個master都可以有多個slave節(jié)點
3.master之間通過ping監(jiān)測彼此健康狀態(tài) (這點類似于之前的哨兵模式)
4.客戶端請求可以訪問集群任意節(jié)點,最終都會被轉(zhuǎn)發(fā)到正確節(jié)點 (路由:客戶端請求可以訪問集群任意節(jié)點,最終都會被轉(zhuǎn)發(fā)到正確節(jié)點。)
具體的路由規(guī)則:
Redis分片集群引入了哈希槽的概念,Redis集群有16384個哈希槽,每個key通過 CRC16 校驗后對 16384 取模來
決定放置哪個槽,集群的每個節(jié)點負責(zé)一部分hash槽。
Redis分片集群中數(shù)據(jù)的存儲和讀取:
redis分片集群引入了哈希槽的概念,redis集群有16384個哈希槽,將16384個插槽分配到不同的實例
讀寫數(shù)據(jù):根據(jù)key的有效部分計算哈希值。對16384取余(有效部分,如果key前面有大括號的
內(nèi)容就是有效部分,如果沒有,則以key本身做為有效部分)余數(shù)做為插槽,尋找插槽所在的實例
redis集群環(huán)境部署(環(huán)境配置)
只配置從庫,不用配置主庫!
- 原因:redis默認都是主庫
查看當(dāng)前redis庫的信息,分析是否是主庫
- 命令:info replication
127.0.0.1:6379> info replication # 查看當(dāng)前庫的信息
# Replication
role:master # 角色 master
master connected_slaves:0 # 沒有從機
master_replid:b63c90e6c501143759cb0e7f450bd1eb0c70882a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
搭建redis集群準(zhǔn)備工作
- 復(fù)制3個redis.conf配置文件,然后修改對應(yīng)的集群信息
- 分別修改3個redis.conf對應(yīng)的以下4個屬性配置
- port端口修改
- pid名字
- log文件名字
- dump.rdb名字
- 修改完畢之后,啟動我們的3個redis服務(wù)器
-
- 分別啟動3個redis服務(wù)命令
- 啟動完畢,通過進程查看信息
一主二從集群搭建(命令或文件配置)(這種方式的redis集群實際工作用不到,僅供基礎(chǔ)學(xué)習(xí))
命令方式配置
默認情況下, 每臺Redis 服務(wù)器都是主節(jié)點?; 我們一般情況下只用配置從機就好了!
- 認老大! 一主 (6379)二從(6380,6381)
- 配置從機,去6380和6381配置,命令:
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 # SLAVEOF host 6379 登錄6380找6379主機當(dāng)自己的老大!
OK
127.0.0.1:6380> info replication # 查詢信息redis6380的服務(wù)信息
role:slave # 查看當(dāng)前角色是從機
master_host:127.0.0.1 # 可以的看到主機的信息和端口號
master_port:6379
- 配置結(jié)果查詢:6380,6381的角色role變成了slave從機
- 如果兩個都配置完了,就有兩個從機了
-
- 登錄6379主機,查看主機下的兩個從節(jié)點
- 真實的主從配置應(yīng)該在配置文件中配置,這樣的話是永久的, 我們這里使用的是命令,暫時的!
文件方式配置(一主二從,持久化的,對于哨兵模式,不建議使用這種)
- 登錄redis從機,進入redis.conf配置文件配置replicaof
- 如果主機有密碼,則配置主機密碼
一主二從細節(jié)
- 主機可以寫, 從機不能寫只能讀! 主機中的所有信息和數(shù)據(jù),都會自動被從機保存.
測試主機寫,從機讀
- 主機寫:
- 從機讀:
- 從機寫,會報錯
測試: 主機宕機斷開連接,從機會有什么變化
-
- 從機沒有變化,依然指向主機,并且只能讀不能寫
- 如果想把從機改為主機,只能手動去設(shè)置,或者配置哨兵通過選舉,將從機變?yōu)橹鳈C
測試2:這個時候, 主機如果回來了,從機有什么變化
- 從機依舊可以直接獲取到主機寫的信息!保證高可用性
測試3:如果從機斷了,會有什么后果
- 由于是使用命令行來配置的從機,這個時候如果從機重啟了,就會變成主機 (所以建議在redis.conf配置文件中配置從機)!
- 但只要重新將主機變?yōu)閺臋C, 立馬就會從主機中獲取值!
主從復(fù)制原理
- Slave啟動成功連接到master后會發(fā)送一個sync同步命令
- Master接到命令,啟動后臺的存盤進程,同時收集所有接收到的用于修改數(shù)據(jù)集命令, 在后臺進程完畢之后,master將傳送整個數(shù)據(jù)文件到slave,并完成一次完全同步.
- 全量復(fù)制: 而slave服務(wù)在接收到數(shù)據(jù)庫文件數(shù)據(jù)后, 將其存盤并加載到內(nèi)存中.
- 增量復(fù)制: Master繼續(xù)將新的所有收集到的修改命令依次傳給slave,完成同步.
- 但是只要是重新連接master, 一次完全同步(全量復(fù)制)將被自動執(zhí)行! 我們的數(shù)據(jù)一定可以在從機中看到!
一主兩從的第二種搭建方式(層層鏈路)哨兵模式的手動版
層層鏈路
- 79是主節(jié)點
- 80是79的從節(jié)點
- 81是79的從節(jié)點
- 上一個M連接下一個S!
- 這時候也可以完成我們的主從復(fù)制!
如果沒有老大了,這個時候能不能選擇一個老大出來呢? 手動!
- 謀朝篡位
-
- 如果主機斷開了連接, 我們可以使用
slaveof no one
?讓自己變成主機! 其他的節(jié)點就可以手動連接到最新的這個主節(jié)點(手動)! 如果這個時候老大修復(fù)了, 那就只能重新配置連接! - 所以建議使用命令配置集群,方便將從節(jié)點改為主節(jié)點后,不用在去改配置文件
- 如果主機斷開了連接, 我們可以使用
I/O多路復(fù)用模型
redis為什么這么快
用戶空間和內(nèi)核空間
Linux系統(tǒng)中一個進程使用的內(nèi)存情況劃分兩部分:內(nèi)核空間,用戶空間。
用戶空間只能執(zhí)行受限的命令(Ring3),而且不能直接調(diào)用系統(tǒng)資源(比如網(wǎng)卡數(shù)據(jù)),必須通過內(nèi)核提供的接口來訪問。
內(nèi)核空間可以執(zhí)行特權(quán)命令(Ring0),調(diào)用一切系統(tǒng)資源。
Linux系統(tǒng)為了提高IO效率,會在用戶空間和內(nèi)核空間都加入緩沖區(qū):
寫數(shù)據(jù)時,要把用戶緩沖數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū),然后寫入設(shè)備
讀數(shù)據(jù)時,要從設(shè)備讀取數(shù)據(jù)到內(nèi)核緩沖區(qū),然后拷貝到用戶緩沖區(qū)
如圖所示
阻塞IO
顧名思義,阻塞IO就是兩個階段都必須阻塞等待
階段一:
1.用戶進程嘗試讀取數(shù)據(jù)(比如網(wǎng)卡數(shù)據(jù))
2.此時數(shù)據(jù)尚未到達,內(nèi)核需要等待數(shù)據(jù)
3.此時用戶進程也處于阻塞狀態(tài)
階段二:
1.數(shù)據(jù)到達并拷貝到內(nèi)核緩沖區(qū),代表已就緒
2.將內(nèi)核數(shù)據(jù)拷貝到用戶緩沖區(qū)
3.拷貝過程中,用戶進程依然阻塞等待
4.拷貝完成,用戶進程解除阻塞,處理數(shù)據(jù)
可以看到,阻塞IO模型中,用戶進程在兩個階段都是阻塞狀態(tài)。
非阻塞IO
顧名思義,非阻塞IO的recvfrom操作會立即返回結(jié)果而不是阻塞用戶進程。
階段一:
1.用戶進程嘗試讀取數(shù)據(jù)(比如網(wǎng)卡數(shù)據(jù))
2.此時數(shù)據(jù)尚未到達,內(nèi)核需要等待數(shù)據(jù)
3.返回異常給用戶進程
4.用戶進程拿到error后,再次嘗試讀取
5.循環(huán)往復(fù),直到數(shù)據(jù)就緒
階段二:
將內(nèi)核數(shù)據(jù)拷貝到用戶緩沖區(qū)
拷貝過程中,用戶進程依然阻塞等待
拷貝完成,用戶進程解除阻塞,處理數(shù)據(jù)
可以看到,非阻塞IO模型中,用戶進程在第一個階段是非阻露,第二個階段是阻塞狀態(tài)。雖然是非阻塞,但性能并沒有得到提高。而且忙等機制會導(dǎo)致CPU空轉(zhuǎn),CPU使用率暴增。
IO多路復(fù)用
Redis網(wǎng)絡(luò)模型
Redis通過IO多路復(fù)用來提高網(wǎng)絡(luò)性能,并且支持各種不同的多路復(fù)用實現(xiàn),并且將這些實現(xiàn)進行封裝,提供了統(tǒng)一的高性能事件庫。
主要是IO多路復(fù)用+事件派發(fā)機制:
Redis 6.0之后,為了提升性能,引入了多線程處理: