1. 程式人生 > 資料庫 >Redis進階(二)個人參考使用

Redis進階(二)個人參考使用

分散式快取架構-Redis(二)

Redis釋出訂閱

Redis 釋出訂閱(pub/sub)是一種訊息通訊模式:傳送者(pub)傳送訊息,訂閱者(sub)接收訊息。

Redis 客戶端可以訂閱任意數量的頻道。

下圖展示了頻道 channel1 , 以及訂閱這個頻道的三個客戶端 —— client2 、 client5 和 client1 之間的關係:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-bf5D6wgG-1607954996201)(img\1.png)]

當有新訊息通過 PUBLISH 命令傳送給頻道 channel1 時, 這個訊息就會被髮送給訂閱它的三個客戶端:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-Qy5ahu3Z-1607954996203)(img\2.png)]

訂閱訊息命令

SUBSCRIBE channel1

釋出訊息命令

PUBLISH channel1 hello

使用Stream完成訊息釋出與消費

Stream釋出和消費的的流程

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-fHiFvS4M-1607954996205)(img\image-20200812051912502.png)]

使用到的指令

  • XADD:建立Stream
  • XGROUP:是用來建立刪除和管理消費組的
  • XREADGROUP:是用來通過消費組從Stream裡讀訊息的
  • XACK:是用來確認訊息已經消費的

建立Stream

xadd streamkey * field val
# 為了防止訊息太長可以使用maxlen限制訊息長度
# xadd streamkey maxlen 1000 * field val

建立消費組

# 消費組1
xgroup create streamkey g1 0-0
# 消費組2
xgroup create streamkey g2 $
# 檢視消費組資訊
xinfo groups streamkey
# consumers 該消費組消費者數量
# pending 該消費組正在處理的訊息的數量

消費訊息

# 消費者1
xreadgroup GROUP g1 c1 count 1 streams streamkey >
# 消費者2
xreadgroup GROUP g1 c2 count 1 streams streamkey >
# >號表示從當前消費組的last_delivered_id後面開始讀
# 這裡任然可是使用 block 0 進行阻塞等待

#立即確認訊息
xack streamkey g1 ID ...

# 觀察消費組指定消費者
xinfo consumers streamkey g1
# name
# pending
# idle 空閒了多長時間ms沒有讀取訊息了
# 訊息確認
# 獲取消費歷史
XREADGROUP GROUP g1 c1 STREAMS streamkey 0
# 修正組的時間戳
XGROUP SETID streamkey g1 1591840117074-0
# 確認訊息
xack streamkey g1 ID ...

Redis持久化

什麼是Redis持久化

就是將記憶體資料儲存到硬碟。

Redis 持久化儲存 (AOF 與 RDB 兩種模式)

RDB持久化

RDB 是以二進位制檔案,是在某個時間點將資料寫入一個臨時檔案,持久化結束後,用這個臨時檔案替換上次持久化的檔案,達到資料恢復。
***優點:***使用單獨子程序來進行持久化,主程序不會進行任何 IO 操作,保證了 redis 的高效能
缺點:

  • RDB 是間隔一段時間進行持久化,如果持久化之間 redis 發生故障,會發生資料丟失。
  • 子程序需要開銷和主程序相同的記憶體完成資料儲存,可能會導致記憶體溢位。

所以這種方式更適合資料要求不嚴謹的時候。

這個執行資料寫入到臨時檔案的時間點是通過配置確定的,通過配置redis 在 n 秒內如果超過 m 個 key 被修改這執行一次 RDB 操作。這個操作就類似於在這個時間點來儲存一次 Redis 的所有資料,一次快照資料。所有這個持久化方法也通常叫做快照(snapshots)。

RDB 預設開啟,redis.conf 中的具體配置引數如下:

#dbfilename:持久化資料儲存在本地的檔案
dbfilename dump.rdb
#dir:持久化資料儲存在本地的路徑,如果是在/redis/redis-3.0.6/src下啟動的redis-cli,則資料會儲存在當前src目錄下
dir ./
##snapshot觸發的時機,save
##如下為900秒後,至少有一個變更操作,才會snapshot
##對於此值的設定,需要謹慎,評估系統的變更操作密集程度
##可以通過“save “””來關閉snapshot功能
#save時間,以下分別表示更改了1個key時間隔900s進行持久化儲存;更改了10個key300s進行儲存;更改10000個key60s進行儲存。
save 900 1
save 300 10
save 60 10000
##當snapshot時出現錯誤無法繼續時,是否阻塞客戶端“變更操作”,“錯誤”可能因為磁碟已滿/磁碟故障/OS級別異常等
stop-writes-on-bgsave-error yes
##是否啟用rdb檔案壓縮,預設為“yes”,壓縮往往意味著“額外的cpu消耗”,同時也意味這較小的檔案尺寸以及較短的網路傳輸時間
rdbcompression yes

