文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈為小伙伴奉上以下珍貴的學習資源:
- 瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高並發實戰》 面試必備 + 大廠必備 + 漲薪必備
- 瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高並發核心編程》 面試必備 + 大廠必備 + 漲薪必備
- 資源寶庫: Java程序員必備 網盤資源大集合 價值>1000元 隨便取 GO->【博客園總入口 】
- 送書活動:聯合機械工業出版社 Java高並發三部曲 150本紙質書大贈送,手快有,手慢無 ->【博客園總入口 】
- 獨孤九劍:Netty靈魂實驗 : 本地 100W連接 高並發實驗,瞬間提升Java內力
- 最純粹的技術交流:和大廠 小伙伴、技術高手、架構師 進行 純粹的的技術問題交流、探討求助、問題圍觀學習
推薦: 瘋狂創客圈 高質量 博文
| 高並發 必讀 的精彩博文 | |
|---|---|
| nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
| Zookeeper 分布式鎖 (圖解+秒懂+史上最全) | Webflux(史上最全) |
| SpringCloud gateway (史上最全) | TCP/IP(圖解+秒懂+史上最全) |
| 10分鍾看懂, Java NIO 底層原理 | Feign原理 (圖解) |
| 更多精彩博文 ..... | 請參見【 瘋狂創客圈 高並發 總目錄 】 |
史上最全 Java 面試題 30 專題 總目錄
史上最全 Java 面試題:Redis 篇
入門級Redis試題
試題一:為什么使用redis?
分析:博主覺得在項目中使用redis,主要是從兩個角度去考慮:性能和並發。當然,redis還具備可以做分布式鎖等其他功能,但是如果只是為了分布式鎖這些其他功能,完全還有其他中間件(如zookpeer等)代替,並不是非要使用redis。因此,這個問題主要從性能和並發兩個角度去答。
回答:如下所示,分為兩點
(一)性能
我們在碰到需要執行耗時特別久,且結果不頻繁變動的SQL,就特別適合將運行結果放入緩存。這樣,后面的請求就去緩存中讀取,使得請求能夠迅速響應。
(二)並發
在大並發的情況下,所有的請求直接訪問數據庫,數據庫會出現連接異常。這個時候,就需要使用redis做一個緩沖操作,讓請求先訪問到redis,而不是直接訪問數據庫。
參考資料
瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高並發實戰》 面試必備 + 面試必備 + 面試必備
Redis基礎試題
1. Redis有哪些數據結構?
字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。
如果你是Redis中高級用戶,還需要加上下面幾種數據結構HyperLogLog、Geo、Pub/Sub。
如果你說還玩過Redis Module,像BloomFilter,RedisSearch,Redis-ML,面試官得眼睛就開始發亮了。
2. 使用過Redis分布式鎖么,它是什么回事?
先拿setnx來爭搶鎖,搶到之后,再用expire給鎖加一個過期時間防止鎖忘記了釋放。
這時候對方會告訴你說你回答得不錯,然后接着問如果在setnx之后執行expire之前進程意外crash或者要重啟維護了,那會怎么樣?
這時候你要給予驚訝的反饋:唉,是喔,這個鎖就永遠得不到釋放了。緊接着你需要抓一抓自己得腦袋,故作思考片刻,好像接下來的結果是你主動思考出來的,然后回答:我記得set指令有非常復雜的參數,這個應該是可以同時把setnx和expire合成一條指令來用的!對方這時會顯露笑容,心里開始默念:嗯,這小子還不錯。
3. Redis里面有1億個key,其中有10w個key是以某個固定的已知的前綴開頭的,如何將它們全部找出來?
使用keys指令可以掃出指定模式的key列表。
對方接着追問:如果這個redis正在給線上的業務提供服務,那使用keys指令會有什么問題?
這個時候你要回答redis關鍵的一個特性:redis的單線程的。keys指令會導致線程阻塞一段時間,線上服務會停頓,直到指令執行完畢,服務才能恢復。這個時候可以使用scan指令,scan指令可以無阻塞的提取出指定模式的key列表,但是會有一定的重復概率,在客戶端做一次去重就可以了,但是整體所花費的時間會比直接用keys指令長。
4. 使用過Redis做異步隊列么,你是怎么用的?
一般使用list結構作為隊列,rpush生產消息,lpop消費消息。當lpop沒有消息的時候,要適當sleep一會再重試。
如果對方追問可不可以不用sleep呢?list還有個指令叫blpop,在沒有消息的時候,它會阻塞住直到消息到來。
如果對方追問能不能生產一次消費多次呢?使用pub/sub主題訂閱者模式,可以實現1:N的消息隊列。
如果對方追問pub/sub有什么缺點?在消費者下線的情況下,生產的消息會丟失,得使用專業的消息隊列如rabbitmq等。
如果對方追問redis如何實現延時隊列?我估計現在你很想把面試官一棒打死如果你手上有一根棒球棍的話,怎么問的這么詳細。但是你很克制,然后神態自若的回答道:使用sortedset,拿時間戳作為score,消息內容作為key調用zadd來生產消息,消費者用zrangebyscore指令獲取N秒之前的數據輪詢進行處理。
到這里,面試官暗地里已經對你豎起了大拇指。但是他不知道的是此刻你卻豎起了中指,在椅子背后。
5. 如果有大量的key需要設置同一時間過期,一般需要注意什么?
如果大量的key過期時間設置的過於集中,到過期的那個時間點,redis可能會出現短暫的卡頓現象。一般需要在時間上加一個隨機值,使得過期時間分散一些。
6. Redis如何做持久化的?
bgsave做鏡像全量持久化,aof做增量持久化。因為bgsave會耗費較長時間,不夠實時,在停機的時候會導致大量丟失數據,所以需要aof來配合使用。在redis實例重啟時,優先使用aof來恢復內存的狀態,如果沒有aof日志,就會使用rdb文件來恢復。
如果再問aof文件過大恢復時間過長怎么辦?你告訴面試官,Redis會定期做aof重寫,壓縮aof文件日志大小。如果面試官不夠滿意,再拿出殺手鐧答案,Redis4.0之后有了混合持久化的功能,將bgsave的全量和aof的增量做了融合處理,這樣既保證了恢復的效率又兼顧了數據的安全性。這個功能甚至很多面試官都不知道,他們肯定會對你刮目相看。
如果對方追問那如果突然機器掉電會怎樣?取決於aof日志sync屬性的配置,如果不要求性能,在每條寫指令時都sync一下磁盤,就不會丟失數據。但是在高性能的要求下每次都sync是不現實的,一般都使用定時sync,比如1s1次,這個時候最多就會丟失1s的數據。
7. Pipeline有什么好處,為什么要用pipeline?
可以將多次IO往返的時間縮減為一次,前提是pipeline執行的指令之間沒有因果相關性。使用redis-benchmark進行壓測的時候可以發現影響redis的QPS峰值的一個重要因素是pipeline批次指令的數目。
8. Redis的同步機制了解么?
從從同步。第一次同步時,主節點做一次bgsave,並同時將后續修改操作記錄到內存buffer,待完成后將rdb文件全量同步到復制節點,復制節點接受完成后將rdb鏡像加載到內存。加載完成后,再通知主節點將期間修改的操作記錄同步到復制節點進行重放就完成了同步過程。
9. 是否使用過Redis集群,集群的原理是什么?
Redis Sentinal着眼於高可用,在master宕機時會自動將slave提升為master,繼續提供服務。
Redis Cluster着眼於擴展性,在單個redis內存不足時,使用Cluster進行分片存儲。
Redis提升試題
1、在項目中緩存是如何使用的?為什么要用緩存?緩存使用不當會造成什么后果?
面試官心理分析
這個問題,互聯網公司必問,要是一個人連緩存都不太清楚,那確實比較尷尬。
只要問到緩存,上來第一個問題,肯定是先問問你項目哪里用了緩存?為啥要用?不用行不行?如果用了以后可能會有什么不良的后果?
這就是看看你對緩存這個東西背后有沒有思考,如果你就是傻乎乎的瞎用,沒法給面試官一個合理的解答,那面試官對你印象肯定不太好,覺得你平時思考太少,就知道干活兒。
面試題剖析
項目中緩存是如何使用的?
這個,需要結合自己項目的業務來。
為什么要用緩存?
用緩存,主要有兩個用途:高性能、高並發。
高性能
假設這么個場景,你有個操作,一個請求過來,吭哧吭哧你各種亂七八糟操作 mysql,半天查出來一個結果,耗時 600ms。但是這個結果可能接下來幾個小時都不會變了,或者變了也可以不用立即反饋給用戶。那么此時咋辦?
緩存啊,折騰 600ms 查出來的結果,扔緩存里,一個 key 對應一個 value,下次再有人查,別走 mysql折騰 600ms 了,直接從緩存里,通過一個 key 查出來一個 value,2ms 搞定。性能提升 300 倍。
就是說對於一些需要復雜操作耗時查出來的結果,且確定后面不怎么變化,但是有很多讀請求,那么直接將查詢出來的結果放在緩存中,后面直接讀緩存就好。
高並發
所以要是你有個系統,高峰期一秒鍾過來的請求有 1 萬,那一個 mysql 單機絕對會死掉。你這個時候就只能上緩存,把很多數據放緩存,別放 mysql。緩存功能簡單,說白了就是 key-value 式操作,單機支撐的並發量輕松一秒幾萬十幾萬,支撐高並發 so easy。單機承載並發量是 mysql 單機的幾十倍。
緩存是走內存的,內存天然就支撐高並發。
用了緩存之后會有什么不良后果?
常見的緩存問題有以下幾個:
緩存與數據庫雙寫不一致 、緩存雪崩、緩存穿透、緩存並發競爭后面再詳細說明。
2、redis 和 memcached 有什么區別?redis 的線程模型是什么?為什么 redis 單線程卻能支撐高並發?
面試官心理分析
這個是問 redis 的時候,最基本的問題吧,redis 最基本的一個內部原理和特點,就是 redis 實際上是個單線程工作模型,你要是這個都不知道,那后面玩兒 redis 的時候,出了問題豈不是什么都不知道?
還有可能面試官會問問你 redis 和 memcached 的區別,但是 memcached 是早些年各大互聯網公司常用的緩存方案,但是現在近幾年基本都是 redis,沒什么公司用 memcached 了。
面試題剖析
redis 和 memcached 有啥區別?
redis 支持復雜的數據結構
redis 相比 memcached 來說,擁有更多的數據結構,能支持更豐富的數據操作。如果需要緩存能夠支持更復雜的結構和操作, redis 會是不錯的選擇。
redis 原生支持集群模式
在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 沒有原生的集群模式,需要依靠客戶端來實現往集群中分片寫入數據。
性能對比
由於 redis 只使用單核,而 memcached 可以使用多核,所以平均每一個核上 redis 在存儲小數據時比memcached 性能更高。而在 100k 以上的數據中,memcached 性能要高於 redis。雖然 redis 最近也在存儲大數據的性能上進行優化,但是比起 memcached,還是稍有遜色。
redis 的線程模型
redis 內部使用文件事件處理器 file event handler,這個文件事件處理器是單線程的,所以 redis 才叫做單線程的模型。它采用 IO 多路復用機制同時監聽多個 socket,將產生事件的 socket 壓入內存隊列中,事件分派器根據 socket 上的事件類型來選擇對應的事件處理器進行處理。
文件事件處理器的結構包含 4 個部分:
- 多個 socket
- IO 多路復用程序
- 文件事件分派器
- 事件處理器(連接應答處理器、命令請求處理器、命令回復處理器)
多個 socket 可能會並發產生不同的操作,每個操作對應不同的文件事件,但是 IO 多路復用程序會監聽多個 socket,會將產生事件的 socket 放入隊列中排隊,事件分派器每次從隊列中取出一個 socket,根據 socket 的事件類型交給對應的事件處理器進行處理。
來看客戶端與 redis 的一次通信過程:

要明白,通信是通過 socket 來完成的,不懂的同學可以先去看一看 socket 網絡編程。
- 客戶端 socket01 向 redis 的 server socket 請求建立連接,此時 server socket 會產生一個
AE_READABLE事件,IO 多路復用程序監聽到 server socket 產生的事件后,將該事件壓入隊列中。文件事件分派器從隊列中獲取該事件,交給連接應答處理器。連接應答處理器會創建一個能與客戶端通信的 socket01,並將該 socket01 的AE_READABLE事件與命令請求處理器關聯。- 假設此時客戶端發送了一個
set key value請求,此時 redis 中的 socket01 會產生AE_READABLE事件,IO 多路復用程序將事件壓入隊列,此時事件分派器從隊列中獲取到該事件,由於前面 socket01 的AE_READABLE事件已經與命令請求處理器關聯,因此事件分派器將事件交給命令請求處理器來處理。命令請求處理器讀取 socket01 的key value並在自己內存中完成key value的設置。操作完成后,它會將 socket01 的AE_WRITABLE事件與令回復處理器關聯。- 如果此時客戶端准備好接收返回結果了,那么 redis 中的 socket01 會產生一個
AE_WRITABLE事件,同樣壓入隊列中,事件分派器找到相關聯的命令回復處理器,由命令回復處理器對 socket01 輸入本次操作的一個結果,比如ok,之后解除 socket01 的AE_WRITABLE事件與命令回復處理器的關聯。
這樣便完成了一次通信。
為啥 redis 單線程模型也能效率這么高?
- 純內存操作
- 核心是基於非阻塞的 IO 多路復用機制
- 單線程反而避免了多線程的頻繁上下文切換問題
3、redis 都有哪些數據類型?分別在哪些場景下使用比較合適?
面試官心理分析
除非是面試官感覺看你簡歷,是工作 3 年以內的比較初級的同學,可能對技術沒有很深入的研究,面試官才會問這類問題。否則,在寶貴的面試時間里,面試官實在不想多問。
其實問這個問題,主要有兩個原因:
- 看看你到底有沒有全面的了解 redis 有哪些功能,一般怎么來用,啥場景用什么,就怕你別就會最簡單的 KV 操作;
- 看看你在實際項目里都怎么玩兒過 redis。
要是你回答的不好,沒說出幾種數據類型,也沒說什么場景,你完了,面試官對你印象肯定不好,覺得你平時就是做個簡單的 set 和 get。
面試題剖析
redis 主要有以下幾種數據類型:
- string
- hash
- list
- set
- sorted set
string
這是最簡單的類型,就是普通的 set 和 get,做簡單的 KV 緩存。
set college szu
hash
這個是類似 map 的一種結構,這個一般就是可以將結構化的數據,比如一個對象(前提是這個對象沒嵌套其他的對象)給緩存在 redis 里,然后每次讀寫緩存的時候,可以就操作 hash 里的某個字段。
hset person name bingo
hset person age 20
hset person id 1
hget person name
person = {
"name": "bingo",
"age": 20,
"id": 1
}
list
list 是有序列表,這個可以玩兒出很多花樣。
比如可以通過 list 存儲一些列表型的數據結構,類似粉絲列表、文章的評論列表之類的東西。
比如可以通過 lrange 命令,讀取某個閉區間內的元素,可以基於 list 實現分頁查詢,這個是很棒的一個功能,基於 redis 實現簡單的高性能分頁,可以做類似微博那種下拉不斷分頁的東西,性能高,就一頁一頁走。
# 0開始位置,-1結束位置,結束位置為-1時,表示列表的最后一個位置,即查看所有。
lrange mylist 0 -1
比如可以搞個簡單的消息隊列,從 list 頭懟進去,從 list 尾巴那里弄出來。
lpush mylist 1
lpush mylist 2
lpush mylist 3 4 5
# 1
rpop mylist
set
set 是無序集合,自動去重。
直接基於 set 將系統里需要去重的數據扔進去,自動就給去重了,如果你需要對一些數據進行快速的全局去重,你當然也可以基於 jvm 內存里的 HashSet 進行去重,但是如果你的某個系統部署在多台機器上呢?得基於 redis 進行全局的 set 去重。
可以基於 set 玩兒交集、並集、差集的操作,比如交集吧,可以把兩個人的粉絲列表整一個交集,看看倆人的共同好友是誰?對吧。
把兩個大 V 的粉絲都放在兩個 set 中,對兩個 set 做交集。
#-------操作一個set-------
# 添加元素
sadd mySet 1
# 查看全部元素
smembers mySet
# 判斷是否包含某個值
sismember mySet 3
# 刪除某個/些元素
srem mySet 1
srem mySet 2 4
# 查看元素個數
scard mySet
# 隨機刪除一個元素
spop mySet
#-------操作多個set-------
# 將一個set的元素移動到另外一個set
smove yourSet mySet 2
# 求兩set的交集
sinter yourSet mySet
# 求兩set的並集
sunion yourSet mySet
# 求在yourSet中而不在mySet中的元素
sdiff yourSet mySet
sorted set
sorted set 是排序的 set,去重但可以排序,寫進去的時候給一個分數,自動根據分數排序。
zadd board 85 zhangsan
zadd board 72 lisi
zadd board 96 wangwu
zadd board 63 zhaoliu
# 獲取排名前三的用戶(默認是升序,所以需要 rev 改為降序)
zrevrange board 0 3
# 獲取某用戶的排名
zrank board zhaoliu
4、redis 的過期策略都有哪些?內存淘汰機制都有哪些?手寫一下LRU代碼實現?
面試官心理分析
如果你連這個問題都不知道,上來就懵了,回答不出來,那線上你寫代碼的時候,想當然的認為寫進 redis的數據就一定會存在,后面導致系統各種 bug,誰來負責?
常見的有兩個問題:
(1)往 redis 寫入的數據怎么沒了?
可能有同學會遇到,在生產環境的 redis 經常會丟掉一些數據,寫進去了,過一會兒可能就沒了。我的天,同學,你問這個問題就說明 redis 你就沒用對啊。redis 是緩存,你給當存儲了是吧?
啥叫緩存?用內存當緩存。內存是無限的嗎,內存是很寶貴而且是有限的,磁盤是廉價而且是大量的。可能一台機器就幾十個 G 的內存,但是可以有幾個 T 的硬盤空間。redis 主要是基於內存來進行高性能、高並發的讀寫操作的。
那既然內存是有限的,比如 redis 就只能用 10G,你要是往里面寫了 20G 的數據,會咋辦?當然會干掉10G 的數據,然后就保留 10G 的數據了。那干掉哪些數據?保留哪些數據?當然是干掉不常用的數據,保留常用的數據了。
(2)數據明明過期了,怎么還占用着內存?
這是由 redis 的過期策略來決定。
面試題剖析
redis 過期策略
redis 過期策略是:定期刪除+惰性刪除。
所謂定期刪除,指的是 redis 默認是每隔 100ms 就隨機抽取一些設置了過期時間的 key,檢查其是否過期,如果過期就刪除。
假設 redis 里放了 10w 個 key,都設置了過期時間,你每隔幾百毫秒,就檢查 10w 個 key,那 redis 基本上就死了,cpu 負載會很高的,消耗在你的檢查過期 key 上了。注意,這里可不是每隔 100ms 就遍歷所有的設置過期時間的 key,那樣就是一場性能上的災難。實際上 redis 是每隔 100ms 隨機抽取一些key 來檢查和刪除的。
但是問題是,定期刪除可能會導致很多過期 key 到了時間並沒有被刪除掉,那咋整呢?所以就是惰性刪除了。這就是說,在你獲取某個 key 的時候,redis 會檢查一下 ,這個 key 如果設置了過期時間那么是否過期了?如果過期了此時就會刪除,不會給你返回任何東西。
獲取 key 的時候,如果此時 key 已經過期,就刪除,不會返回任何東西。
答案是:走內存淘汰機制。
內存淘汰機制
redis 內存淘汰機制有以下幾個:
- noeviction: 當內存不足以容納新寫入數據時,新寫入操作會報錯,這個一般沒人用吧,實在是太惡心了。
- allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的 key(這個是最常用的)。
- allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個 key,這個一般沒人用吧,為啥要隨機,肯定是把最近最少使用的 key 給干掉啊。
- volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的 key(這個一般不太合適)。
- volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個 key。
- volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的 key 優先移除。
手寫一個 LRU 算法
你可以現場手寫最原始的 LRU 算法,那個代碼量太大了,似乎不太現實。
不求自己純手工從底層開始打造出自己的 LRU,但是起碼要知道如何利用已有的 JDK 數據結構實現一個Java 版的 LRU。
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
/**
* 傳遞進來最多能緩存多少數據
* @param cacheSize 緩存大小
*/
public LRUCache(int cacheSize) {
//true 表示讓 linkedHashMap 按照訪問順序來進行排序,最近訪問的放在頭部,最老訪問的的放在尾部
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
//當map中的數據量大於制定的緩存個數的時候,就自動刪除最老的數據
return size() > CACHE_SIZE;
}
}
父類的構造方法:LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder);
-
initialCapacity:初始容量
-
loadFactor:裝載因子
-
accessOrder:訪問順序
問題1:關於裝載因子的為什么是0.75
請參見尼恩的 《HashMap - 圖解 -秒懂》 博文和配套視頻
問題2:accessOrder訪問順序 的取值含義:
- false,所有的Entry按照插入的順序排列
- true,所有的Entry按照訪問的順序排列
5、如何保證 redis 的高並發和高可用?redis 的主從復制原理能介紹一下么?redis 的哨兵原理能介紹一下么?
面試官心理分析
其實問這個問題,主要是考考你,redis 單機能承載多高並發?如果單機扛不住如何擴容扛更多的並發?redis 會不會掛?既然 redis 會掛那怎么保證 redis 是高可用的?
其實針對的都是項目中你肯定要考慮的一些問題,如果你沒考慮過,那確實你對生產系統中的問題思考太少。
面試題剖析
如果你用 redis 緩存技術的話,肯定要考慮如何用 redis 來加多台機器,保證 redis 是高並發的,還有就是如何讓 redis 保證自己不是掛掉以后就直接死掉了,即 redis 高可用。
由於此節內容較多,因此,會分為兩個小節進行講解。- redis 主從架構 - redis 基於哨兵實現高可用redis 實現高並發主要依靠主從架構,一主多從,一般來說,很多項目其實就足夠了,單主用來寫入數據,單機幾萬 QPS,多從用來查詢數據,多個從實例可以提供每秒 10w 的 QPS。
如果想要在實現高並發的同時,容納大量的數據,那么就需要 redis 集群,使用 redis 集群之后,可以提供每秒幾十萬的讀寫並發。
redis 高可用,如果是做主從架構部署,那么加上哨兵就可以了,就可以實現,任何一個實例宕機,可以進行主備切換。
6、redis 的持久化有哪幾種方式?不同的持久化機制都有什么 優缺點?持久化機制具體底層是如何實現的?
面試官心理分析
redis 如果僅僅只是將數據緩存在內存里面,如果 redis 宕機了再重啟,內存里的數據就全部都弄丟了啊。
你必須得用 redis 的持久化機制,將數據寫入內存的同時,異步的慢慢的將數據寫入磁盤文件里,進行持久化。
如果 redis 宕機重啟,自動從磁盤上加載之前持久化的一些數據就可以了,也許會丟失少許數據,但是至少不會將所有數據都弄丟。
這個其實一樣,針對的都是 redis 的生產環境可能遇到的一些問題,就是 redis 要是掛了再重啟,內存里的數據不就全丟了?能不能重啟的時候把數據給恢復了?
面試題剖析
持久化主要是做災難恢復、數據恢復,也可以歸類到高可用的一個環節中去,比如你 redis 整個掛了,然后 redis 就不可用了,你要做的事情就是讓 redis 變得可用,盡快變得可用。
重啟 redis,盡快讓它對外提供服務,如果沒做數據備份,這時候 redis 啟動了,也不可用啊,數據都沒了。
很可能說,大量的請求過來,緩存全部無法命中,在 redis 里根本找不到數據,這個時候就死定了,出現緩存雪崩問題。所有請求沒有在redis命中,就會去mysql數據庫這種數據源頭中去找,一下子mysql承接高並發,然后就掛了…
如果你把 redis 持久化做好,備份和恢復方案做到企業級的程度,那么即使你的 redis 故障了,也可以通過備份數據,快速恢復,一旦恢復立即對外提供服務。
redis 持久化的兩種方式
- RDB:RDB 持久化機制,是對 redis 中的數據執行周期性的持久化。
- AOF:AOF 機制對每條寫入命令作為日志,以 append-only 的模式寫入一個日志文件中,在 redis重啟的時候,可以通過回放 AOF 日志中的寫入指令來重新構建整個數據集。
通過 RDB 或 AOF,都可以將 redis 內存中的數據給持久化到磁盤上面來,然后可以將這些數據備份到別的地方去,比如說阿里雲等雲服務。
如果 redis 掛了,服務器上的內存和磁盤上的數據都丟了,可以從雲服務上拷貝回來之前的數據,放到指定的目錄中,然后重新啟動 redis,redis 就會自動根據持久化數據文件中的數據,去恢復內存中的數據,繼續對外提供服務。
如果同時使用 RDB 和 AOF 兩種持久化機制,那么在 redis 重啟的時候,會使用 AOF 來重新構建數據,因為 AOF 中的數據更加完整。
RDB 優缺點
- RDB 會生成多個數據文件,每個數據文件都代表了某一個時刻中 redis 的數據,這種多個數據文件的方式,非常適合做冷備,可以將這種完整的數據文件發送到一些遠程的安全存儲上去,比如說 Amazon的 S3 雲服務上去,在國內可以是阿里雲的 ODPS 分布式存儲上,以預定好的備份策略來定期備份 redis中的數據。
- RDB 對 redis 對外提供的讀寫服務,影響非常小,可以讓 redis 保持高性能,因為 redis 主進程只需要 fork 一個子進程,讓子進程執行磁盤 IO 操作來進行 RDB 持久化即可。·
- 相對於 AOF 持久化機制來說,直接基於 RDB 數據文件來重啟和恢復 redis 進程,更加快速。
- 如果想要在 redis 故障時,盡可能少的丟失數據,那么 RDB 沒有 AOF 好。一般來說,RDB 數據快照文件,都是每隔 5 分鍾,或者更長時間生成一次,這個時候就得接受一旦 redis 進程宕機,那么會丟失最近 5 分鍾的數據。
- RDB 每次在 fork 子進程來執行 RDB 快照數據文件生成的時候,如果數據文件特別大,可能會導致對客戶端提供的服務暫停數毫秒,或者甚至數秒。
AOF 優缺點
- AOF 可以更好的保護數據不丟失,一般 AOF 會每隔 1 秒,通過一個后台線程執行一次 fsync 操作,最多丟失 1 秒鍾的數據。
- AOF 日志文件以 append-only 模式寫入,所以沒有任何磁盤尋址的開銷,寫入性能非常高,而且文件不容易破損,即使文件尾部破損,也很容易修復。
- AOF 日志文件即使過大的時候,出現后台重寫操作,也不會影響客戶端的讀寫。因為在 rewrite log的時候,會對其中的指令進行壓縮,創建出一份需要恢復數據的最小日志出來。在創建新日志文件的時候,老的日志文件還是照常寫入。當新的merge后日志文件ready的時候,在交換新老日志文件即可。
- AOF 日志文件的命令通過非常可讀的方式進行記錄,這個特性非常適合做災難性的誤刪除的緊急恢復。比如某人不小心用 flushall 命令清空了所有數據,只要這個時候后台 rewrite 還沒有發生,那么就可以立即拷貝 AOF 文件,將最后一條 flushall 命令給刪了,然后再將該 AOF 文件放回去,就可以通過恢復機制,自動恢復所有數據。
- 對於同一份數據來說,AOF 日志文件通常比 RDB 數據快照文件更大。
- AOF 開啟后,支持的寫 QPS 會比 RDB 支持的寫 QPS 低,因為 AOF 一般會配置成每秒 fsync 一次日志文件,當然,每秒一次 fsync,性能也還是很高的。(如果實時寫入,那么 QPS 會大降,redis 性 能會大大降低)
- 以前 AOF 發生過 bug,就是通過 AOF 記錄的日志,進行數據恢復的時候,沒有恢復一模一樣的數據出來。所以說,類似 AOF 這種較為復雜的基於命令日志 / merge / 回放的方式,比基於 RDB 每次持久化一份完整的數據快照文件的方式,更加脆弱一些,容易有 bug。不過 AOF 就是為了避免 rewrite 過程導致的 bug,因此每次 rewrite 並不是基於舊的指令日志進行 merge 的,而是基於當時內存中的數據進行指令的重新構建,這樣健壯性會好很多。
RDB 和 AOF 到底該如何選擇
- 不要僅僅使用 RDB,因為那樣會導致你丟失很多數據;
- 也不要僅僅使用 AOF,因為那樣有兩個問題:第一,你通過 AOF 做冷備,沒有 RDB 做冷備來的恢復速度更快;第二,RDB 每次簡單粗暴生成數據快照,更加健壯,可以避免 AOF 這種復雜的備份和恢復機制的 bug;
9、如何保證緩存與數據庫的雙寫一致性?
面試官心理分析析
你只要用緩存,就可能會涉及到緩存與數據庫雙存儲雙寫,你只要是雙寫,就一定會有數據一致性的問題,那么你如何解決一致性問題?#### 面試題剖析試題剖析
一般來說,如果允許緩存可以稍微的跟數據庫偶爾有不一致的情況,也就是說如果你的系統不是嚴格要求“緩存+數據庫” 必須保持一致性的話,最好不要做這個方案,即:讀請求和寫請求串行化,串到一個內存隊列里去。
串行化可以保證一定不會出現不一致的情況,但是它也會導致系統的吞吐量大幅度降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。
Cache Aside Pattern
最經典的緩存+數據庫讀寫的模式,就是 Cache Aside Pattern。- 讀的時候,先讀緩存,緩存沒有的話,就讀數據庫,然后取出數據后放入緩存,同時返回響應。- 更新的時候,先更新數據庫,然后再刪除緩存。
為什么是刪除緩存,而不是更新緩存?
原因很簡單,很多時候,在復雜點的緩存場景,緩存不單單是數據庫中直接取出來的值。
比如可能更新了某個表的一個字段,然后其對應的緩存,是需要查詢另外兩個表的數據並進行運算,才能計算出緩存最新的值的。
另外更新緩存的代價有時候是很高的。是不是說,每次修改數據庫的時候,都一定要將其對應的緩存更新一份?也許有的場景是這樣,但是對於比較復雜的緩存數據計算的場景,就不是這樣了。如果你頻繁修改一個緩存涉及的多個表,緩存也頻繁更新。但是問題在於,這個緩存到底會不會被頻繁訪問到?
舉個栗子,一個緩存涉及的表的字段,在 1 分鍾內就修改了 20 次,或者是 100 次,那么緩存更新 20 次、100 次;但是這個緩存在 1 分鍾內只被讀取了 1 次,有大量的冷數據。實際上,如果你只是刪除緩存的話,那么在 1 分鍾內,這個緩存不過就重新計算一次而已,開銷大幅度降低。用到緩存才去算緩存。
其實刪除緩存,而不是更新緩存,就是一個 lazy 計算的思想,不要每次都重新做復雜的計算,不管它會不會用到,而是讓它到需要被使用的時候再重新計算。像 mybatis,hibernate,都有懶加載思想。查詢一個部門,部門帶了一個員工的 list,沒有必要說每次查詢部門,都里面的 1000 個員工的數據也同時查出來啊。80%的情況,查這個部門,就只是要訪問這個部門的信息就可以了。先查部門,同時要訪問里面的員工,那么這個時候只有在你要訪問里面的員工的時候,才會去數據庫里面查詢1000個員工。
最初級的緩存不一致問題及解決方案
問題:先更新數據庫,再刪除緩存。如果刪除緩存失敗了,那么會導致數據庫中是新數據,緩存中是舊數據,數據就出現了不一致。

