一、Redis 1、簡介 【官方簡介地址:】 https://redis.io/topics/introduction 看不懂不要緊,先混個眼熟,慢慢來...。 【初步認識 Redis:】 Redis is an open source (BSD licensed), in-memory data ...
一、Redis
1、簡介
【官方簡介地址:】 https://redis.io/topics/introduction
看不懂不要緊,先混個眼熟,慢慢來...。
【初步認識 Redis:】 Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. 【翻譯:】 Redis 是一個開源的、基於記憶體的數據存儲結構,可以作為資料庫、緩存、消息中間件。 【重點:】 基於記憶體、支持多種數據結構、常用於緩存。
2、為什麼使用 Redis 作為緩存?
(1)為什麼要使用緩存?
對於一個系統來說,若直接操作資料庫,每次讀寫都經過磁碟操作,當併發量過高時,磁碟讀寫速度極大地影響系統的性能。使用緩存,即在訪問磁碟前設置一個緩衝區,若緩衝區沒有數據,再去資料庫進行操作,這樣可以極大地減少磁碟操作,從而提高系統性能。
(2)Redis 是基於記憶體的、一個高性能的 key - value 資料庫(非關係型資料庫)。
記憶體的處理速度比操作磁碟快,可以提高性能。
緩存分擔了部分請求,減少了資料庫訪問壓力,提高了併發量。
說起 key - value 庫,容易想到 Java 中的 Map,map 實現的是本地緩存(即每台機器各自擁有自己的緩存),容量有限,隨著 JVM 存在、消失。而 Redis 實現的是分散式緩存(即多台機器可以共用一份緩存數據),其數據可以持久化到硬碟中,可以自定義緩存過期機制。
3、Redis 的數據結構?使用場景?
(1)常用命令:
【參考地址:】 http://doc.redisfans.com/ https://www.cnblogs.com/l-y-h/p/12656614.html
(2)常用數據結構:
Redis 是由 C 語言編寫的,其存儲是以 key - value 的形式。key 為字元串,value 為 Redis 的數據結構。常用數據結構為:string、list、set、hash、sortedset。
底層實現原理,以後有空再去研究...
不同數據結構,若採用不同的編碼格式,底層會有不同的實現。
(3)常用數據結構使用場景(舉例,可能不太恰當,大致理解一下):
String 使用場景:
比如:一些博客、文章的閱讀量、點贊數等。
可以根據 文章 ID 生成一個鍵。當某用戶閱讀、點贊後,在相應的 value 上加 1。
比如 :
key 為 文章閱讀量:文章id,
value 為對應的 文章閱讀量。
可以通過 incr、decr 等進行加減閱讀量。
【根據文章ID 生成一個 key:(每個文章都有不同的 id,從而區分不同的 key)】 set article:readcount:1001 0 文章 id 為 1001 的文章當前閱讀量為 0 set article:readcount:1002 0 文章 id 為 1002 的文章當前閱讀量為 0 【閱讀時,數量增 1:】 incr article:readcount:1001 文章 id 為 1001 的文章閱讀量加 1 【獲取閱讀量:】 get article:readcount:1001 獲取文章 id 為 1001 的文章閱讀量
Hash 使用場景:
比如:電商網站的購物車。
可以根據 用戶ID 生成一個 key,商品 ID 為 field,商品數量為 field 對應的 value。
可以使用 hgetall 獲取所有的 field - value,即實現全選。
可以使用 hincrby 對指定的 field 修改數量。
可以使用 hlen 獲取當前購物車商品的種類。等等操作。
比如:
key 為 用戶 ID user:用戶 ID
field 為 商品 ID wares:商品 ID
value 為 商品數量 商品數量
註:
其餘信息可以通過 ajax 根據 用戶 ID 、商品 ID 進行查詢並返回顯示。
【根據用戶 ID、商品 ID、商品數量 生成一個 key,】 hset user:10001 wares:3001 1 給 10001 用戶 添加 一個 3001 商品。 hset user:10001 wares:3002 2 給 10001 用戶 添加 兩個 3002 商品。 【全選操作:】 hgetall user:10001 獲取 10001 用戶所有的 商品(field)以及數量(value) 【增加商品數量:】 hincrby user:10001 wares:3002 3 給 10001 用戶再增加 3 個 3002 商品
List 使用場景:
比如:微信訂閱號推送的消息。
不同的公眾號推送消息有先有後,最後是按照時間順序進行排序顯示(最近的時間顯示在最上面)。
可以使用 List 存儲接收的消息 ID。每接受一個 公眾號消息 的 ID,就 LPUSH 進 List 中,最後使用 LRANGE 去獲取最新的推送消息。
【接收公眾號推送消息的 ID:】 LPUSH msg:我的訂閱號-id 安徽共青團:10001 LPUSH msg:我的訂閱號-id 唐唐頻道:20001 LPUSH msg:我的訂閱號-id 全是黑科技:34811 LPUSH msg:我的訂閱號-id 程式人生:2233 LPUSH msg:我的訂閱號-id 共青團中央:32345 【展示公眾號 ID:】 LRANGE msg:我的訂閱號-id 0 -1
Set 使用場景:
比如:抽獎小程式,獲取朋友圈點贊的用戶信息,可能關註的人(需要使用並集等操作)等。
抽獎就是在一堆用戶中隨機抽取用戶。由於 Set 不可重覆性,可以保證用戶唯一。
使用 SADD 可以添加用戶 ID 到 set 中。
使用 SMEMBERS 可以查看當前參與抽獎的所有元素。
使用 SRANDMEMBER、SPOP 可以抽取獲獎者用戶。
【添加用戶:】 sadd user 1001 1002 1003 1004 【查看所有用戶:】 smembers user 【抽選用戶,不刪除用戶:】 srandmember user 3 【抽選用戶,刪除用戶:】 spop user 3
sortedset(zset)使用場景:
比如:微博熱搜榜、百度熱議榜等。
二、Redis 持久化、資料庫、單線程
1、Redis 資料庫
Redis 預設有 16 個庫,庫編號為 db0 - db15。資料庫之間的數據是相互隔離的、互不影響的。
Redis 是 C/S 結構,有一個 redis-cli 和 redis-server。 redis-server 用於啟動 Redis 服務,預設資料庫數量為 16,可以修改。redis-cli 用於連接某個資料庫。
資料庫中採用哈希表存儲鍵值對,其中 value 可以為不同類型的數據結構。
2、Redis 鍵過期處理
(1)為什麼進行過期處理?
Redis 是基於記憶體的,記憶體容量比較有限,如果長期將 key - value 存放在 記憶體中,會占用大量記憶體,這樣肯定是不行的,所以需要對 key 設置過期時間,當 key 過期後,系統響應並將其刪除,從而減少記憶體的占用。
(2)過期策略:
定時刪除:到某個時間點,就進行刪除 過期鍵 的操作,對 記憶體 友好,對 CPU 不友好。
惰性刪除:每次獲取鍵時,判斷該鍵是否過期,過期則刪除,對 CPU 友好,對 記憶體 不友好。
定期刪除:每過一段時間,就去刪除 過期鍵。
Redis 中採用 惰性刪除 + 定期刪除,即意味著 某個鍵 到了過期時間,也不一定會被立即刪除。
(3)記憶體淘汰機制:
由於 Redis 可能會不及時的刪除過期 key,導致 記憶體里堆積了很多沒用的 key,會消耗大量記憶體。此時,需要通過記憶體淘汰機制,選擇不需要的 key,並將其刪除。
比如:設置消耗記憶體最大值,當超過記憶體最大值後,進行數據淘汰,將最近最少使用的 key 數據淘汰(一般應用於熱搜排行榜的場景)。
【常見記憶體淘汰機制:】 allkeys-lru: 在所有 key 中,移除最近最少使用的 key(常用) allkeys-random: 在所有 key 中,隨機移除 key。 volatile-lru: 在設置過期時間的 key 中,移除最近最少使用的 key volatile-random: 在設置過期時間的 key 中,隨機移除 key。 volatile-ttl: 在設置過期時間的 key 中,優先移除 即將過期 的 key。
3、數據持久化 -- RDB
Redis 是基於記憶體的,Redis 一旦重啟,所有數據都會丟失,所以一般會將數據持久化到硬碟中,Redis 重啟後可以通過硬碟恢複數據。
Redis 採用兩種方法進行數據持久化 -- RDB 、AOF。
(1)RDB(Redis DataBase)
RDB 基於快照,可以指定時間間隔、將某一時刻的所有數據保存到一個 RDB 文件中,是一個二進位文件,預設為 dump.rdb。Redis 啟動時,若發現存在 rdb 文件,則會自動載入該文件(載入的過程是一個阻塞的狀態)。
(2)通過三種方式可以實現 RDB。
Method1:SAVE 命令觸發
客戶端執行 SAVE 命令後,會阻塞當前 Redis 伺服器(即 Redis 不能處理其他命令),直到 RDB 過程結束。若存在舊的 RDB 文件,會進行替換。(此方式若數據量過大,會影響系統性能)
Method2:BGSAVE 命令觸發
客戶端執行 BGSAVE 命令後,會創建一個子進程,由子進程來創建 RDB 文件,不會阻塞當前 Redis 伺服器。
Method3:redis.conf 配置文件中配置
【save 格式:】 save m n 指的是 m 時間間隔內,至少出現了 n 次 key 變化,則進行保存 【舉例:】 save 60 10000 指的是 60 秒內,至少出現了 10000 次 key 變化,則保存
(3)SAVE 與 BGSAVE 比較:
SAVE 屬於 同步操作,會阻塞當前 Redis 伺服器,但不會消耗額外記憶體。
BGSAVE 屬於 非同步操作,不會阻塞當前 Redis 伺服器,但會消耗額外記憶體(創建子進程)。
(4)RDB 優缺點:
優點:
RDB 是全量備份,將數據壓縮到二進位文件中,格式緊湊(文件小),適合數據備份以及恢復。
RDB 可以使用子進程去創建 RDB 文件,主進程不進行 磁碟操作。
缺點:
子進程進行持久化時,父進程若修改記憶體中的數據,子進程不會知曉,此時可能造成數據丟失。
4、數據持久化 -- AOF
(1)AOF(Append Only File)
AOF 指當 Redis 伺服器執行寫命令時,會將寫命令 保存到 AOF 文件中(可以理解為日誌記錄)。
(2)AOF 執行流程:
Step1:命令追加到緩衝區
遇到寫命令時,將命令寫入 aof_buf 緩衝區。
Step2:確認是否需要將緩衝區內容寫入文件。
通過配置文件 redis.conf 中 appendfsync 去確定是否將緩衝區內容寫入文件。
appendfsync always # 每次有數據修改發生時都會寫入AOF文件(磁碟開銷大)。 appendfsync everysec # 每秒鐘同步一次,該策略為AOF的預設策略(丟失 1 秒數據)。 appendfsync no # 從不同步。高效但是數據不會被持久化(數據丟失)。
Step3:文件從緩衝區寫入到文件。
將緩衝區的內容寫入到 aof 文件中。
不停的執行寫命令操作後,會使得 aof 文件變得越來越大,可以使用 BGREWRITEAOF 命令進行 AOF 重寫(可以合併 寫操作命令,減少文件內容冗餘),此重寫基於當前 資料庫數據重寫,不需要讀取舊的 aof 文件。
BGREWRITEAOF 命令會創建子進程,由子進程進行 AOF 重寫,其會存在一個 AOF 重寫緩衝區,重寫緩衝區用於 記錄 創建子進程後 主進程執行的 寫操作。當子進程執行完 AOF 重寫後,向父進程發送請求,將重寫緩衝區的數據寫入新的 aof 文件中,從而使 當前資料庫 與 AOF 文件寫操作一致。
(3)AOF優缺點:
優點:
可以更好的保護數據,預設進行 1 秒同步一次的操作,最多丟失 1 秒數據。
缺點:
AOF 文件過大,恢複數據速度較慢。
(4)AOF、RDB 如何選擇?
AOF、RDB 可以同時使用,但伺服器優先使用 AOF 文件進行數據還原。
AOF:丟失數據少(視 appendfsync 而定),文件體積大,恢複數據速度較慢。
RDB:可能丟失一部分數據,文件體積小,恢複數據速度較快。
5、為什麼 Redis 是單線程?速度為什麼快?
(1)為什麼 Redis 是單線程的?
Redis 基於記憶體進行操作,CPU 不是 Redis 的瓶頸,且單線程 比 多線程容易實現。
(2)速度為什麼快?
基於記憶體操作,讀寫速度快。
單線程操作,避免頻繁上下文切換。
採用了非阻塞 I/O 多路復用機制,保證系統高吞吐量。
註:
非阻塞 I/O 多路復用機制,用來保證多個連接時的系統吞吐量(此處不展開,有時間再總結)。
多路 指的是 多個 socket 連接。
復用 指的是 共用 同一個線程。
簡單的講,就是使單線程高效的處理多個連接請求。
6、Redis 和 memcached 區別?
(1)Redis 可以將數據持久化到硬碟中,memcached 只能將數據存儲在記憶體中(斷電後消失)。
(2)Redis 支持多種數據類型,memcached 支持類型簡單。
三、緩存雪崩、緩存穿透、緩存與資料庫讀寫一致
1、緩存穿透是什麼?如何解決?
(1)緩存穿透是什麼?
緩存穿透指查詢一個不存在的數據,且數據不在緩存中,則查詢會從資料庫查詢,而資料庫查不到數據,則不會將數據存儲在緩存中。以致於每次查詢都會繞過緩存,從資料庫查數據,使緩存失效。
(2)緩存穿透的可能原因?解決?
原因:
請求的參數不合理。
比如資料庫的 id 自增,且從 100 開始,但是每次請求都是 100 以下的 id 或者 負數的 id,則每次查詢,緩存中沒有值,直接去查資料庫,而資料庫查不到值,就不會將數據保存到緩存中,從而使緩存失效。
解決:
方式一:對參數進行過濾處理(比如 BloomFilter),不合法的參數不會訪問到資料庫。
方式二:當資料庫找不到數據時,返回一個空對象到緩存中,並設置一個過期時間,這樣就可以從緩存中獲取數據了。
2、緩存雪崩是什麼?如何解決?
(1)緩存雪崩是什麼?
緩存雪崩指的是由於某種原因,導致緩衝層出現了問題,所有的請求(大量請求)直接訪問資料庫(可以理解為發生大量數據穿透),從而使資料庫宕機。
(2)緩存雪崩的可能原因?解決?
原因一:
Redis 服務掛掉了,即緩存失效,所有請求不經過緩存直達資料庫,資料庫反應不過來而宕機。
如何解決:
Step1:應該儘量避免 Redis 服務掛掉。
為了實現 Redis 高可用,應該使用 主從模式 + 哨兵模式(或者採用 Redis 集群),儘量避免 Redis 服務掛掉。
Step2:應該儘量避免 資料庫 掛掉。
萬一 Redis 服務真的掛了,應當進行 熔斷、降低、限流等操作,儘量避免資料庫被幹掉,至少要保證服務還能正常運行。
Step3:數據恢復。
對 Redis 數據進行持久化,重啟 Redis 服務後,載入磁碟數據進行數據恢復。
原因二:
Redis 對數據設置了過期時間,同一時間這些數據失效,此時恰巧有大量請求同時訪問這些數據,會穿過緩存直接訪問資料庫,造成大量緩存穿透,從而導致資料庫宕機。
如何解決:
緩存的同時,將過期時間設置成隨機值,此時能極大避免大量數據 過期時間一致。
3、緩存、資料庫讀寫一致
(1)讀操作流程:
Step1:查詢緩存中是否存在數據,存在數據則直接返回。
Step2:緩存中不存在數據,則查詢資料庫中是否存在數據,存在數據,則將數據保存在緩存中,並返回數據。
(2)讀寫操作同時進行時可能出現數據不一致。
造成讀寫不一致的情況有很多。
比如一件商品,開始時 資料庫、緩存里顯示的庫存數量均為 1000。此時讀操作並沒有問題。現在賣出一件商品,需要更新資料庫,假如更新資料庫數據成功,但是更新緩存數據失敗 ,即此時資料庫顯示庫存數量為 999,而緩存顯示數量為 1000,則下次操作,獲取到的商品數量仍為 1000,此時就造成了讀寫不一致。
(3)如何解決讀寫不一致?
方式一:一般給緩存的數據設置過期時間,數據過期則被刪除,下次會從資料庫查詢並更新緩存。
方式二:保證資料庫、緩存更新的原子性(分散式事務)。要麼同時成功、要麼同時失敗。
(4)更新緩存、資料庫的兩種方式:
方式一:先更新緩存,再更新資料庫。
方式二:先更新資料庫,再更新緩存。
註:
對於更新緩存,一般直接刪除某個數據,簡單粗暴。下次讀取時從資料庫讀取並保存到緩存中。
對於方式一(單線程情況):
若刪除緩存失敗,可以直接拋出異常,此時資料庫與緩存數據均無變化,即數據一致。
若刪除緩存成功,但是更新資料庫失敗,此時緩存中沒有該數據,下次讀取時,從資料庫中讀取並保存到緩存中,從而數據一致。
若刪除緩存、更新資料庫均成功,下次讀取數據肯定一致。
對於方式一(高併發情況):
線程 A 進行更新操作,線程 B 進行讀操作。
線程 A 刪除緩存,此時線程 B 進行讀取,發現緩存不存在,則直接從資料庫中讀取,並將該值存入緩存。
線程 A 對資料庫數據進行更新,此時緩存中的值 與 資料庫的值不一致了。
如何解決上述的數據不一致:
將命令操作積壓到隊列中(先進先出),進行串列化,比如先刪除緩存,再更新資料庫,最後再進行讀取。
對於方式二(單線程情況):
若更新資料庫失敗,則直接拋出異常,此時資料庫與緩存數據均無變化,即數據一致。
若更新資料庫成功,但刪除緩存失敗,則資料庫的數據為新數據,與緩存數據不一致了。
若更新資料庫、刪除緩存均成功,則下次讀寫的數據肯定一致。
如何解決上述的數據不一致:
不斷重覆刪除 key,直至可以刪除。
對於方式二(高併發情況):
線程 A 進行查詢操作,線程 B 進行更新操作。
線程 A 查詢時,恰好緩存失效,直接通過資料庫進行查詢,此時 線程 B 更新資料庫數據,併進行緩存刪除,然後 線程 A 將從資料庫獲取的數據寫入緩存中,此時緩存數據與資料庫數據不一致了。
上例情況發生概率很低,畢竟寫操作的速度慢於讀操作,且讀操作要先於寫操作進入資料庫,且慢於寫操作操作緩存,同時滿足這個情況的概率只能說是走了狗屎運。