AOF持久化

Append-only file,將“操作 + 資料”以格式化指令的方式追加到操作日誌檔案的尾部,在 append 操作返回後(已經寫入到檔案或者即將寫入),才進行實際的資料變更,“日誌檔案”儲存了歷史所有的操作過程;當 server 需要資料恢復時,可以直接 replay 此日誌檔案,即可還原所有的操作過程。AOF 相對可靠,它和 mysql 中 bin.log、apache.log、zookeeper 中 txn-log 簡直異曲同工。AOF 檔案內容是字串,非常容易閱讀和解析。
***優點:***可以保持更高的資料完整性,如果設定追加 file 的時間是 1s,如果 redis 發生故障,最多會丟失 1s 的資料;且如果日誌寫入不完整支援 redis-check-aof 來進行日誌修復;AOF 檔案沒被 rewrite 之前(檔案過大時會對命令進行合併重寫),可以刪除其中的某些命令(比如誤操作的 flushall)。
***缺點:***AOF 檔案比 RDB 檔案大,且恢復速度慢。

我們可以簡單的認為 AOF 就是日誌檔案,此檔案只會記錄“變更操作”(例如:set/del 等),如果 server 中持續的大量變更操作,將會導致 AOF 檔案非常的龐大,意味著 server 失效後,資料恢復的過程將會很長;事實上,一條資料經過多次變更,將會產生多條 AOF 記錄,其實只要儲存當前的狀態,歷史的操作記錄是可以拋棄的;因為 AOF 持久化模式還伴生了“AOF rewrite”。
AOF 的特性決定了它相對比較安全,如果你期望資料更少的丟失,那麼可以採用 AOF 模式。如果 AOF 檔案正在被寫入時突然 server 失效,有可能導致檔案的最後一次記錄是不完整,你可以通過手工或者程式的方式去檢測並修正不完整的記錄,以便通過 aof 檔案恢復能夠正常;同時需要提醒,如果你的 redis 持久化手段中有 aof,那麼在 server 故障失效後再次啟動前,需要檢測 aof 檔案的完整性。

AOF 預設關閉,開啟方法,修改配置檔案 reds.conf:appendonly yes

##此選項為aof功能的開關,預設為“no”,可以通過“yes”來開啟aof功能  
##只有在“yes”下,aof重寫/檔案同步等特性才會生效  
appendonly yes  

##指定aof檔名稱
appendfilename appendonly.aof  

##指定aof操作中檔案同步策略,有三個合法值:always everysec no,預設為everysec  
appendfsync everysec  
##在aof-rewrite期間,appendfsync是否暫緩檔案同步,"no"表示“不暫緩”,“yes”表示“暫緩”,預設為“no”  
no-appendfsync-on-rewrite no  

##aof檔案rewrite觸發的最小檔案尺寸(mb,gb),只有大於此aof檔案大於此尺寸是才會觸發rewrite,預設“64mb”,建議“512mb”  
auto-aof-rewrite-min-size 64mb  

##相對於“上一次”rewrite,本次rewrite觸發時aof檔案應該增長的百分比。  
##每一次rewrite之後,redis都會記錄下此時“新aof”檔案的大小(例如A),那麼當aof檔案增長到A*(1 + p)之後  
##觸發下一次rewrite,每一次aof記錄的新增,都會檢測當前aof檔案的尺寸。  
auto-aof-rewrite-percentage 100  

AOF 是檔案操作,對於變更操作比較密集的 server,那麼必將造成磁碟 IO 的負荷加重;此外 linux 對檔案操作採取了“延遲寫入”手段,即並非每次 write 操作都會觸發實際磁碟操作,而是進入了 buffer 中,當 buffer 資料達到閥值時觸發實際寫入(也有其他時機),這是 linux 對檔案系統的優化,但是這卻有可能帶來隱患,如果 buffer 沒有重新整理到磁碟,此時物理機器失效(比如斷電),那麼有可能導致最後一條或者多條 aof 記錄的丟失。通過上述配置檔案,可以得知 redis 提供了 3 中 aof 記錄同步選項:

  • always:每一條 aof 記錄都立即同步到檔案,這是最安全的方式,也以為更多的磁碟操作和阻塞延遲,是 IO 開支較大。

  • everysec:每秒同步一次,效能和安全都比較中庸的方式,也是 redis 推薦的方式。如果遇到物理伺服器故障,有可能導致最近一秒內 aof 記錄丟失(可能為部分丟失)。

  • no:redis 並不直接呼叫檔案同步,而是交給作業系統來處理,作業系統可以根據 buffer 填充情況 / 通道空閒時間等擇機觸發同步;這是一種普通的檔案操作方式。效能較好,在物理伺服器故障時,資料丟失量會因 OS 配置有關。