解決思路:先刪除緩存,再更新數據庫。如果數據庫更新失敗了,那么數據庫中是舊數據,緩存中是空的,那么數據不會不一致。因為讀的時候緩存沒有,所以去讀了數據庫中的舊數據,然后更新到緩存中。
比較復雜的數據不一致問題分析
數據發生了變更,先刪除了緩存,然后要去修改數據庫,此時還沒修改。一個請求過來,去讀緩存,發現緩存空了,去查詢數據庫,查到了修改前的舊數據,放到了緩存中。隨后數據變更的程序完成了數據庫的修改。完了,數據庫和緩存中的數據不一樣了...
為什么上億流量高並發場景下,緩存會出現這個問題?
只有在對一個數據在並發的進行讀寫的時候,才可能會出現這種問題。其實如果說你的並發量很低的話,特別是讀並發很低,每天訪問量就 1 萬次,那么很少的情況下,會出現剛才描述的那種不一致的場景。但是問題是,如果每天的是上億的流量,每秒並發讀是幾萬,每秒只要有數據更新的請求,就可能會出現上述的數據庫+緩存不一致的情況。
解決方案如下:
更新數據的時候,根據數據的唯一標識,將操作路由之后,發送到一個 jvm 內部隊列中。讀取數據的時候,如果發現數據不在緩存中,那么將重新讀取數據+更新緩存的操作,根據唯一標識路由之后,也發送同一個jvm 內部隊列中。
一個隊列對應一個工作線程,每個工作線程串行拿到對應的操作,然后一條一條的執行。這樣的話一個數據變更的操作,先刪除緩存,然后再去更新數據庫,但是還沒完成更新。此時如果一個讀請求過來,沒有讀到緩存,那么可以先將緩存更新的請求發送到隊列中,此時會在隊列中積壓,然后同步等待緩存更新完成。
這里有一個優化點,一個隊列中,其實多個更新緩存請求串在一起是沒意義的,因此可以做過濾,如果發現隊列中已經有一個更新緩存的請求了,那么就不用再放個更新請求操作進去了,直接等待前面的更新操作請求完成即可。
待那個隊列對應的工作線程完成了上一個操作的數據庫的修改之后,才會去執行下一個操作,也就是緩存更新的操作,此時會從數據庫中讀取最新的值,然后寫入緩存中。
如果請求還在等待時間范圍內,不斷輪詢發現可以取到值了,那么就直接返回;如果請求等待的時間超過一定時長,那么這一次直接從數據庫中讀取當前的舊值。
高並發的場景下,該解決方案要注意的問題:
(1)讀請求長時阻塞
由於讀請求進行了非常輕度的異步化,所以一定要注意讀超時的問題,每個讀請求必須在超時時間范圍內返回。該解決方案,最大的風險點在於說,可能數據更新很頻繁,導致隊列中積壓了大量更新操作在里面,然后讀請求會發生大量的超時,最后導致大量的請求直接走數據庫。務必通過一些模擬真實的測試,看看更新數據的頻率是怎樣的。
另外一點,因為一個隊列中,可能會積壓針對多個數據項的更新操作,因此需要根據自己的業務情況進行測試,可能需要部署多個服務,每個服務分攤一些數據的更新操作。如果一個內存隊列里居然會擠壓 100 個商品的庫存修改操作,每隔庫存修改操作要耗費 10ms 去完成,那么最后一個商品的讀請求,可能等待 10 *100 = 1000ms = 1s 后,才能得到數據,這個時候就導致讀請求的長時阻塞。
一定要做根據實際業務系統的運行情況,去進行一些壓力測試,和模擬線上環境,去看看最繁忙的時候,內存隊列可能會擠壓多少更新操作,可能會導致最后一個更新操作對應的讀請求,會 hang 多少時間,如果讀請求在 200ms 返回,如果你計算過后,哪怕是最繁忙的時候,積壓 10 個更新操作,最多等待 200ms,那還可以的。
如果一個內存隊列中可能積壓的更新操作特別多,那么你就要加機器,讓每個機器上部署的服務實例處理更少的數據,那么每個內存隊列中積壓的更新操作就會越少。
其實根據之前的項目經驗,一般來說,數據的寫頻率是很低的,因此實際上正常來說,在隊列中積壓的更新操作應該是很少的。像這種針對讀高並發、讀緩存架構的項目,一般來說寫請求是非常少的,每秒的 QPS 能到幾百就不錯了。
我們來實際粗略測算一下。
如果一秒有 500 的寫操作,如果分成 5 個時間片,每 200ms 就 100 個寫操作,放到 20 個內存隊列中,每個內存隊列,可能就積壓 5 個寫操作。每個寫操作性能測試后,一般是在 20ms 左右就完成,那么針對每個內存隊列的數據的讀請求,也就最多 hang 一會兒,200ms 以內肯定能返回了。
經過剛才簡單的測算,我們知道,單機支撐的寫 QPS 在幾百是沒問題的,如果寫 QPS 擴大了 10 倍,那么就擴容機器,擴容 10 倍的機器,每個機器 20 個隊列。
(2)讀請求並發量過高
這里還必須做好壓力測試,確保恰巧碰上上述情況的時候,還有一個風險,就是突然間大量讀請求會在幾十 毫秒的延時 hang 在服務上,看服務能不能扛的住,需要多少機器才能扛住最大的極限情況的峰值。
但是因為並不是所有的數據都在同一時間更新,緩存也不會同一時間失效,所以每次可能也就是少數數據的緩存失效了,然后那些數據對應的讀請求過來,並發量應該也不會特別大。
(3)多服務實例部署的請求路由
可能這個服務部署了多個實例,那么必須保證說,執行數據更新操作,以及執行緩存更新操作的請求,都通過 Nginx 服務器路由到相同的服務實例上。
比如說,對同一個商品的讀寫請求,全部路由到同一台機器上。可以自己去做服務間的按照某個請求參數的hash 路由,也可以用 Nginx 的 hash 路由功能等等。
(4)熱點商品的路由問題,導致請求的傾斜
萬一某個商品的讀寫請求特別高,全部打到相同的機器的相同的隊列里面去了,可能會造成某台機器的壓力過大。就是說,因為只有在商品數據更新的時候才會清空緩存,然后才會導致讀寫並發,所以其實要根據業務系統去看,如果更新頻率不是太高的話,這個問題的影響並不是特別大,但是的確可能某些機器的負載會高一些。
10 為什么是刪除緩存,而不是更新緩存?
原因很簡單,很多時候,在復雜點的緩存場景,緩存不單單是數據庫中直接取出來的值。
比如可能更新了某個表的一個字段,然后其對應的緩存,是需要查詢另外兩個表的數據並進行運算,才能計算出緩存最新的值的。
另外更新緩存的代價有時候是很高的。是不是說,每次修改數據庫的時候,都一定要將其對應的緩存更新一份?也許有的場景是這樣,但是對於比較復雜的緩存數據計算的場景,就不是這樣了。
如果你頻繁修改一個緩存涉及的多個表,緩存也頻繁更新。但是問題在於,這個緩存到底會不會被頻繁訪問到?
舉個栗子,一個緩存涉及的表的字段,在 1 分鍾內就修改了 20 次,或者是 100 次,那么緩存更新 20 次、100 次;但是這個緩存在 1 分鍾內只被讀取了 1 次,有大量的冷數據。
實際上,如果你只是刪除緩存的話,那么在 1 分鍾內,這個緩存不過就重新計算一次而已,開銷大幅度降低。用到緩存才去算緩存。
其實刪除緩存,而不是更新緩存,就是一個 lazy 計算的思想,不要每次都重新做復雜的計算,不管它會不會用到,而是讓它到需要被使用的時候再重新計算。像 mybatis,hibernate,都有懶加載思想。
查詢一個部門,部門帶了一個員工的 list,沒有必要說每次查詢部門,都里面的 1000 個員工的數據也同時查出來啊。80%的情況,查這個部門,就只是要訪問這個部門的信息就可以了。先查部門,同時要訪問里面的員工,那么這個時候只有在你要訪問里面的員工的時候,才會去數據庫里面查詢1000個員工。
2)最初級的緩存不一致問題及解決方案
問題:先更新數據庫,再刪除緩存。如果刪除緩存失敗了,那么會導致數據庫中是新數據,緩存中是舊數據,數據就出現了不一致。

