數據量大讀寫緩慢如何優化(5)【讀緩存】

在前面的四篇文章中,咱們從數據持久化層來聊了一些架構設計方案,來處理數據量大讀寫緩慢的問題。可是架構設計並非只有這一方面的設計思路,本篇開始咱們來從緩存層面來一塊兒看看如何設計。redis

1、業務場景四

在一個電商系統中,存放了50000多條商品數據,每次用戶瀏覽商品詳情頁時,須要先從數據庫中讀取數據,再進行數據拼裝和計算,耗費的時間有時長達1秒。數據庫

這就致使每次點擊商品詳情頁時,頁面打開速度慢,此時該如何減小數據庫讀操做的壓力呢?緩存

在項目時間緊張,趕進度的時候,沒更多的精力關注此類問題。可是當系統流量起來以後,這種問題就不能不考慮了。服務器

此時採起的方案也比較通用,把全部的商品數據緩存起來就行。數據結構

關於緩存的問題,最簡單的實現方法是使用本地緩存。在Google Guava中有一個cache內存緩存模塊,它把全部商品的ID與商品詳細信息一對一緩存至JVM內存中,用戶獲取商品詳情數據時,系統會自動根據商品ID直接從緩存中讀取數據,大大提高了用戶頁面訪問速度。架構

不過,經過簡單換算後,咱們發現這個方法明顯不合理,先來舉個例子:併發

1條商品數據中,每每包含品牌、分類、參數、規格、服務、描述等字段,光存儲這些商品數據就得佔用500K左右內存,再將這些數據緩存到本地的話,差很少還須要佔用500K*50000=25G內存。此時,假設商品服務有30個服務器節點,光緩存商品數據就須要額外準備750G內存空間,這種方法顯然不可取。負載均衡

爲此,咱們想到了另一個解å決辦法——分佈式存儲,先將全部緩存數據集中存儲在同一個地方,而並不是保存到各個服務器節點中,而後全部的服務器節點從這個地方讀取數據。分佈式

那麼這個統一存儲緩存的地方須要使用什麼技術呢?這就涉及接下來咱們要聊的緩存中間件的技術選型問題。ide

2、緩存中間件技術選型

咱們先將市面上比較流行的緩存中間件(Memcached、MongoDB、Redis)進行簡單對比,這樣你們就不用深刻進行選型調研了。

Memcached MongoDB Redis
數據結構 簡單key-value 很是全面,文檔型數據庫 String、List、Set、Hash、Bitmap等
持久化 不支持 支持 支持
集羣 客戶端本身控制 支持 支持
性能 中等

據我瞭解,以上三種技術中,目前市面上通用的緩存中間件技術是Redis,使用MongoDB的公司最少,由於他只是一個數據庫,因爲他的讀寫速度與其餘數據庫相比較快,因此人們才把它當作相似緩存的存儲。

在這裏,咱們總結一下Redis之因此比memcached流行的三種緣由:

一、數據結構

舉個例子,在使用 Memcached 保存 List 緩存對象的過程當中,若是咱們往 List 增長一條數據,首先須要讀取整個 List ,再反序列化塞入數據,接着再序列化存儲回 Memcached。而對於 Redis 而言,它僅僅是一個 Redis 請求,會直接幫咱們塞入數據並存儲,簡單快捷。

二、持久化

對於 Memcached 來講,一旦系統宕機數據就會丟失。經過 Memcached 的官方文檔得知,1.5.18 之後 Memcached 支持 restartable cache,其實現原理是重啓時 CLI 先發信號給守護進程,而後守護進程將內存持久化至一個文件中,系統重啓時再從那個文件恢復數據。不過,這個設計僅在正常重啓狀況下使用,意外狀況仍是不處理。

三、集羣(這點尤其重要)

Memcached 的集羣設計很是簡單,客戶端根據 Hash 值直接判斷存取的 Memcached 節點。而 Redis 的集羣因在高可用、主從、冗餘、failover 等方面都有所考慮,因此集羣設計相對複雜些,屬於較常規的分佈式高可用架構。

所以,通過一番「慎重」的思考,咱們最終決定使用Redis做爲緩存的中間件。

技術選型完,咱們開始考慮緩存的一些具體問題,先從緩存什麼時候存儲數據入手。

3、緩存什麼時候存儲數據

使用緩存的邏輯是這樣的:

一、先嚐試從緩存中讀取數據;

二、緩存中沒有數據或者數據過時,再從數據庫中讀取數據保存到緩存中;

三、最終把緩存數據返回給調用方。