觸發 rewrite 的時機可以通過配置檔案來宣告,同時 redis 中可以通過 bgrewriteaof 指令人工干預。

redis-cli -h ip -p port bgrewriteaof

因為 rewrite 操作 /aof 記錄同步 /snapshot 都消耗磁碟 IO,redis 採取了“schedule”策略:無論是“人工干預”還是系統觸發,snapshot 和 rewrite 需要逐個被執行。

AOF rewrite 過程並不阻塞客戶端請求。系統會開啟一個子程序來完成。

Redis主從複製

概述

1、redis的複製功能是支援多個數據庫之間的資料同步。一類是主資料庫(master)一類是從資料庫(slave),主資料庫可以進行讀寫操作,當發生寫操作的時候自動將資料同步到從資料庫,而從資料庫一般是隻讀的,並接收主資料庫同步過來的資料,一個主資料庫可以有多個從資料庫,而一個從資料庫只能有一個主資料庫。

2、通過redis的複製功能可以很好的實現資料庫的讀寫分離,提高伺服器的負載能力。主資料庫主要進行寫操作,而從資料庫負責讀操作。

主從複製過程:見下圖

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-g7L7sH1N-1607954996206)(img\3.png)]

執行過程:

1:當一個從資料庫啟動時,會向主資料庫傳送sync命令,

2:主資料庫接收到sync命令後會開始在後臺儲存快照(執行rdb操作),並將儲存期間接收到的命令快取起來

3:當快照完成後,redis會將快照檔案和所有快取的命令傳送給從資料庫。

4:從資料庫收到後,會載入快照檔案並執行收到的快取的命令。

修改從Redis從配置檔案

# 設定主伺服器地址和埠
# slaveof <masterip> <masterport> 3.x版本使用指令
replicaof <masterip> <masterport> 
# 主Redis配置了密碼,則需要配置
# masterauth 123456

可以使用info命令檢視主從資訊

info replication

Redis哨兵機制

什麼是哨兵機制

Redis的哨兵(sentinel) 系統用於管理多個 Redis 伺服器,該系統執行以下三個任務:

  • 監控(Monitoring): 哨兵(sentinel) 會不斷地檢查你的Master和Slave是否運作正常。
  • 提醒(Notification):當被監控的某個 Redis出現問題時, 哨兵(sentinel) 可以通過 API 向管理員或者其他應用程式傳送通知。
  • 自動故障遷移(Automatic failover):當一個Master不能正常工作時,哨兵(sentinel) 會開始一次自動故障遷移操作,它會將失效Master的其中一個Slave升級為新的Master, 並讓失效Master的其他Slave改為複製新的Master; 當客戶端試圖連線失效的Master時,叢集也會向客戶端返回新Master的地址,使得叢集可以使用Master代替失效Master。

哨兵(sentinel) 是一個分散式系統,你可以在一個架構中執行多個哨兵(sentinel) 程序,這些程序使用流言協議(gossipprotocols)來接收關於Master是否下線的資訊,並使用投票協議(agreement protocols)來決定是否執行自動故障遷移,以及選擇哪個Slave作為新的Master.

每個哨兵(sentinel) 會向其它哨兵(sentinel)、master、slave定時傳送訊息,以確認對方是否”活”著,如果發現對方在指定時間(可配置)內未迴應,則暫時認為對方已掛(所謂的”主觀認為宕機” Subjective Down,簡稱sdown).

若“哨兵群”中的多數sentinel,都報告某一master沒響應,系統才認為該master"徹底死亡"(即:客觀上的真正down機,Objective Down,簡稱odown),通過一定的vote演算法,從剩下的slave節點中,選一臺提升為master,然後自動修改相關配置.

雖然哨兵(sentinel) 釋出為一個單獨的可執行檔案 redis-sentinel ,但實際上它只是一個執行在特殊模式下的 Redis 伺服器,你可以在啟動一個普通 Redis 伺服器時通過給定 --sentinel 選項來啟動哨兵(sentinel).