解決思路:先刪除緩存,再更新數據庫。如果數據庫更新失敗了,那么數據庫中是舊數據,緩存中是空的,那么數據不會不一致。因為讀的時候緩存沒有,所以去讀了數據庫中的舊數據,然后更新到緩存中。
3)比較復雜的數據不一致問題分析
數據發生了變更,先刪除了緩存,然后要去修改數據庫,此時還沒修改。一個請求過來,去讀緩存,發現緩存空了,去查詢數據庫,查到了修改前的舊數據,放到了緩存中。隨后數據變更的程序完成了數據庫的修改。
完了,數據庫和緩存中的數據不一樣了…
11、redis 的並發競爭問題是什么?如何解決這個問題?了解redis 事務的 CAS 方案嗎?
面試官心理分析
這個也是線上非常常見的一個問題,就是多客戶端同時並發寫一個 key,可能本來應該先到的數據后到了,導致數據版本錯了;或者是多客戶端同時獲取一個 key,修改值之后再寫回去,只要順序錯了,數據就錯了。
而且 redis 自己就有天然解決這個問題的 CAS 類的樂觀鎖方案。
Optimistic locking using check-and-set(樂觀鎖)
樂觀鎖介紹:
watch指令在redis事物中提供了CAS的行為。為了檢測被watch的keys在是否有多個clients同時改變引起沖突,這些keys將會被監控。如果至少有一個被監控的key在執行exec命令前被修改,整個事物將會回滾,不執行任何動作,從而保證原子性操作,並且執行exec會得到null的回復。
樂觀鎖工作機制:
watch 命令會監視給定的每一個key,當exec時如果監視的任一個key自從調用watch后發生過變化,則整個事務會回滾,不執行任何動作。注意watch的key是對整個連接有效的,事務也一樣。如果連接斷開,監視和事務都會被自動清除。當然exec,discard,unwatch命令,及客戶端連接關閉都會清除連接中的所有監視。還有,如果watch一個不穩定(有生命周期)的key並且此key自然過期,exec仍然會執行事務隊列的指令。