這種邏輯惟一麻煩的地方:當用戶發來大量併發請求,且全部請求同時擠在上面第2步,此時若是這些請求所有從數據庫讀取數據,會直接擠爆數據庫。

上面所說的擠爆能夠分爲三種狀況,咱們單獨展開說一下:

一、單一數據過時或者不存在,這種狀況稱爲緩存擊穿。

此時解決方案:第一個線程若是發現key不存在,先給key加鎖,再從數據庫讀取數據保存到緩存中,最後釋放鎖。若是其餘線程正在讀取同一個key值,它必須等到鎖釋放後才行。(關於鎖的問題在第一篇文章中已經聊過了,就再也不說了)

二、數據大面積過時或者Redis宕機,這種狀況稱之爲緩存雪崩。

此時,咱們設置緩存緩存過時時間隨機分佈或永不過時便可。

三、一個惡意請求獲取的key不在數據庫中,這種狀況稱之爲緩存穿透。

這種狀況若是不作處理,惡意請求每次都會查詢數據庫,無疑給數據庫增長了壓力。

這裏分享2種解決辦法:①在業務邏輯上直接校驗,在數據庫不被訪問的前提下過濾掉不存在的key;②將惡意請求的key存放一個空值在緩存中,防止惡意請求騷擾數據庫。

最後,說明一下關於緩存預熱:在深夜無人或訪問量小的時候,咱們能夠考慮將預熱的熱數據保存到緩存中,這樣流量大的時候,用戶查詢無須再從數據庫讀取數據,大大減小了數據讀壓力。

關於緩存什麼時候存數據的問題咱們就討論完了,接下來開始討論更新緩存的問題,這部份內容涉及雙寫(緩存+數據庫)。

4、如何更新緩存

更新緩存的步驟特別簡單,總共就兩步:更新數據庫和更新緩存。但就這麼簡單的兩步,咱們須要考慮好幾個問題。

一、先更新數據庫仍是先更新緩存?更新緩存時先刪除仍是直接更新?

二、假設第一步成功了,第二步失敗了怎麼辦?

三、假設2個線程同時更新一個數據,A線程先完成第一步,B線程先完成第二步,此時該怎麼辦?

其中,第一個問題就存在4種組合問題,咱們先針對第 1 種組合問題給出對應的解決方案。(以上幾個問題由於緊密關聯,無法單獨考慮,下面咱們就一塊兒說明。)

組合一:先更新緩存,再更新數據庫

對於這個組合,會遇到這種狀況:假設第 2 步數據庫更新失敗了,要求回滾緩存的更新,這時該怎麼辦呢?咱們知道 Redis 不支持事務回滾,除非咱們採用手工回滾的方式,先保存原有數據,而後再將緩存更新回原來的數據,這種解決方案就有點尷尬了。

這裏簡單舉個例子,好比:

一、原來緩存中的值是 a,兩個線程同時更新庫存;

二、線程 A 將緩存中的值更新成 b,且保存了原來的值 a,而後更新數據庫;

三、線程 B 將緩存中的值更新成 c,且保存了原來的值 b,而後更新數據庫;

四、線程 A 更新數據庫時失敗了,它必須回滾了,那如今緩存中的值更新回什麼呢?

要不這樣吧,咱們在A線程更新緩存與數據庫整個過程當中,先把緩存及數據庫都鎖上,確保別人不能更新,這樣的方法可不可行呢?固然是可行的,可是別人能不能讀呢?

假設A更新數據庫失敗回滾緩存時,線程C也來參一腿,它須要先讀取緩存中的值,這時又返回什麼值呢?

看到這個場景,你是否是有點印象了?不錯,這就是典型的事務隔離級別場景。咱們只是使用一下緩存而已,你讓我本身實現事務隔離級別,這個要求會不會有點高?咱們仍是考慮別的吧。

組合二:先刪除緩存,再更新數據庫

使用這種方案,就算咱們更新數據庫失敗了也不須要回滾緩存。這種作法雖然巧妙規避了失敗回滾的問題,卻引來了兩個更大的問題。

一、假設A線程先刪除緩存,再更新數據庫。在A線程完成更新數據庫庫以前,後執行的B線程反而超前完成了操做,讀取key發現沒數據後,將數據庫中的舊值放到了緩存中。A線程在B線程都完成後再更新數據庫,這樣就會出現緩存(舊值)與數據庫的值(新值)不一致的問題。

二、爲了解決一致性的問題,咱們可讓A線程給key加鎖,由於寫操做特別耗時,這種處理方法會致使大量的讀請求卡在鎖中。

以上描述的典型的高可用和一致性難以兩全的問題,要再加上分區容錯就是CAP了,這裏咱們就不展開討論了。