哨兵(sentinel) 的一些設計思路和zookeeper非常類似,如下圖所示。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-6JEyVmFa-1607954996207)(img\4.png)]

哨兵模式修改配置

實現步驟:

1 拷貝到一份sentinel.conf

cp sentinel.conf /usr/local/redis/redis.6.0.5/sentinel-1/sentinel.conf

2 修改sentinel.conf配置檔案

#根據情況調整配置
#埠
port 26379
#日誌檔案配置
logfile "/var/run/redis-sentinel-1.log"
#程序號檔案配置
pidfile /var/run/redis-sentinel-1.pid
#工作目錄
dir "/usr/local/redis/redis.6.0.5/sentinel-1"

#固定配置
#後臺啟動
daemonize yes
#主節點 名稱 IP 埠號 選舉次數(需要幾個哨兵同意則認為主伺服器失效)
sentinel monitor mymaster 192.168.220.128 6379 1
#主節點密碼
#sentinel auth-pass mymaster 123456
#修改心跳檢測 5000毫秒
sentinel down-after-milliseconds mymaster 5000
#在執行故障轉移時, 最多可以有多少個從伺服器同時對新的主伺服器進行同步
sentinel parallel-syncs mymaster 2
#進行故障轉移時如果超過了配置的<times>時間就表示故障轉移超時失敗
sentinel failover-timeout mymaster 15000

3 啟動哨兵模式

redis-sentinel /usr/local/redis/redis.6.0.5/sentinel-1/sentinel.conf

4 停止哨兵模式

redis-cli -p [Sentinel PORT] -h [Sentinel IP] shutdown

SpringBoot整合主從複製

只需要修改Redis配置即可

#配置redis連線
#選擇資料庫,預設值為0
spring.redis.database=0
#密碼
spring.redis.password=
#請求超時時間
spring.redis.timeout=0
#配置jedis連線池
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0

#設定哨兵監聽主伺服器
spring.redis.sentinel.master=mymaster
#設定哨兵群,多個哨兵使用,號分割
spring.redis.sentinel.nodes=192.168.220.128:26379

Redis事務

Redis 事務可以一次執行多個命令, 並且帶有以下兩個重要的保證:

事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端傳送來的命令請求所打斷。

事務是一個原子操作:事務中的命令要麼全部被執行,要麼全部都不執行。

一個事務從開始到執行會經歷以下三個階段:

  1. 開始事務。

  2. 命令入隊。

  3. 執行事務。

以下是一個事務的例子, 它先以 MULTI 開始一個事務, 然後將多個命令入隊到事務中, 最後由 EXEC 命令觸發事務, 一併執行事務中的所有命令:

#開啟事務
MULTI
#指令1
SET book-name "Mastering C++ in 21 days"
#指令2
GET book-name
#指令3
SADD tag "C++" "Programming" "Mastering Series"
#執行事務
EXEC

Redis 快取相關問題

快取穿透

快取穿透是指查詢一個數據庫一定不存在的資料。
我們以前正常的使用Redis快取的流程大致是:
1、資料查詢首先進行快取查詢
2、如果資料存在則直接返回快取資料
3、如果資料不存在,就對資料庫進行查詢,並把查詢到的資料放進快取
4、如果資料庫查詢資料為空,則不放進快取

例如, 我們的資料表中主鍵是自增產生的,所有的主鍵值都大於0。此時如果使用者傳入的引數為-1,會是怎麼樣的?

這個-1,就是一定不存在的物件。程式就會每次都去查詢資料庫,而每次查詢都是空,每次又都不會進行快取。假如有人惡意攻擊,就可以利用這個漏洞,對資料庫造成壓力,甚至壓垮我們的資料庫。

解決方案:
(1)利用互斥鎖,快取失效的時候,先去獲得鎖,得到鎖了,再去請求資料庫。沒得到鎖,則休眠一段時間重試。

使用到的相關指令:

  • SETNX:將key設定值為value,如果key不存在,這種情況下等同SET命令。 當key存在時,什麼也不做
  • GETSET:先查詢出原來的值,值不存在就返回nil。然後再設定值

定義鎖元件