redis的Watch機制是什么?
Redis Watch 命令用於監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那么事務將被打斷。注意使用multi 開始事務,exec 提交事務。
語法, redis Watch 命令基本語法如下:
WATCH key [key …]
驗證:首先開啟兩個redis客戶端,客戶端1和客戶端2.
- 1、客戶端1中,先set一個值
redis 127.0.0.1:6379> set number 10
OK
- 2、客戶端1開啟Watch 此值。
redis 127.0.0.1:6379> watch number
OK
- 3、客戶端1開啟事務,修改此值
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379> set number 100
QUEUED
redis 127.0.0.1:6379> get number
QUEUED
redis 127.0.0.1:6379>
注意此時先不要exec執行
- 4、客戶端2,去修改此值
redis 127.0.0.1:6379> set number 500
OK
- 5、客戶端1,執行
exec執行
redis 127.0.0.1:6379> exec
(nil)
redis 127.0.0.1:6379> get number
"500"
發現為nil,執行未成功,獲取的值為客戶端2修改后的值。
11、生產環境中的 redis 是怎么部署的?
面試官心理分析析
看看你了解不了解你們公司的 redis 生產集群的部署架構,如果你不了解,那么確實你就很失職了,你的redis 是主從架構?集群架構?用了哪種集群方案?有沒有做高可用保證?有沒有開啟持久化機制確保可以進行數據恢復?線上 redis 給幾個 G 的內存?設置了哪些參數?壓測后你們 redis 集群承載多少QPS?
兄弟,這些你必須是門兒清的,否則你確實是沒好好思考過。
面試題剖析
redis cluster,10 台機器,5 台機器部署了 redis 主實例,另外 5 台機器部署了 redis 的從實例, 每個主實例掛了一個從實例,5 個節點對外提供讀寫服務,每個節點的讀寫高峰 qps 可能可以達到每秒 5 萬,5 台機器最多是 25 萬讀寫請求/s。
機器是什么配置?32G 內存+ 8 核 CPU + 1T 磁盤,但是分配給 redis 進程的是 10g 內存,一般線上生產環境,redis 的內存盡量不要超過 10g,超過 10g 可能會有問題。
5 台機器對外提供讀寫,一共有 50g 內存。
因為每個主實例都掛了一個從實例,所以是高可用的,任何一個主實例宕機,都會自動故障遷移,redis 從實例會自動變成主實例繼續提供讀寫服務。
你往內存里寫的是什么數據?每條數據的大小是多少?商品數據,每條數據是 10kb。100 條數據是 1mb,10 萬條數據是 1g。常駐內存的是 200 萬條商品數據,占用內存是 20g,僅僅不到總內存的 50%。目前高峰期每秒就是 3500 左右的請求量。
其實大型的公司,會有基礎架構的 team 負責緩存集群的運維。
Redis高級試題
12.了解什么是 Redis 的雪崩、穿透和擊穿?Redis 崩潰之后會怎么樣?系統該如何應對這種情況?如何處理 Redis 的穿透?
緩存雪崩
對於系統 A,假設每天高峰期每秒 5000 個請求,本來緩存在高峰期可以扛住每秒 4000 個請求,但是緩存機器意外發生了全盤宕機。緩存掛了,此時 1 秒 5000 個請求全部落數據庫,數據庫必然扛不住,它會報一下警,然后就掛了。此時,如果沒有采用什么特別的方案來處理這個故障,DBA 很着急,重啟數據庫,但是數據庫立馬又被新的流量給打死了。

緩存雪崩的事前事中事后的解決方案如下:
- 事前:Redis 高可用,主從+哨兵,Redis cluster,避免全盤崩潰。
- 事中:本地 ehcache 緩存 + hystrix 限流&降級,避免 MySQL 被打死。
- 事后:Redis 持久化,一旦重啟,自動從磁盤上加載數據,快速恢復緩存數據。