組合三:先更新數據庫,再更新緩存

對於組合三,咱們一樣須要考慮兩個問題。

一、假設第一步成功,第二步失敗了怎麼辦?由於緩存不是主流程,數據庫纔是,因此咱們不會由於更新緩存失敗而回滾第一步對數據庫的更新。此時,咱們通常採用的作法是作重試機制,但重試機制若是存在延時仍是會出現數據庫與緩存不一致的狀況,很是很差處理啊。

二、假設2個線程同時更新同一個數據,A線程先完成了第一步,B線程先完成了第二步怎麼辦?

假設2個線程同時更新同一個數據,A線程先完成了第一步,B線程先完成了第二步怎麼辦?咱們接着來推演整個過程:A線程把值更新a,B線程把值更新成b,此時數據庫中的最新值是b,由於A線程先完成了第一步,後完成第二步,因此緩存中的最新值是a,數據庫與緩存的值仍是不一致,仍是很差處理啊。

所以,咱們不建議採用以上這個方案。

組合四:先更新數據庫,再刪除緩存

針對組合四,咱們看看到底會存在哪些問題。

一、假設第一步成功了,第二步失敗了怎麼辦?這種狀況的出現機率與上個組合相比明顯少很多,由於刪除比更新容易多了。此時雖然它不完美,但出現一致性的問題機率少。

二、假設2個線程同時更新同一個數據,A線程先完成第一步,B線程先完成第二步怎麼辦?

這裏咱們接着推演整個過程:A線程把值更新成a,B線程把值更新成b,此時數據庫中的最新值是b,由於A線程先完成了第一步,至於第二步誰先完成已經無所謂了,反正是直接刪除緩存數據。

看到這裏,咱們發現組合四完美解決了以上難題,因此建議更新緩存時,先更新數據庫再刪除緩存。

不過,這個解決方案也會引起另外3個問題。

  • 刪除緩存數據後變相出現緩存擊穿,此時該怎麼辦?此問題在前面咱們已經給出了方案。
  • 刪除緩存失敗如何重試?能夠參考以前的查詢分離使用重試的方案解決。
  • 刪除緩存失敗,重試成功前出現髒數據。這個須要與業務商量,畢竟這種狀況仍是少見,咱們能夠根據實際業務狀況判斷是否須要解決這個瑕疵。畢竟任何一個方案都不是完美的,但若是剩下1%的問題須要咱們花好幾倍的代價去解決,從技術上來說得不償失,這就要求架構師協同PM去說服業務方。

前面咱們花了大篇幅討論更新緩存的邏輯,接下來咱們來討論緩存的高可用設計。

5、緩存的高可用設計

關於緩存高可用設計問題,在設計高可用方案時,咱們須要考慮5個要點:

一、負載均衡:是否能夠經過加節點的方式水平分擔讀請求壓力。

二、分片:是否能夠經過劃分到不一樣的節點的方式水平分擔寫壓力。

三、數據冗餘:一個節點的數據若是掛掉了,其餘節點是否能夠直接備份掛掉節點的職責。

四、Fail-over:任何節點掛掉後,集羣的職責是否能夠從新分配,以此保障集羣正常工做。

五、一致性保證:在數據冗餘、failover、分片機制的數據轉移過程當中,若是某個地方出幺蛾子,可否保證全部的節點數據或節點與數據庫之間數據的一致性。(依靠redis自己是不行的)

若是對緩存高可用有需求咱們能夠用使用Redis的cluster模式,關於前面提到的點它都有涉及。至於cluster怎麼配置,能夠參考Redis官方文檔或網上教程,這裏就不展開了。

一、緩存的監控

緩存上線後,咱們還須要定時查看緩存的使用狀況,再判斷業務邏輯是否須要優化,也是就是所謂的緩存的監控。

在查看緩存使用狀況時,通常咱們會監控緩存命中率、內存使用率、慢日誌、延遲、客戶端鏈接數等數據。固然,隨着問題的深刻咱們還須要增長其餘指標,這裏就不詳細說了。

至於最終使用哪一種監控工具,須要根據實際狀況而定。這裏推薦幾款開源監控工具,好比RedisLive、Redis-monitor等。

6、此方案的價值和不足

以上方案能夠順利解決讀數據請求壓垮數據庫的問題,目前互聯網架構也基本是採起這裏方案。可是這個方案還存在一個不足,沒法解決寫數據請求量大的問題,也就是說寫請求多時,數據庫仍是會扛不住。針對這個問題,後面的文章中咱們接着討論。

相關文章
相關標籤/搜索