@Component
@Slf4j
public class RedisLock {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 加鎖
     * @param key 如商品的唯一標誌 1
     * @param value 當前時間+超時時間 1號執行緒 10:50:01 2 10:50:01
     * @return
     */
    public boolean lock(String key,String value){
        //表示第一個拿到鎖
        if(redisTemplate.opsForValue().setIfAbsent(key,value)){//對應setnx命令
            //可以成功設定,也就是key不存在
            return true;
        }
        //判斷鎖超時 - 防止原來的操作異常,沒有執行解鎖操作  防止死鎖
        String currentValue = (String) redisTemplate.opsForValue().get(key);
        //如果鎖過期
        if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){//currentValue不為空且小於當前時間
            //獲取上一個鎖的時間value
            String oldValue = (String)redisTemplate.opsForValue().getAndSet(key,value);//對應getset,如果key存在
            //假設兩個執行緒同時進來,key被佔用了。獲取的值currentValue=A(get取的舊的值肯定是一樣的),兩個執行緒的value都是B,key都是K.鎖時間已經過期了。
            //而這裡面的getAndSet一次只會一個執行,也就是一個執行之後,上一個的value已經變成了B。只有一個執行緒獲取的上一個值會是A,另一個執行緒拿到的值是B。
            if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue) ){
                //oldValue不為空且oldValue等於currentValue,也就是校驗是不是上個對應的商品時間戳,也是防止併發
                return true;
            }
        }
        //無鎖
        return false;
    }
    /**
     * 解鎖
     * @param key
     * @param value
     */
    public void unlock(String key,String value){
        try {
            String currentValue = (String)redisTemplate.opsForValue().get(key);
            if(!StringUtils.isEmpty(currentValue) && currentValue.equals(value) ){
                redisTemplate.opsForValue().getOperations().delete(key);//刪除key
            }
        } catch (Exception e) {
            log.error("[Redis分散式鎖] 解鎖出現異常了,{}",e);
        }
    }
}

使用流程流程

public void orderProductMocckDiffUser(String productId) {
    //1 加鎖
    int tryTimes = 3;
    long time = -1;
    do{
		time = System.currentTimeMillis() + TIMEOUT;
        if(!redisLock.lock(productId,String.valueOf(time))){
            //休眠1s
            Thread.sleep(1000);
    	}else{
            //加鎖成功
            break;
        }
        time = -1;
        tryTimes --;
    }while(tryTimes > 0);
    
    if(time != -1){
    	
    	//2 省略資料處理流程

    	//3 解鎖
    	redisLock.unlock(productId,String.valueOf(time));    
    }
}

(2)採用非同步更新策略,無論 key 是否取到值,都直接返回。value 值中維護一個快取失效時間,快取如果過期,非同步起一個執行緒去讀資料庫,更新快取。需要做快取預熱(專案啟動前,先載入快取)操作。
(3)提供一個能迅速判斷請求是否有效的攔截機制(將key快取起來)。迅速判斷出,請求所攜帶的 Key 是否合法有效。如果不合法,則直接返回。

快取雪崩

快取雪崩,是指在某一個時間段,快取集中過期失效。在快取集中失效的這個時間段對資料的訪問查詢,都落到了資料庫上,對於資料庫而言,就會產生週期性的壓力波峰。為了避免快取雪崩的發生,我們可以將快取的資料設定不同的失效時間,這樣就可以避免快取資料在某個時間段集中失效。

解決方案:

(1)給快取的失效時間,加上一個隨機值,避免集體失效。
(2)使用互斥鎖,但是該方案吞吐量明顯下降了。
(3)雙快取。我們有兩個快取,快取 A 和快取 B。快取 A 的失效時間為 20 分鐘,快取 B 不設失效時間。自己做快取預熱操作。然後細分以下幾個小點:

  1. 從快取 A 讀資料庫,有則直接返回
  2. A 沒有資料,直接從 B 讀資料,直接返回,並且非同步啟動一個更新執行緒。
  3. 更新執行緒同時更新快取 A 和快取 B。

快取擊穿

快取擊穿,是指一個key非常熱點(例如雙十一期間進行搶購的商品資料),在不停的扛著高併發,高併發集中對這一個點進行訪問,當這個key在失效的瞬間,持續的高併發就穿破快取,直接請求到資料庫上,就像在一個屏障上鑿開了一個洞。

我們同樣可以將這些熱點資料設定永不過期就可以解決快取擊穿的問題了。

自動過期設定

設定過期時間

expire(key,有效時間(秒))
expireAt(key,系統時間毫秒數)

獲取過期時間

TTL(key)

對映到Redistemplate中的方法

設定過期時間

//在指定時間過期
redisTemplate.expireAt("user2", Instant.ofEpochMilli(System.currentTimeMillis() + 10000));
//設定快取有效時間
redisTemplate.expire("user2",Duration.ofSeconds(20L));

獲取過期時間

Long user21 = redisTemplate.getExpire("user2");