用戶發送一個請求,系統 A 收到請求后,先查本地 ehcache 緩存,如果沒查到再查 Redis。如果 ehcache 和 Redis 都沒有,再查數據庫,將數據庫中的結果,寫入 ehcache 和 Redis 中。
限流組件,可以設置每秒的請求,有多少能通過組件,剩余的未通過的請求,怎么辦?走降級!可以返回一些默認的值,或者友情提示,或者空值。
好處:
- 數據庫絕對不會死,限流組件確保了每秒只有多少個請求能通過。
- 只要數據庫不死,就是說,對用戶來說,2/5 的請求都是可以被處理的。
- 只要有 2/5 的請求可以被處理,就意味着你的系統沒死,對用戶來說,可能就是點擊幾次刷不出來頁面,但是多點幾次,就可以刷出來了。
緩存穿透
對於系統A,假設一秒 5000 個請求,結果其中 4000 個請求是黑客發出的惡意攻擊。
黑客發出的那 4000 個攻擊,緩存中查不到,每次你去數據庫里查,也查不到。
舉個栗子。數據庫 id 是從 1 開始的,結果黑客發過來的請求 id 全部都是負數。這樣的話,緩存中不會有,請求每次都“繞過緩存”,直接查詢數據庫。這種惡意攻擊場景的緩存穿透就會直接把數據庫給打死。

解決方式很簡單,每次系統 A 從數據庫中只要沒查到,就寫一個空值到緩存里去,比如 set -999 UNKNOWN 。然后設置一個過期時間,這樣的話,下次有相同的 key 來訪問的時候,在緩存失效之前,都可以直接從緩存中取數據。
這種方式雖然是簡單,在某些場景(如數據量大的博客)下不優雅,還可能會緩存過多的空值,更加優雅的方式就是:使用 bitmap 布隆過濾
緩存擊穿
緩存擊穿,就是說某個 key 非常熱點,訪問非常頻繁,處於集中式高並發訪問的情況,當這個 key 在失效的瞬間,大量的請求就擊穿了緩存,直接請求數據庫,就像是在一道屏障上鑿開了一個洞。
不同場景下的解決方式可如下:
- 若緩存的數據是基本不會發生更新的,則可嘗試將該熱點數據設置為永不過期。
- 若緩存的數據更新不頻繁,且緩存刷新的整個流程耗時較少的情況下,則可以采用基於 Redis、zookeeper 等分布式中間件的分布式互斥鎖,或者本地互斥鎖以保證僅少量的請求能請求數據庫並重新構建緩存,其余線程則在鎖釋放后能訪問到新緩存。
- 若緩存的數據更新頻繁或者在緩存刷新的流程耗時較長的情況下,可以利用定時線程在緩存過期前主動地重新構建緩存或者延后緩存的過期時間,以保證所有的請求能一直訪問到對應的緩存。
緩存擊穿和 緩存穿透這兩者的區別:
- 緩存擊穿重點在“擊” 就是某個或者是幾個熱點 key 穿透了緩存層
- 緩存穿透重點在“透”:大量的請求繞過了緩存層
Redis布隆過濾器防惡意流量擊穿緩存
什么是惡意流量穿透
假設我們的Redis里存有一組用戶的注冊email,以email作為Key存在,同時它對應着DB里的User表的部分字段。
一般來說,一個合理的請求過來我們會先在Redis里判斷這個用戶是否是會員,因為從緩存里讀數據返回快。如果這個會員在緩存中不存在那么我們會去DB中查詢一下。
現在試想,有千萬個不同IP的請求(不要以為沒有,我們就在2018年和2019年碰到了,因為攻擊的成本很低)帶着Redis里根本不存在的key來訪問你的網站,這時我們來設想一下:
- 請求到達Web服務器;
- 請求派發到應用層->微服務層;
- 請求去Redis撈數據,Redis內不存在這個Key;
- 於是請求到達DB層,在DB建立connection后進行一次查詢
千萬乃至上億的DB連接請求,先不說Redis是否撐的住DB也會被瞬間打爆。這就是“Redis穿透”,它會打爆你的緩存或者是連DB一起打爆進而引起一系列的“雪崩效應”。

怎么防
那就是使用布隆過濾器,可以把所有的user表里的關鍵查詢字段放於Redis的bloom過濾器內。有人會說,這不瘋了,我有4000萬會員?so what!
你把4000會員放在Redis里是比較誇張,有些網站有8000萬、1億會員呢?
因此我沒讓你直接放在Redis里,而是放在布隆過濾器內!
布隆過濾器內不是直接把key,value這樣放進去的,它存放的內容是這么一個bitmap中。
bitmap
所謂的Bit-map就是用一個bit位來標記某個元素對應的Value,通過Bit為單位來存儲數據,可以大大節省存儲空間.
所以我們可以通過一個int型的整數的32比特位來存儲32個10進制的數字,那么這樣所帶來的好處是內存占用少、
效率很高(不需要比較和位移)比如我們要存儲5(101)、3(11)四個數字,那么我們申請int型的內存空間,會有32
個比特位。這四個數字的二進制分別對應
從右往左開始數,比如第一個數字是5,對應的二進制數據是101, 那么從右往左數到第5位,把對應的二進制數據
存儲到32個比特位上。
第一個5就是 00000000000000000000000000101000
輸入3時候 00000000000000000000000000001100
Bloom Filte介紹
1. 含義
(1). 布隆過濾器(Bloom Filter)是由Howard Bloom在1970年提出的一種比較巧妙的概率型數據結構,它實際上是由一個很長的二進制(0或1)向量和一系列隨機映射函數組成。
(2). 布隆過濾器可以用於檢索一個元素是否在一個集合中。它可以告訴你某種東西一定不存在或者可能存在。當布隆過濾器說,某種東西存在時,這種東西可能不存在;當布隆過濾器說,某種東西不存在時,那么這種東西一定不存在。
(3). 布隆過濾器 優點:A. 空間效率高,占用空間少 B. 查詢時間短
缺點:A. 有一定的誤判率 B. 元素不能刪除
2. 原理
當一個元素被加入集合時,通過K個散列函數將這個元素映射成一個位數組中的K個點(使用多個哈希函數對元素key (bloom中不存value) 進行哈希,算出一個整數索引值,然后對位數組長度進行取模運算得到一個位置,每個無偏哈希函數都會得到一個不同的位置),把它們置為1。
檢索時,我們只要看看這些點是不是都是1就(大約)知道集合中有沒有它了:① 如果這些點有任何一個為0(如下圖的e),則被檢元素一定不在;如果都是1(如下圖的d),並不能完全說明這個元素就一定存在其中,有可能這些位置為1是因為其他元素的存在,這就是布隆過濾器會出現誤判的原因。
如下圖:

