分析:博主以爲在項目中使用redis,主要是從兩個角度去考慮:性能和併發。固然,redis還具有能夠作分佈式鎖等其餘功能,可是若是隻是爲了分佈式鎖這些其餘功能,徹底還有其餘中間件(如zookpeer等)代替,並非非要使用redis。所以,這個問題主要從性能和併發兩個角度去答。
回答:以下所示,分爲兩點
(一)性能
咱們在碰到須要執行耗時特別久,且結果不頻繁變更的SQL,就特別適合將運行結果放入緩存。這樣,後面的請求就去緩存中讀取,使得請求可以迅速響應。html
(二)併發
在大併發的狀況下,全部的請求直接訪問數據庫,數據庫會出現鏈接異常。這個時候,就須要使用redis作一個緩衝操做,讓請求先訪問到redis,而不是直接訪問數據庫。node
瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備mysql
字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。
若是你是Redis中高級用戶,還須要加上下面幾種數據結構HyperLogLog、Geo、Pub/Sub。
若是你說還玩過Redis Module,像BloomFilter,RedisSearch,Redis-ML,面試官得眼睛就開始發亮了。面試
先拿setnx來爭搶鎖,搶到以後,再用expire給鎖加一個過時時間防止鎖忘記了釋放。
這時候對方會告訴你說你回答得不錯,而後接着問若是在setnx以後執行expire以前進程意外crash或者要重啓維護了,那會怎麼樣?
這時候你要給予驚訝的反饋:唉,是喔,這個鎖就永遠得不到釋放了。緊接着你須要抓一抓本身得腦殼,故做思考片刻,好像接下來的結果是你主動思考出來的,而後回答:我記得set指令有很是複雜的參數,這個應該是能夠同時把setnx和expire合成一條指令來用的!對方這時會顯露笑容,內心開始默唸:嗯,這小子還不錯。redis
使用keys指令能夠掃出指定模式的key列表。
對方接着追問:若是這個redis正在給線上的業務提供服務,那使用keys指令會有什麼問題?
這個時候你要回答redis關鍵的一個特性:redis的單線程的。keys指令會致使線程阻塞一段時間,線上服務會停頓,直到指令執行完畢,服務才能恢復。這個時候能夠使用scan指令,scan指令能夠無阻塞的提取出指定模式的key列表,可是會有必定的重複機率,在客戶端作一次去重就能夠了,可是總體所花費的時間會比直接用keys指令長。算法
通常使用list結構做爲隊列,rpush生產消息,lpop消費消息。當lpop沒有消息的時候,要適當sleep一會再重試。
若是對方追問可不能夠不用sleep呢?list還有個指令叫blpop,在沒有消息的時候,它會阻塞住直到消息到來。
若是對方追問能不能生產一次消費屢次呢?使用pub/sub主題訂閱者模式,能夠實現1:N的消息隊列。
若是對方追問pub/sub有什麼缺點?在消費者下線的狀況下,生產的消息會丟失,得使用專業的消息隊列如rabbitmq等。
若是對方追問redis如何實現延時隊列?我估計如今你很想把面試官一棒打死若是你手上有一根棒球棍的話,怎麼問的這麼詳細。可是你很剋制,而後神態自若的回答道:使用sortedset,拿時間戳做爲score,消息內容做爲key調用zadd來生產消息,消費者用zrangebyscore指令獲取N秒以前的數據輪詢進行處理。
到這裏,面試官暗地裏已經對你豎起了大拇指。可是他不知道的是此刻你卻豎起了中指,在椅子背後。sql
若是大量的key過時時間設置的過於集中,到過時的那個時間點,redis可能會出現短暫的卡頓現象。通常須要在時間上加一個隨機值,使得過時時間分散一些。數據庫
bgsave作鏡像全量持久化,aof作增量持久化。由於bgsave會耗費較長時間,不夠實時,在停機的時候會致使大量丟失數據,因此須要aof來配合使用。在redis實例重啓時,優先使用aof來恢復內存的狀態,若是沒有aof日誌,就會使用rdb文件來恢復。
若是再問aof文件過大恢復時間過長怎麼辦?你告訴面試官,Redis會按期作aof重寫,壓縮aof文件日誌大小。若是面試官不夠滿意,再拿出殺手鐗答案,Redis4.0以後有了混合持久化的功能,將bgsave的全量和aof的增量作了融合處理,這樣既保證了恢復的效率又兼顧了數據的安全性。這個功能甚至不少面試官都不知道,他們確定會對你另眼相看。
若是對方追問那若是忽然機器掉電會怎樣?取決於aof日誌sync屬性的配置,若是不要求性能,在每條寫指令時都sync一下磁盤,就不會丟失數據。可是在高性能的要求下每次都sync是不現實的,通常都使用定時sync,好比1s1次,這個時候最多就會丟失1s的數據。編程
能夠將屢次IO往返的時間縮減爲一次,前提是pipeline執行的指令之間沒有因果相關性。使用redis-benchmark進行壓測的時候能夠發現影響redis的QPS峯值的一個重要因素是pipeline批次指令的數目。api
從從同步。第一次同步時,主節點作一次bgsave,並同時將後續修改操做記錄到內存buffer,待完成後將rdb文件全量同步到複製節點,複製節點接受完成後將rdb鏡像加載到內存。加載完成後,再通知主節點將期間修改的操做記錄同步到複製節點進行重放就完成了同步過程。
Redis Sentinal着眼於高可用,在master宕機時會自動將slave提高爲master,繼續提供服務。
Redis Cluster着眼於擴展性,在單個redis內存不足時,使用Cluster進行分片存儲。
這個問題,互聯網公司必問,要是一我的連緩存都不太清楚,那確實比較尷尬。
只要問到緩存,上來第一個問題,確定是先問問你項目哪裏用了緩存?爲啥要用?不用行不行?若是用了之後可能會有什麼不良的後果?
這就是看看你對緩存這個東西背後有沒有思考,若是你就是傻乎乎的瞎用,無法給面試官一個合理的解答,那面試官對你印象確定不太好,以爲你平時思考太少,就知道幹活兒。
項目中緩存是如何使用的?
這個,須要結合本身項目的業務來。
爲何要用緩存?
用緩存,主要有兩個用途:高性能、高併發。
假設這麼個場景,你有個操做,一個請求過來,吭哧吭哧你各類亂七八糟操做 mysql,半天查出來一個結果,耗時 600ms。可是這個結果可能接下來幾個小時都不會變了,或者變了也能夠不用當即反饋給用戶。那麼此時咋辦?
緩存啊,折騰 600ms 查出來的結果,扔緩存裏,一個 key 對應一個 value,下次再有人查,別走 mysql折騰 600ms 了,直接從緩存裏,經過一個 key 查出來一個 value,2ms 搞定。性能提高 300 倍。
就是說對於一些須要複雜操做耗時查出來的結果,且肯定後面不怎麼變化,可是有不少讀請求,那麼直接將查詢出來的結果放在緩存中,後面直接讀緩存就好。
因此要是你有個系統,高峯期一秒鐘過來的請求有 1 萬,那一個 mysql 單機絕對會死掉。你這個時候就只能上緩存,把不少數據放緩存,別放 mysql。緩存功能簡單,說白了就是 key-value 式操做,單機支撐的併發量輕鬆一秒幾萬十幾萬,支撐高併發 so easy。單機承載併發量是 mysql 單機的幾十倍。
緩存是走內存的,內存自然就支撐高併發。
用了緩存以後會有什麼不良後果?
常見的緩存問題有如下幾個:
緩存與數據庫雙寫不一致 、緩存雪崩、緩存穿透、緩存併發競爭後面再詳細說明。
這個是問 redis 的時候,最基本的問題吧,redis 最基本的一個內部原理和特色,就是 redis 其實是個單線程工做模型,你要是這個都不知道,那後面玩兒 redis 的時候,出了問題豈不是什麼都不知道?
還有可能面試官會問問你 redis 和 memcached 的區別,可是 memcached 是早些年各大互聯網公司經常使用的緩存方案,可是如今近幾年基本都是 redis,沒什麼公司用 memcached 了。
redis 和 memcached 有啥區別?
redis 相比 memcached 來講,擁有更多的數據結構,能支持更豐富的數據操做。若是須要緩存可以支持更復雜的結構和操做, 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,會將產生事件的 socket 放入隊列中排隊,事件分派器每次從隊列中取出一個 socket,根據 socket 的事件類型交給對應的事件處理器進行處理。
來看客戶端與 redis 的一次通訊過程:
給緩存在 redis 裏,而後每次讀寫緩存的時候,能夠就操做 hash 裏的某個字段。
hset person name bingohset person age 20hset person id 1 hget person nameperson = { "name": "bingo", "age": 20, "id": 1}
list
list 是有序列表,這個能夠玩兒出不少花樣。
好比能夠經過 list 存儲一些列表型的數據結構,相似粉絲列表、文章的評論列表之類的東西。
好比能夠經過 lrange 命令,讀取某個閉區間內的元素,能夠基於 list 實現分頁查詢,這個是很棒的一個功能,基於 redis 實現簡單的高性能分頁,能夠作相似微博那種下拉不斷分頁的東西,性能高,就一頁一頁走。
# 0 開始位置,-1 結束位置,結束位置爲-1 時,表示列表的最後一個位置,即查看全部。lrange mylist 0 -1
好比能夠搞個簡單的消息隊列,從 list 頭懟進去,從 list 尾巴那裏弄出來。
lpush mylist 1lpush mylist 2lpush mylist 3 4 5# 1rpop mylist
set
set 是無序集合,自動去重。
直接基於 set 將系統裏須要去重的數據扔進去,自動就給去重了,若是你須要對一些數據進行快速的全局去重,你固然也能夠基於 jvm 內存裏的 HashSet 進行去重,可是若是你的某個系統部署在多臺機器上呢?
得基於 redis 進行全局的 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 的元素移動到另一個 setsmove yourSet mySet 2# 求兩 set 的交集 sinter yourSet mySet# 求兩 set 的並集 sunion yourSet mySet# 求在 yourSet 中而不在 mySet 中的元素 sdiff yourSet mySet
sorted set
sorted set 是排序的 set,去重但能夠排序,寫進去的時候給一個分數,自動根據分數排序。
zadd board 85 zhangsanzadd board 72 lisi zadd board 96 wangwuzadd board 63 zhaoliu# 獲取排名前三的用戶(默認是升序,因此須要 rev 改成降序) zrevrange board 0 3# 獲取某用戶的排名 zrank board zhaoliu
若是你連這個問題都不知道,上來就懵了,回答不出來,那線上你寫代碼的時候,想固然的認爲寫進 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 內存淘汰機制有如下幾個:
手寫一個 LRU 算法
你能夠現場手寫最原始的 LRU 算法,那個代碼量太大了,彷佛不太現實。
不求本身純手工從底層開始打造出本身的 LRU,可是起碼要知道如何利用已有的 JDK 數據結構實現一個Java 版的 LRU。
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; }}
其實問這個問題,主要是考考你,redis 單機能承載多高併發?若是單機扛不住如何擴容扛更多的併發?redis 會不會掛?既然 redis 會掛那怎麼保證 redis 是高可用的?
其實針對的都是項目中你確定要考慮的一些問題,若是你沒考慮過,那確實你對生產系統中的問題思考太少。
若是你用 redis 緩存技術的話,確定要考慮如何用 redis 來加多臺機器,保證 redis 是高併發的,還有就是如何讓 redis 保證本身不是掛掉之後就直接死掉了,即 redis 高可用。
因爲此節內容較多,所以,會分爲兩個小節進行講解。- redis 主從架構 - redis 基於哨兵實現高可用redis 實現高併發主要依靠主從架構,一主多從,通常來講,不少項目其實就足夠了,單主用來寫入數據,單機幾萬 QPS,多從用來查詢數據,多個從實例能夠提供每秒 10w 的 QPS。
若是想要在實現高併發的同時,容納大量的數據,那麼就須要 redis 集羣,使用 redis 集羣以後,能夠提供每秒幾十萬的讀寫併發。
redis 高可用,若是是作主從架構部署,那麼加上哨兵就能夠了,就能夠實現,任何一個實例宕機,能夠進行主備切換。
redis 若是僅僅只是將數據緩存在內存裏面,若是 redis 宕機了再重啓,內存裏的數據就所有都弄丟了啊。
你必須得用 redis 的持久化機制,將數據寫入內存的同時,異步的慢慢的將數據寫入磁盤文件裏,進行持久化。
若是 redis 宕機重啓,自動從磁盤上加載以前持久化的一些數據就能夠了,也許會丟失少量數據,可是至少不會將全部數據都弄丟。
這個其實同樣,針對的都是 redis 的生產環境可能遇到的一些問題,就是 redis 要是掛了再重啓,內存裏的數據不就全丟了?能不能重啓的時候把數據給恢復了?
持久化主要是作災難恢復、數據恢復,也能夠歸類到高可用的一個環節中去,好比你 redis 整個掛了,而後 redis 就不可用了,你要作的事情就是讓 redis 變得可用,儘快變得可用。
重啓 redis,儘快讓它對外提供服務,若是沒作數據備份,這時候 redis 啓動了,也不可用啊,數據都沒了。
極可能說,大量的請求過來,緩存所有沒法命中,在 redis 里根本找不到數據,這個時候就死定了,出現緩存雪崩問題。全部請求沒有在redis命中,就會去mysql數據庫這種數據源頭中去找,一會兒mysql承接高併發,而後就掛了…
若是你把 redis 持久化作好,備份和恢復方案作到企業級的程度,那麼即便你的 redis 故障了,也能夠經過備份數據,快速恢復,一旦恢復當即對外提供服務。
redis 持久化的兩種方式
經過 RDB 或 AOF,均可以將 redis 內存中的數據給持久化到磁盤上面來,而後能夠將這些數據備份到別的地方去,好比說阿里雲等雲服務。
若是 redis 掛了,服務器上的內存和磁盤上的數據都丟了,能夠從雲服務上拷貝回來以前的數據,放到指定的目錄中,而後從新啓動 redis,redis 就會自動根據持久化數據文件中的數據,去恢復內存中的數據,繼續對外提供服務。
若是同時使用 RDB 和 AOF 兩種持久化機制,那麼在 redis 重啓的時候,會使用 AOF 來從新構建數據,由於 AOF 中的數據更加完整。
RDB 優缺點
AOF 優缺點
RDB 和 AOF 到底該如何選擇
你只要用緩存,就可能會涉及到緩存與數據庫雙存儲雙寫,你只要是雙寫,就必定會有數據一致性的問題,那麼你如何解決一致性問題?#### 面試題剖析試題剖析
通常來講,若是容許緩存能夠稍微的跟數據庫偶爾有不一致的狀況,也就是說若是你的系統不是嚴格要求「緩存+數據庫」 必須保持一致性的話,最好不要作這個方案,即:讀請求和寫請求串行化,串到一個內存隊列裏去。
串行化能夠保證必定不會出現不一致的狀況,可是它也會致使系統的吞吐量大幅度下降,用比正常狀況下多幾倍的機器去支撐線上的一個請求。
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)熱點商品的路由問題,致使請求的傾斜
萬一某個商品的讀寫請求特別高,所有打到相同的機器的相同的隊列裏面去了,可能會形成某臺機器的壓力過大。就是說,由於只有在商品數據更新的時候纔會清空緩存,而後纔會致使讀寫併發,因此其實要根據業務系統去看,若是更新頻率不是過高的話,這個問題的影響並非特別大,可是的確可能某些機器的負載會高一些。
緣由很簡單,不少時候,在複雜點的緩存場景,緩存不僅僅是數據庫中直接取出來的值。
好比可能更新了某個表的一個字段,而後其對應的緩存,是須要查詢另外兩個表的數據並進行運算,才能計算出緩存最新的值的。
另外更新緩存的代價有時候是很高的。是否是說,每次修改數據庫的時候,都必定要將其對應的緩存更新一份?也許有的場景是這樣,可是對於比較複雜的緩存數據計算的場景,就不是這樣了。
若是你頻繁修改一個緩存涉及的多個表,緩存也頻繁更新。可是問題在於,這個緩存到底會不會被頻繁訪問到?
舉個栗子,一個緩存涉及的表的字段,在 1 分鐘內就修改了 20 次,或者是 100 次,那麼緩存更新 20 次、100 次;可是這個緩存在 1 分鐘內只被讀取了 1 次,有大量的冷數據。
實際上,若是你只是刪除緩存的話,那麼在 1 分鐘內,這個緩存不過就從新計算一次而已,開銷大幅度下降。用到緩存纔去算緩存。
其實刪除緩存,而不是更新緩存,就是一個 lazy 計算的思想,不要每次都從新作複雜的計算,無論它會不會用到,而是讓它到須要被使用的時候再從新計算。像 mybatis,hibernate,都有懶加載思想。
查詢一個部門,部門帶了一個員工的 list,沒有必要說每次查詢部門,都裏面的 1000 個員工的數據也同時查出來啊。80%的狀況,查這個部門,就只是要訪問這個部門的信息就能夠了。先查部門,同時要訪問裏面的員工,那麼這個時候只有在你要訪問裏面的員工的時候,纔會去數據庫裏面查詢1000個員工。
2)最初級的緩存不一致問題及解決方案
問題:先更新數據庫,再刪除緩存。若是刪除緩存失敗了,那麼會致使數據庫中是新數據,緩存中是舊數據,數據就出現了不一致。
解決思路:先刪除緩存,再更新數據庫。若是數據庫更新失敗了,那麼數據庫中是舊數據,緩存中是空的,那麼數據不會不一致。由於讀的時候緩存沒有,因此去讀了數據庫中的舊數據,而後更新到緩存中。
3)比較複雜的數據不一致問題分析
數據發生了變動,先刪除了緩存,而後要去修改數據庫,此時還沒修改。一個請求過來,去讀緩存,發現緩存空了,去查詢數據庫,查到了修改前的舊數據,放到了緩存中。隨後數據變動的程序完成了數據庫的修改。
完了,數據庫和緩存中的數據不同了…
這個也是線上很是常見的一個問題,就是多客戶端同時併發寫一個 key,可能原本應該先到的數據後到了,致使數據版本錯了;或者是多客戶端同時獲取一個 key,修改值以後再寫回去,只要順序錯了,數據就錯了。
並且 redis 本身就有自然解決這個問題的 CAS 類的樂觀鎖方案。
####### 面試題剖析 面試題剖析
某個時刻,多個系統實例都去更新某個 key。能夠基於 zookeeper 實現分佈式鎖。每一個系統經過zookeeper 獲取分佈式鎖,確保同一時間,只能有一個系統實例在操做某個 key,別人都不容許讀和寫。
你要寫入緩存的數據,都是從 mysql 裏查出來的,都得寫入 mysql 中,寫入 mysql 中的時候必須保存一個時間戳,從 mysql 查出來的時候,時間戳也查出來。
每次要寫以前,先判斷一下當前這個 value 的時間戳是否比緩存裏的 value 的時間戳要新。若是是的話,那麼能夠寫,不然,就不能用舊的數據覆蓋新的數據。
看看你瞭解不瞭解大家公司的 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 負責緩存集羣的運維。
對於系統 A,假設天天高峯期每秒 5000 個請求,原本緩存在高峯期能夠扛住每秒 4000 個請求,可是緩存機器意外發生了全盤宕機。緩存掛了,此時 1 秒 5000 個請求所有落數據庫,數據庫必然扛不住,它會報一下警,而後就掛了。此時,若是沒有采用什麼特別的方案來處理這個故障,DBA 很着急,重啓數據庫,可是數據庫立馬又被新的流量給打死了。
緩存雪崩的事前事中過後的解決方案以下:
用戶發送一個請求,系統 A 收到請求後,先查本地 ehcache 緩存,若是沒查到再查 Redis。若是 ehcache 和 Redis 都沒有,再查數據庫,將數據庫中的結果,寫入 ehcache 和 Redis 中。
限流組件,能夠設置每秒的請求,有多少能經過組件,剩餘的未經過的請求,怎麼辦?走降級!能夠返回一些默認的值,或者友情提示,或者空值。
好處:
對於系統A,假設一秒 5000 個請求,結果其中 4000 個請求是黑客發出的惡意攻擊。
黑客發出的那 4000 個攻擊,緩存中查不到,每次你去數據庫裏查,也查不到。
舉個栗子。數據庫 id 是從 1 開始的,結果黑客發過來的請求 id 所有都是負數。這樣的話,緩存中不會有,請求每次都「繞過緩存」,直接查詢數據庫。這種惡意攻擊場景的緩存穿透就會直接把數據庫給打死。
解決方式很簡單,每次系統 A 從數據庫中只要沒查到,就寫一個空值到緩存裏去,好比 set -999 UNKNOWN
。而後設置一個過時時間,這樣的話,下次有相同的 key 來訪問的時候,在緩存失效以前,均可以直接從緩存中取數據。這種方式雖然是簡單,可是不優雅,可能會緩存過多的空值,更加優雅的方式就是:使用bitmap
緩存擊穿,就是說某個 key 很是熱點,訪問很是頻繁,處於集中式高併發訪問的狀況,當這個 key 在失效的瞬間,大量的請求就擊穿了緩存,直接請求數據庫,就像是在一道屏障上鑿開了一個洞。
不一樣場景下的解決方式可以下:
緩存擊穿重點在「擊」 就是某個或者是幾個熱點key穿透了緩存層 緩存穿透重點在「透」:大量的請求繞過了緩存層
簡單的講:就是多客戶端同時併發寫一個 key,可能原本應該先到的數據後到了,致使數據版本錯了;或者是多客戶端同時獲取一個 key,修改值以後再寫回去,只要順序錯了,數據就錯了。
並且 Redis 本身就有自然解決這個問題的 CAS 類的樂觀鎖方案,使用版本號進行控制,cas的思想這裏就不詳細說了。
在 Redis cluster 架構下,每一個 Redis 要放開兩個端口號,好比一個是 6379,另一個就是 加1w 的端口號,好比 16379。
16379 端口號是用來進行節點間通訊的,也就是 cluster bus 的東西,cluster bus 的通訊,用來進行故障檢測、配置更新、故障轉移受權。cluster bus 用了另一種二進制的協議, gossip
協議,用於節點間進行高效的數據交換,佔用更少的網絡帶寬和處理時間。
集羣元數據的維護有兩種方式:集中式、Gossip 協議。Redis cluster 節點間採用 gossip 協議進行通訊。
集中式是將集羣元數據(節點信息、故障等等)幾種存儲在某個節點上。集中式元數據集中存儲的一個典型表明,就是大數據領域的 storm
。它是分佈式的大數據實時計算引擎,是集中式的元數據存儲的結構,底層基於 zookeeper(分佈式協調的中間件)對全部元數據進行存儲維護。
Redis 維護集羣元數據採用另外一個方式, gossip
協議,全部節點都持有一份元數據,不一樣的節點若是出現了元數據的變動,就不斷將元數據發送給其它的節點,讓其它節點也進行元數據的變動。
集中式的好處在於,元數據的讀取和更新,時效性很是好,一旦元數據出現了變動,就當即更新到集中式的存儲中,其它節點讀取的時候就能夠感知到;很差在於,全部的元數據的更新壓力所有集中在一個地方,可能會致使元數據的存儲有壓力。
gossip 好處在於,元數據的更新比較分散,不是集中在一個地方,更新請求會陸陸續續打到全部節點上去更新,下降了壓力;很差在於,元數據的更新有延時,可能致使集羣中的一些操做會有一些滯後。
ping
消息,同時其它幾個節點接收到 ping
以後返回 pong
。gossip 協議包含多種消息,包含 ping
, pong
, meet
, fail
等等。
Redis-trib.rb add-node 1
其實內部就是發送了一個 gossip meet 消息給新加入的節點,通知那個節點去加入咱們的集羣。
ping 時要攜帶一些元數據,若是很頻繁,可能會加劇網絡負擔。
每一個節點每秒會執行 10 次 ping,每次會選擇 5 個最久沒有通訊的其它節點。固然若是發現某個節點通訊延時達到了 cluster_node_timeout / 2
,那麼當即發送 ping,避免數據交換延時過長,落後的時間太長了。好比說,兩個節點之間都 10 分鐘沒有交換數據了,那麼整個集羣處於嚴重的元數據不一致的狀況,就會有問題。因此 cluster_node_timeout
能夠調節,若是調得比較大,那麼會下降 ping 的頻率。
每次 ping,會帶上本身節點的信息,還有就是帶上 1/10 其它節點的信息,發送出去,進行交換。至少包含 3
個其它節點的信息,最多包含 總節點數減 2
個其它節點的信息。
來了一個 key,首先計算 hash 值,而後對節點數取模。而後打在不一樣的 master 節點上。一旦某一個 master 節點宕機,全部請求過來,都會基於最新的剩餘 master 節點數去取模,嘗試去取數據。這會致使大部分的請求過來,所有沒法拿到有效的緩存,致使大量的流量涌入數據庫。
一致性 hash 算法將整個 hash 值空間組織成一個虛擬的圓環,整個空間按順時針方向組織,下一步將各個 master 節點(使用服務器的 ip 或主機名)進行 hash。這樣就能肯定每一個節點在其哈希環上的位置。
來了一個 key,首先計算 hash 值,並肯定此數據在環上的位置,今後位置沿環順時針「行走」,遇到的第一個 master 節點就是 key 所在位置。
在一致性哈希算法中,若是一個節點掛了,受影響的數據僅僅是此節點到環空間前一個節點(沿着逆時針方向行走遇到的第一個節點)之間的數據,其它不受影響。增長一個節點也同理。
燃鵝,一致性哈希算法在節點太少時,容易由於節點分佈不均勻而形成緩存熱點的問題。爲了解決這種熱點問題,一致性 hash 算法引入了虛擬節點機制,即對每個節點計算多個 hash,每一個計算結果位置都放置一個虛擬節點。這樣就實現了數據的均勻分佈,負載均衡。
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,不是機器。
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 的功能。
要是系統嚴格要求 「緩存+數據庫」 必須保持一致性的話,能夠使用:讀請求和寫請求串行化,串到一個內存隊列裏去。串行化能夠保證必定不會出現不一致的狀況,可是它也會致使系統的吞吐量大幅度下降,用比正常狀況下多幾倍的機器去支撐線上的一個請求。另外一種方式就是:Cache Aside Pattern
爲何是刪除緩存,而不是更新緩存?
緣由很簡單,不少時候,在複雜點的緩存場景,緩存不僅僅是數據庫中直接取出來的值。
好比可能更新了某個表的一個字段,而後其對應的緩存,是須要查詢另外兩個表的數據並進行運算,才能計算出緩存最新的值的。
另外更新緩存的代價有時候是很高的。是否是說,每次修改數據庫的時候,都必定要將其對應的緩存更新一份?也許有的場景是這樣,可是對於比較複雜的緩存數據計算的場景,就不是這樣了。若是你頻繁修改一個緩存涉及的多個表,緩存也頻繁更新。可是問題在於,這個緩存到底會不會被頻繁訪問到?
舉個栗子,一個緩存涉及的表的字段,在 1 分鐘內就修改了 20 次,或者是 100 次,那麼緩存更新 20 次、100 次;可是這個緩存在 1 分鐘內只被讀取了 1 次,有大量的冷數據。實際上,若是你只是刪除緩存的話,那麼在 1 分鐘內,這個緩存不過就從新計算一次而已,開銷大幅度下降。用到緩存纔去算緩存。
其實刪除緩存,而不是更新緩存,就是一個 lazy 計算的思想,不要每次都從新作複雜的計算,無論它會不會用到,而是讓它到須要被使用的時候再從新計算。像 mybatis,hibernate,都有懶加載思想。查詢一個部門,部門帶了一個員工的 list,沒有必要說每次查詢部門,都把裏面的 1000 個員工的數據也同時查出來啊。80% 的狀況,查這個部門,就只是要訪問這個部門的信息就能夠了。先查部門,同時要訪問裏面的員工,那麼這個時候只有在你要訪問裏面的員工的時候,纔會去數據庫裏面查詢 1000 個員工。
問題:先更新數據庫,再刪除緩存。若是刪除緩存失敗了,那麼會致使數據庫中是新數據,緩存中是舊數據,數據就出現了不一致。
解決思路:先刪除緩存,再更新數據庫。若是數據庫更新失敗了,那麼數據庫中是舊數據,緩存中是空的,那麼數據不會不一致。由於讀的時候緩存沒有,因此去讀了數據庫中的舊數據,而後更新到緩存中。
數據發生了變動,先刪除了緩存,而後要去修改數據庫,此時還沒修改。一個請求過來,去讀緩存,發現緩存空了,去查詢數據庫,查到了修改前的舊數據,放到了緩存中。隨後數據變動的程序完成了數據庫的修改。完了,數據庫和緩存中的數據不同了…
只有在對一個數據在併發的進行讀寫的時候,纔可能會出現這種問題。其實若是說你的併發量很低的話,特別是讀併發很低,天天訪問量就 1 萬次,那麼不多的狀況下,會出現剛纔描述的那種不一致的場景。可是問題是,若是天天的是上億的流量,每秒併發讀是幾萬,每秒只要有數據更新的請求,就可能會出現上述的數據庫+緩存不一致的狀況。
瘋狂創客圈 - Java高併發研習社羣,爲你們開啓大廠之門