補充:
Bloom Filter跟 ‘’單哈希函數BitMap‘’ 不同之處在於:Bloom Filter使用了k個哈希函數,每個字符串跟k個bit對應,從而降低了沖突的概率。
3. 實現
(1). Redis的bitmap
基於redis的 bitmap數據結構 的相關指令來執行。
(2). RedisBloom (推薦)
布隆過濾器可以使用Redis中的位圖(bitmap)操作實現,直到Redis4.0版本提供了插件功能,Redis官方提供的布隆過濾器才正式登場,布隆過濾器作為一個插件加載到Redis Server中,官網推薦了一個 RedisBloom 作為 Redis 布隆過濾器的 Module。
詳細安裝、指令操作參考:https://github.com/RedisBloom/RedisBloom
文檔地址:https://oss.redislabs.com/redisbloom/
(3). PyreBloom
pyreBloom 是 Python 中 Redis + BloomFilter 模塊,是c語言實現。如果覺得Redis module的形式部署很麻煩或者線上環境Redis版本不是 4.0 及以上,則可以采用這個,但是它是在 hiredis 基礎上,需要安裝hiredis,且不支持重連和重試。
(4). Lua腳本實現
詳見:https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter
(5). guvua包自帶的布隆過濾器
Bloom Filte 應用場景
1. 解決緩存穿透
(1). 含義
業務請求中數據緩存中沒有,DB中也沒有,導致類似請求直接跨過緩存,反復在DB中查詢,與此同時緩存也不會得到更新。(詳見:https://www.cnblogs.com/yaopengfei/p/13878124.html)
(2). 解決思路
事先把存在的key都放到redis的Bloom Filter 中,他的用途就是存在性檢測,如果 BloomFilter 中不存在,那么數據一定不存在;如果 BloomFilter 中存在,實際數據也有可能會不存在。
剖析:****布隆過濾器可能會誤判,放過部分請求,當不影響整體,所以目前該方案是處理此類問題最佳方案。
2. 黑名單校驗
識別垃圾郵件,只要發送者在黑名單中的,就識別為垃圾郵件。假設黑名單的數量是數以億計的,存放起來就是非常耗費存儲空間的,布隆過濾器則是一個較好的解決方案。把所有黑名單都放在布隆過濾器中,再收到郵件時,判斷郵件地址是否在布隆過濾器中即可。
ps:
如果用哈希表,每存儲一億個 email地址,就需要 1.6GB的內存(用哈希表實現的具體辦法是將每一個 email地址對應成一個八字節的信息指紋,然后將這些信息指紋存入哈希表,由於哈希表的存儲效率一般只有 50%,因此一個 email地址需要占用十六個字節。一億個地址大約要 1.6GB,即十六億字節的內存)。因此存貯幾十億個郵件地址可能需要上百 GB的內存。而Bloom Filter只需要哈希表 1/8到 1/4 的大小就能解決同樣的問題。
3. Web攔截器
(1). 含義
如果相同請求則攔截,防止重復被攻擊。
(2). 解決思路
用戶第一次請求,將請求參數放入布隆過濾器中,當第二次請求時,先判斷請求參數是否被布隆過濾器命中,從而提高緩存命中率。
布隆過濾器其他場景
比如有如下幾個需求:
1、原本有10億個號碼,現在又來了10萬個號碼,要快速准確判斷這10萬個號碼是否在10億個號碼庫中?
解決辦法一:將10億個號碼存入數據庫中,進行數據庫查詢,准確性有了,但是速度會比較慢。
解決辦法二:將10億號碼放入內存中,比如Redis緩存中,這里我們算一下占用內存大小:10億*8字節=8GB,通過內存查詢,准確性和速度都有了,但是大約8gb的內存空間,挺浪費內存空間的。
2、接觸過爬蟲的,應該有這么一個需求,需要爬蟲的網站千千萬萬,對於一個新的網站url,我們如何判斷這個url我們是否已經爬過了?
解決辦法還是上面的兩種,很顯然,都不太好。
3、同理還有垃圾郵箱的過濾。
那么對於類似這種,大數據量集合,如何准確快速的判斷某個數據是否在大數據量集合中,並且不占用內存,布隆過濾器應運而生了。
4、假設我用python爬蟲爬了4億條url,需要去重?
給Redis安裝Bloom Filter
Redis從4.0才開始支持bloom filter,因此本例中我們使用的是Redis5.4。
Redis的bloom filter下載地址在這:https://github.com/RedisLabsModules/redisbloom.git
git clone https://github.com/RedisLabsModules/redisbloom.git
cd redisbloom
make # 編譯
讓Redis啟動時可以加載bloom filter有兩種方式:
手工加載式:
redis-server --loadmodule ./redisbloom/rebloom.so
每次啟動自加載:
編輯Redis的redis.conf文件,加入:
loadmodule /soft/redisbloom/redisbloom.so
Like this:

在Redis里使用Bloom Filter
基本指令:
bf.reserve {key} {error_rate} {size}
127.0.0.1:6379> bf.reserve userid 0.01 100000
OK
上面這條命令就是:創建一個空的布隆過濾器,並設置一個期望的錯誤率和初始大小。{error_rate}過濾器的錯誤率在0-1之間,如果要設置0.1%,則應該是0.001。該數值越接近0,內存消耗越大,對cpu利用率越高。
bf.add {key} {item}
127.0.0.1:6379> bf.add userid '181920'
(integer) 1
上面這條命令就是:往過濾器中添加元素。如果key不存在,過濾器會自動創建。
bf.exists {key} {item}
127.0.0.1:6379> bf.exists userid '101310299'
(integer) 1
上面這條命令就是:判斷指定key的value是否在bloomfilter里存在。存在:返回1,不存在:返回0。
引入Redis布隆過濾器防止緩存穿透
緩存穿透(大量查詢一個不存在的key)定義:
緩存穿透,是指查詢一個數據庫中不一定存在的數據;
正常使用緩存查詢數據的流程是,依據key去查詢value,數據查詢先進行緩存查詢,如果key不存在或者key已經過期,再對數據庫進行查詢,並把查詢到的對象,放進緩存。如果數據庫查詢對象為空,則不放進緩存。
如果每次都查詢一個不存在value的key,由於緩存中沒有數據,所以每次都會去查詢數據庫;當對key查詢的並發請求量很大時,每次都訪問DB,很可能對DB造成影響;並且由於緩存不命中,每次都查詢持久層,那么也失去了緩存的意義。
緩存穿透 解決方法
第一種是緩存層緩存空值
將數據庫中的空值也緩存到緩存層中,這樣查詢該空值就不會再訪問DB,而是直接在緩存層訪問就行。
但是這樣有個弊端就是緩存太多空值占用了更多的空間,可以通過給緩存層空值設立一個較短的過期時間來解決,例如60s。
第二種是布隆過濾器
將數據庫中所有的查詢條件,放入布隆過濾器中,
當一個查詢請求過來時,先經過布隆過濾器進行查,如果判斷請求查詢值存在,則繼續查;如果判斷請求查詢不存在,直接丟棄。
這里看Bloom Filter。
我們先看看一般業務緩存流程:

先查詢緩存,緩存不命中再查詢數據庫。 然后將查詢結果放在緩存中即使數據不存在,也需要創建一個緩存,用來防止穿庫。這里需要區分一下數據是否存在。 如果數據不存在,緩存時間可以設置相對較短,防止因為主從同步等問題,導致問題被放大。
這個流程中存在薄弱的問題是,當用戶量太大時,我們會緩存大量數據空數據,並且一旦來一波冷用戶,會造成雪崩效應。 對於這種情況,我們產生第二個版本流程:redis過濾冷用戶緩存流程

我們將數據庫里面,命中的用戶放在redis的set類型中,設置不過期。 這樣相當把redis當作數據庫的索引,只要查詢redis,就可以知道是否數據存在。 redis中不存在就可以直接返回結果。 如果存在就按照上面提到一般業務緩存流程處理。
聰明的你肯定會想到更多的問題:
- redis本身可以做緩存,為什么不直接返回數據呢?
- 如果數據量比較大,單個set,會有性能問題?
- 業務不重要,將全量數據放在redis中,占用服務器大量內存。投入產出不成比例?
問題1需要區分業務場景,結果數據少,我們是可以直接使用redis作為緩存,直接返回數據。 結果比較大就不太適合用redis存放了。比如ugc內容,一個評論里面可能存在上萬字,業務字段多。
redis使用有很多技巧。bigkey 危害比較大,無論是擴容或縮容帶來的內存申請釋放, 還是查詢命令使用不當導致大量數據返回,都會影響redis的穩定。這里就不細談原因及危害了。 解決bigkey 方法很簡單。我們可以使用hash函數來分桶,將數據分散到多個key中。 減少單個key的大小,同時不影響查詢效率。
問題3是redis存儲占用內存太大。因此我們需要減少內存使用。 重新思考一下引入redis的目的。 redis像一個集合,整個業務就是驗證請求的參數是否在集合中。

這個結構就像洗澡的時候用的雙向閥門:左邊熱水,右邊冷水。大部分的編程語言都內置了filter。 拿python舉例,filter函數用於過濾序列, 過濾掉不符合條件的元素,返回由符合條件元素組成的列表。
12. Redis 的並發競爭問題是什么?如何解決這個問題?了解 Redis 事務的 CAS 方案嗎?
簡單的講:就是多客戶端同時並發寫一個 key,可能本來應該先到的數據后到了,導致數據版本錯了;或者是多客戶端同時獲取一個 key,修改值之后再寫回去,只要順序錯了,數據就錯了。
而且 Redis 自己就有天然解決這個問題的 CAS 類的樂觀鎖方案,使用版本號進行控制,cas的思想這里就不詳細說了。
13. Redis 集群模式的工作原理能說一下么?在集群模式下,Redis 的 key 是如何尋址的?分布式尋址都有哪些算法?了解一致性 hash 算法嗎?
Redis cluster 介紹
- 自動將數據進行分片,每個 master 上放一部分數據
- 提供內置的高可用支持,部分 master 不可用時,還是可以繼續工作的
在 Redis cluster 架構下,每個 Redis 要放開兩個端口號,比如一個是 6379,另外一個就是 加1w 的端口號,比如 16379。
16379 端口號是用來進行節點間通信的,也就是 cluster bus 的東西,cluster bus 的通信,用來進行故障檢測、配置更新、故障轉移授權。cluster bus 用了另外一種二進制的協議, gossip 協議,用於節點間進行高效的數據交換,占用更少的網絡帶寬和處理時間。
集群節點間的內部通信機制
基本通信原理
集群元數據的維護有兩種方式:集中式、Gossip 協議。Redis cluster 節點間采用 gossip 協議進行通信。
集中式是將集群元數據(節點信息、故障等等)幾種存儲在某個節點上。集中式元數據集中存儲的一個典型代表,就是大數據領域的 storm 。它是分布式的大數據實時計算引擎,是集中式的元數據存儲的結構,底層基於 zookeeper(分布式協調的中間件)對所有元數據進行存儲維護。

Redis 維護集群元數據采用另一個方式, gossip 協議,所有節點都持有一份元數據,不同的節點如果出現了元數據的變更,就不斷將元數據發送給其它的節點,讓其它節點也進行元數據的變更。

集中式的好處在於,元數據的讀取和更新,時效性非常好,一旦元數據出現了變更,就立即更新到集中式的存儲中,其它節點讀取的時候就可以感知到;不好在於,所有的元數據的更新壓力全部集中在一個地方,可能會導致元數據的存儲有壓力。
gossip 好處在於,元數據的更新比較分散,不是集中在一個地方,更新請求會陸陸續續打到所有節點上去更新,降低了壓力;不好在於,元數據的更新有延時,可能導致集群中的一些操作會有一些滯后。
- 10000 端口:每個節點都有一個專門用於節點間通信的端口,就是自己提供服務的端口號+10000,比如 7001,那么用於節點間通信的就是 17001 端口。每個節點每隔一段時間都會往另外幾個節點發送
ping消息,同時其它幾個節點接收到ping之后返回pong。 - 交換的信息:信息包括故障信息,節點的增加和刪除,hash slot 信息等等。
gossip 協議
gossip 協議包含多種消息,包含 ping , pong , meet , fail 等等。
- meet:某個節點發送 meet 給新加入的節點,讓新節點加入集群中,然后新節點就會開始與其它節點進行通信。
Redis-trib.rb add-node
1
其實內部就是發送了一個 gossip meet 消息給新加入的節點,通知那個節點去加入我們的集群。
- ping:每個節點都會頻繁給其它節點發送 ping,其中包含自己的狀態還有自己維護的集群元數據,互相通過 ping 交換元數據。
- pong:返回 ping 和 meeet,包含自己的狀態和其它信息,也用於信息廣播和更新。
- fail:某個節點判斷另一個節點 fail 之后,就發送 fail 給其它節點,通知其它節點說,某個節點宕機啦。
ping 消息深入
ping 時要攜帶一些元數據,如果很頻繁,可能會加重網絡負擔。
每個節點每秒會執行 10 次 ping,每次會選擇 5 個最久沒有通信的其它節點。當然如果發現某個節點通信延時達到了 cluster_node_timeout / 2 ,那么立即發送 ping,避免數據交換延時過長,落后的時間太長了。比如說,兩個節點之間都 10 分鍾沒有交換數據了,那么整個集群處於嚴重的元數據不一致的情況,就會有問題。所以 cluster_node_timeout 可以調節,如果調得比較大,那么會降低 ping 的頻率。
每次 ping,會帶上自己節點的信息,還有就是帶上 1/10 其它節點的信息,發送出去,進行交換。至少包含 3 個其它節點的信息,最多包含 總節點數減 2 個其它節點的信息。
分布式尋址算法
- hash 算法(大量緩存重建)
- 一致性 hash 算法(自動緩存遷移)+ 虛擬節點(自動負載均衡)
- Redis cluster 的 hash slot 算法
hash 算法
來了一個 key,首先計算 hash 值,然后對節點數取模。然后打在不同的 master 節點上。一旦某一個 master 節點宕機,所有請求過來,都會基於最新的剩余 master 節點數去取模,嘗試去取數據。這會導致大部分的請求過來,全部無法拿到有效的緩存,導致大量的流量涌入數據庫。

一致性 hash 算法
一致性 hash 算法將整個 hash 值空間組織成一個虛擬的圓環,整個空間按順時針方向組織,下一步將各個 master 節點(使用服務器的 ip 或主機名)進行 hash。這樣就能確定每個節點在其哈希環上的位置。
來了一個 key,首先計算 hash 值,並確定此數據在環上的位置,從此位置沿環順時針“行走”,遇到的第一個 master 節點就是 key 所在位置。
在一致性哈希算法中,如果一個節點掛了,受影響的數據僅僅是此節點到環空間前一個節點(沿着逆時針方向行走遇到的第一個節點)之間的數據,其它不受影響。增加一個節點也同理。
燃鵝,一致性哈希算法在節點太少時,容易因為節點分布不均勻而造成緩存熱點的問題。為了解決這種熱點問題,一致性 hash 算法引入了虛擬節點機制,即對每一個節點計算多個 hash,每個計算結果位置都放置一個虛擬節點。這樣就實現了數據的均勻分布,負載均衡。

Redis cluster 的 hash slot 算法
Redis cluster 有固定的 16384 個 hash slot,對每個 key 計算 CRC16 值,然后對 16384 取模,可以獲取 key 對應的 hash slot。
Redis cluster 中每個 master 都會持有部分 slot,比如有 3 個 master,那么可能每個 master 持有 5000 多個 hash slot。hash slot 讓 node 的增加和移除很簡單,增加一個 master,就將其他 master 的 hash slot 移動部分過去,減少一個 master,就將它的 hash slot 移動到其他 master 上去。移動 hash slot 的成本是非常低的。客戶端的 api,可以對指定的數據,讓他們走同一個 hash slot,通過 hash tag 來實現。
任何一台機器宕機,另外兩個節點,不影響的。因為 key 找的是 hash slot,不是機器。

14 Redis cluster 的高可用與主備切換原理
Redis cluster 的高可用的原理,幾乎跟哨兵是類似的。
判斷節點宕機
如果一個節點認為另外一個節點宕機,那么就是 pfail ,主觀宕機。如果多個節點都認為另外一個節點宕機了,那么就是 fail ,客觀宕機,跟哨兵的原理幾乎一樣,sdown,odown。
在 cluster-node-timeout 內,某個節點一直沒有返回 pong ,那么就被認為 pfail 。
如果一個節點認為某個節點 pfail 了,那么會在 gossip ping 消息中, ping 給其他節點,如果超過半數的節點都認為 pfail 了,那么就會變成 fail 。
從節點過濾
對宕機的 master node,從其所有的 slave node 中,選擇一個切換成 master node。
檢查每個 slave node 與 master node 斷開連接的時間,如果超過了 cluster-node-timeout * cluster-slave-validity-factor ,那么就沒有資格切換成 master 。
從節點選舉
每個從節點,都根據自己對 master 復制數據的 offset,來設置一個選舉時間,offset 越大(復制數據越多)的從節點,選舉時間越靠前,優先進行選舉。
所有的 master node 開始 slave 選舉投票,給要進行選舉的 slave 進行投票,如果大部分 master node (N/2 + 1) 都投票給了某個從節點,那么選舉通過,那個從節點可以切換成 master。
從節點執行主備切換,從節點切換為主節點。
與哨兵比較
整個流程跟哨兵相比,非常類似,所以說,Redis cluster 功能強大,直接集成了 replication 和 sentinel 的功能。
15. 如何保證緩存與數據庫的雙寫一致性?
要是系統嚴格要求 “緩存+數據庫” 必須保持一致性的話,可以使用:讀請求和寫請求串行化,串到一個內存隊列里去。串行化可以保證一定不會出現不一致的情況,但是它也會導致系統的吞吐量大幅度降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。另一種方式就是:Cache Aside Pattern
- 讀的時候,先讀緩存,緩存沒有的話,就讀數據庫,然后取出數據后放入緩存,同時返回響應。
- 更新的時候,先更新數據庫,然后再刪除緩存。
為什么是刪除緩存,而不是更新緩存?
原因很簡單,很多時候,在復雜點的緩存場景,緩存不單單是數據庫中直接取出來的值。
比如可能更新了某個表的一個字段,然后其對應的緩存,是需要查詢另外兩個表的數據並進行運算,才能計算出緩存最新的值的。
另外更新緩存的代價有時候是很高的。是不是說,每次修改數據庫的時候,都一定要將其對應的緩存更新一份?也許有的場景是這樣,但是對於比較復雜的緩存數據計算的場景,就不是這樣了。如果你頻繁修改一個緩存涉及的多個表,緩存也頻繁更新。但是問題在於,這個緩存到底會不會被頻繁訪問到?
舉個栗子,一個緩存涉及的表的字段,在 1 分鍾內就修改了 20 次,或者是 100 次,那么緩存更新 20 次、100 次;但是這個緩存在 1 分鍾內只被讀取了 1 次,有大量的冷數據。實際上,如果你只是刪除緩存的話,那么在 1 分鍾內,這個緩存不過就重新計算一次而已,開銷大幅度降低。用到緩存才去算緩存。
其實刪除緩存,而不是更新緩存,就是一個 lazy 計算的思想,不要每次都重新做復雜的計算,不管它會不會用到,而是讓它到需要被使用的時候再重新計算。像 mybatis,hibernate,都有懶加載思想。查詢一個部門,部門帶了一個員工的 list,沒有必要說每次查詢部門,都把里面的 1000 個員工的數據也同時查出來啊。80% 的情況,查這個部門,就只是要訪問這個部門的信息就可以了。先查部門,同時要訪問里面的員工,那么這個時候只有在你要訪問里面的員工的時候,才會去數據庫里面查詢 1000 個員工。
最初級的緩存不一致問題及解決方案
問題:先更新數據庫,再刪除緩存。如果刪除緩存失敗了,那么會導致數據庫中是新數據,緩存中是舊數據,數據就出現了不一致。

解決思路:先刪除緩存,再更新數據庫。如果數據庫更新失敗了,那么數據庫中是舊數據,緩存中是空的,那么數據不會不一致。因為讀的時候緩存沒有,所以去讀了數據庫中的舊數據,然后更新到緩存中。
比較復雜的數據不一致問題分析
數據發生了變更,先刪除了緩存,然后要去修改數據庫,此時還沒修改。一個請求過來,去讀緩存,發現緩存空了,去查詢數據庫,查到了修改前的舊數據,放到了緩存中。隨后數據變更的程序完成了數據庫的修改。完了,數據庫和緩存中的數據不一樣了…
為什么上億流量高並發場景下,緩存會出現這個問題?
只有在對一個數據在並發的進行讀寫的時候,才可能會出現這種問題。其實如果說你的並發量很低的話,特別是讀並發很低,每天訪問量就 1 萬次,那么很少的情況下,會出現剛才描述的那種不一致的場景。但是問題是,如果每天的是上億的流量,每秒並發讀是幾萬,每秒只要有數據更新的請求,就可能會出現上述的數據庫+緩存不一致的情況。
回到◀瘋狂創客圈▶
瘋狂創客圈 - Java高並發研習社群,為大家開啟大廠之門
