響應速度不給力?解鎖正確緩存姿勢
1. 常見概念
在合理應用緩存前,需要了解緩存領域里相關的幾個常用術語:
1)緩存命中:表示數據能夠從緩存中獲取,不需要回源;
2)Cache miss:表示沒有命中緩存,如果緩存內存中還有內存空間的話,會將數據加入到緩存中;
3)存儲成本:當沒有命中緩存時,回源獲取后會將數據放置到存儲中,整個將數據放置到存儲空間所需要的時間以及空間稱之為存儲成本;
4)緩存失效:當源數據發生變更后,意味著緩存中的數據失效;
5)緩存污染:將不經常訪問的數據放置到緩存存儲空間中,以至于高頻訪問的數據無法放置到緩存中;
6)替代策略:當數據放置到緩存空間時,由于空間不足時,就需要從緩存空間中去除已有的數據,選擇去除哪些數據就是由替代策略決定的。常見的替代策略有如下這些:
- Least-Recently-Used(LRU)
- Least-Frequently-Used(LFU)
- SIZE
- First in First Out(FIFO)
由于存儲空間有限,替代策略要解決的核心問題是盡量保留高頻訪問的緩存數據,降低緩存污染以提升緩存命中率和整體的緩存效率,難點在于,需要基于數據歷史訪問情況,以一種合適的對未來訪問情況的預估才能找到更佳的策略。
2. 訪問緩存場景分析
使用緩存通常的操作是,請求先訪問緩存數據,如果緩存中不存在的話,就會回源到數據庫中然后將數據寫入到緩存中;如果存在的話就直接返回數據。從整個過程來看,緩存層就處于數據訪問的前置環節,分擔數據庫在高并發容易出現系統故障的風險,所以在使用過程中需要對緩存層很謹慎的進行分析。在訪問緩存數據時,有常見的三大場景:緩存穿透、緩存擊穿以及緩存雪崩。
2.1 緩存穿透
現象:每次請求直接穿透緩存層,直接回源到數據庫中,給數據庫帶來了巨大訪問壓力,甚至宕機。
原因:訪問數據會先訪問緩存,如果數據不存在緩存中才會查詢數據庫,但是如果查詢數據庫也查詢不出來數據,也是說當前訪問數據永遠不會寫入緩存中。這樣就導致了,訪問一定不存在的數據,就相當于緩存層形同虛設,每次請求都會到db層,造成數據庫負擔過大。
解決方案:
- 方案一:采用bloom filter保存緩存過的key,在訪問請求到來時可以過濾掉不存在的key,防止這些請求到db層;
- 方案二:如果db查詢不到數據,保存空對象到緩存層,設置較短的失效時間;
- 方案三:針對業務場景對請求的參數進行有效性校驗,防止非法請求擊垮db。
2.2 緩存擊穿
現象:當某一key失效時,造成大量請求到db層,擊垮存儲層。
原因:為了保證緩存數據的時效性,通常會設置一個失效時間,如果是熱點key,高并發時會有海量請求直接越過緩存層到數據庫,這樣就會給數據庫造成的負擔增大,設置宕機。
解決方案
方案一:使用互斥鎖,當緩存數據失效時,保證一個請求能夠訪問到數據庫,并更新緩存,其他線程等待并重試;
方案二:緩存數據“永遠不過期”,如果緩存數據不設置失效時間的話,就不會存在熱點key過期造成了大量請求到數據庫。但是,緩存數據就變成“靜態數據”,因此當緩存數據快要過期時,采用異步線程的方式提前進行更新緩存數據。
2.3 緩存雪崩
現象:多個key失效,造成大量請求到db層,導致db層負擔過重甚至宕機。
原因:緩存雪崩是指在我們設置緩存時采用了相同的過期時間,導致緩存在某一時刻同時失效,請求全部轉發到數據庫,最終導致數據庫瞬時壓力過大而崩潰。
解決方案:
方案一:使用互斥鎖的方式,保證只有單個線程進行請求能夠達到db;
方案二:多每個key的失效時間在基礎時間上再加上一個1~5分鐘的隨機值,這樣就能保證大規模key集體失效的概率,并且需要盡量讓多個key的失效時間能夠均勻分布;
2.4 總結
緩存穿透、緩存擊穿以及緩存雪崩這三個術語很容易弄混,也是讀緩存中典型的三個場景問題,做一下簡單的總結是很有必要的。緩存穿透強調是獲取本不存在的緩存數據,請求必然會越過緩存層直接到達到存儲層,很明顯這是利用業務規則的漏洞對系統發起攻擊,解決方案的核心原則是過濾這些非法業務請求,與是否是熱點數據、緩存失效時間等因素沒有關系。
緩存擊穿強調的是熱點key的失效,導致某一時刻大量請求會直接到db層,解決方案的核心原則是規避數據庫的并發操作。緩存雪崩強調的多個key的集體失效,與key是否是熱點數據并不是必然的因素,解決方案的核心原則則讓key之間的失效時間分布更加均勻,避免集體失效的情況。
3 數據更新場景分析
引入緩存后數據會分別存放到緩存以及數據庫兩個地方,因此數據更新時,需要涉及到這兩處地方得更新,并且更新時序的不同會有不同的結果。關于數據更新目前業界已經沉淀了Cache Aside Pattern,Read/Write through等多種方式。
3.1 Cache Aside Pattern
這是很通用的更新策略,主要流程如下:
圖片來源:https://coolshell.cn/articles/17416.html
主要涉及到如下幾點:
- 失效:應用程序先從cache取數據,沒有得到,則從數據庫中取數據,成功后,放到緩存中。
- 命中:應用程序從cache中取數據,取到后返回。
- 更新:先把數據存到數據庫中,成功后,再讓緩存失效。
Cache Aside Pattern在數據更新的時候是采用先更新數據庫,再失效緩存。為什么需要采用這樣的方式來解決數據更新的問題,先假設更新數據庫以及緩存都會事務成功,由于某一種更新導致的不一致性在下一章節進行討論。
1)為什么不是更新緩存,而是失效(刪除)緩存?
❶ 并發寫容易寫覆蓋造成臟數據問題:當數據發生更新的時候,針對緩存數據可以有兩種方式來進行處理分別是更新緩存數據以及失效數據讓下一次讀請求重新從db中獲取數據后重載入緩存中。假設更新緩存數據的話,在并發情況下會存在多線程寫緩存造成臟數據的問題,如下圖:
如上圖所示,假設A、B兩個線程,A先更新數據庫后 B再更新數據庫,然后分別進行更新緩存,但是B先更新緩存成功,A后更新緩存成功,這樣就導致數據庫是最新的數據但是緩存中是舊的臟數據。而如果失效緩存數據的話,可以保證下一次讀請求回源到數據庫將最新的數據載入到緩存中,避免臟數據的問題。因此,針對數據更新緩存采用失效的方式進行處理,也可以參考這篇文章《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》。
❷ 雙寫不同數據源容易造成數據不一致:同時寫數據庫以及緩存數據,任何一個更新失敗都會造成數據不一致,由于“物理失敗”造成的數據不一致在下一個章節進行闡述。另外事務都成功,無論是先更新緩存還是再更新數據庫,還是先更新數據庫再更新緩存,這兩種情況在并發的情況下也很容易出現雙寫不成功,操作時序如下圖,這種方式不推薦。
❸ 先更新緩存再更新數據庫
❹ 先更新數據庫再更新緩存
❺ 違背數據懶加載,避免不必要的計算消耗:如果有些緩存值是需要經過復雜的計算才能得出,所以如果每次更新數據的時候都更新緩存,但是后續在一段時間內并沒有讀取該緩存數據,這樣就白白浪費了大量的計算性能,完全可以后續由讀請求的時候,再去計算即可,這樣更符合數據懶加載,降低計算開銷。
2)可能存在的更新時序?
在確定數據更新后緩存會失效來進行處理的話,針對數據庫以及緩存更新時序就存在如下這幾種:
❶ 先失效緩存再更新數據庫
❷ 假設在并發的情況下,按照這種更新時序會存在什么問題?
如時序圖所示,線程A先失效緩存數據的時候,B線程讀請求發現緩存數據為空的話,就會從數據庫中讀取舊值放入到緩存中,這樣就導致后續的讀請求讀到的都是緩存中的臟數據。針對這樣的情況可以采用延時雙刪的策略來有效避免,偽代碼 如下:
- cache.delKey(key);
- db.update(data);
- Thread.sleep(xxx);
- cache.delKey(key);
主要是在寫請求更新完數據庫后進行休眠一段時間,然后刪除可能由讀請求帶來的臟數據存入到緩存。另外,數據庫如果采用的是主從分離的架構的話,讀出來的數據也有可能是主從未同步完成造成的臟數據。這種通過延時雙刪的方式需要線程休眠,因此很顯然會降低系統吞吐量,并不是一種優雅的解決方式,也可以采用異步刪除的方式。當然可以設置過期時間,到期后緩存失效載入最新的數據,需要系統能夠容忍一段時間的數據不一致。
❸ 先更新數據庫再失效緩存 :這是推薦的更新數據時采用的方式,實際上這也是可能存在數據不一致的情況,時序圖如下:
❹ 假設緩存剛好到期失效時,讀請求從db中讀取數據,寫請求更新完數據后再失效緩存后,讀請求將舊數據存入到緩存中,這種情況也會導致臟數據的問題。實際上這種情況發生的概率很低,要發生這種情況的前提條件是寫數據庫要先于讀數據庫完成,一般而言讀數據庫相比于寫數據庫要耗時更短,這種前提條件成立的概率很低。針對這種”邏輯失敗“造成的數據不一致,可以采用上面所說的異步雙刪的策略以及過期失效的方式來避免。
可以看出在并發的情況下,如果條件苛刻的話,這兩種更新的時序都有可能導致臟數據的情況。只不過在大概率的情況下先更新數據庫再失效緩存能夠保證數據一致,也是業界推薦的處理方式,包括Facebook的論文《Scaling Memcache at Facebook》也使用了這個策略。當數據發生變更上,需要考慮的是最新的數據放置在哪里?很顯然cache aside pattern 選擇的是將最新的數據放到了db上(cache asside pattern:緩存靠邊站),因為數據不一致的情況大概率會存在,需要根據業務場景選擇合適的可信設備存儲最新的數據。
3.2 Write/Read Through
Cache Aside Pattern對db以及緩存的更新邏輯是由調用方自己去控制,很顯然這是一個很復雜的過程。Write/Read Through對調用方而言,緩存是作為整個的數據存儲,而不用關系緩存后面的db,數據庫的更新則是由緩存統一進行管理,對調用方而言只需要和緩存進行交互,整體過程是透明的。
- Read Through:當數據發生更新時,查詢緩存時更新緩存,然后由緩存層同步的更新數據庫即可,對調用方而言只需要和緩存層交互即可;
- Write Through:Write Through 套路和Read Through相仿,不過是在更新數據時發生。當有數據更新的時候,如果沒有命中緩存,直接更新數據庫,然后返回。如果命中了緩存,則更新緩存,然后再由Cache自己同步更新數據庫。如下圖所示(來源于網絡):
3.3 Write Behind Cache Pattern
這種模式是當數據更新的時候直接更新緩存數據,然后建立異步任務去更新數據庫。這種異步方式請求響應會很快,系統的吞吐量會明顯提升。但是,因為是異步更新數據庫,數據一致性的保障就會變弱,如果更新數據庫失敗則會永遠的造成系統臟數據,需要很精細設計系統重試的策略,另外如果異步服務宕機的話,還要考慮更新的數據如何持久化,服務重啟后能夠迅速恢復。在更新數據庫時,由于并發多任務的存在,還需要考慮并發寫是否會造成臟數據的問題,就需要追溯每次更新數據的時序。使用這種模式需要考慮的細節會有很多,設計出一套好的方案是件很不容易的事情。
3.4 更新策略的思考
上面這四種更新策略是非常經典的,也是業界經過大規模業務總結下來的經驗,如果認真分析這四種更新策略的話,也會是受益匪淺,在更新策略的設計我得理解是主要關注如下兩個方面:
最新的數據應該放置在哪里?
緩存的存在是為了系統高性能,利用內存的IO讀取的高速的特性,來提升系統的性能,提高系統吞吐量,另外,緩存的存在會讓一部分讀請求不會到達db層,分解了db的壓力,畢竟db是最容易出現瓶頸的地方。這是為什么利用緩存的兩個重要原因。但是,帶來的問題就是,數據會存在在兩個地方分別是緩存以及數據庫中,當數據更新的時候就需要思考讓”正確的數據應該放在哪個最可信的存儲介質上“,就需要結合業務性質在兩個數據存儲介質上進行選擇。
Cache Aside Pattern選擇先更新數據庫,再失效緩存,這樣可以保證最新最正確的數據一定會落在數據庫中,這樣可以保證核心的業務數據在數據庫中一定是可信的,但是帶來的問題是業務邏輯更復雜,系統處理更新邏輯耗時更長。如果是非核心數據的更新,可以選擇write behind cache pattern的方式,只需要更新緩存即可,能夠快速的響應。缺點是很容易造成數據不一致,數據庫中的數據不一定的就是最可信的數據。所以,不同的更新策略實際上也是將最新的數據優先選擇放在哪里更合適以及系統性能的一種權衡,需要結合業務場景做好trade-off。
4. 數據不一致性
4.1 數據不一致的原因
由于引入緩存,數據就會分散在兩處不同數據源,當數據更新時,實時上很難做到數據一致,除非采用強一致性方案,這里不在進行討論。在找出合適的解決方案前,需要分析下存在數據不一致的主要原因,才能對癥下藥:
1)邏輯失敗造成的數據不一致:在上一章主要分析了更新數據時的四種更新策略,在并發的情況下,無論是先刪除緩存還是更新數據庫,還是更新數據庫再失效緩存,都會數據不一致的情況,主要是因為異步讀寫請求在并發情況下的操作時序導致的數據不一致,稱之為”邏輯失敗“。解決這種因為并發時序導致的問題,核心的解決思想是將異步操作進行串行化。
2)物理失敗造成的數據不一致:在cache aside pattern中先更新數據庫再刪除緩存以及異步雙刪策略等等,如果刪除緩存失敗時都出現數據不一致的情況。但是數據庫更新以及緩存操作是沒辦法放到一個事務中,一般來說,使用緩存是分布式緩存如果緩存服務很耗時,那么將更新數據庫以及失效緩存放到一個事務中,就會造成大量的數據庫連接掛起,嚴重的降低系統性能,甚至會因為數據庫連接數過多,導致系統崩潰。像這種因為緩存操作失敗,導致的數據不一致稱之為”物理失敗“。大多數情況物理失敗的情況會重用重試的方式進行解決。
4.2 數據一致性的解決方案
在絕大部分業務場景中,追求的是最終一致性,針對物理失敗造成的數據不一致常用的方案有:消費消息異步刪除緩存以及訂閱Binlog的方式,針對邏輯失敗造成的數據不一致常用的方案有:隊列異步操作同步化。
4.2.1 消費消息異步刪除緩存
主要流程如下圖所示:
4.2.2 訂閱Binlog
主要流程如下圖所示:
4.2.3 利用隊列串行化
在分析cache aside pattern發現在并發的情況下也會存在數據不一致的場景,只不過發生的概率很低,另外如果先刪除緩存再更新數據庫在并發讀寫的情況下也會存在數據不一致的情況。類似這種由于并發時序導致的數據不一致的情況,都是因為寫請求還沒有結束讀請求讀取的是舊數據,如果讀請求在寫請求之后處理,即請求的處理能夠串行化的話,就能保證讀請求讀到的是寫請求更新的最新的數據。
將請求進行串行化,最常用的方式是采用隊列的方式,一個隊列只能對應一個工作線程,更新數據的寫請求放置隊列中,等待異步處理;讀請求如果能從緩存中獲取數據,則返回,如果緩存中沒有數據,就將讀請求放置到隊列中,等待寫請求數據更新完成。這種方案需要考慮的問題有:
1)讀請求長時間阻塞:如果隊列中擠壓了多個寫請求,則讀請求會存在長時間阻塞的情況,需要設置超時處理策略,一旦超過超時時間,則直接讀取數據庫返回,避免長時間不響應;另外,在業務中需要進行壓測,考慮隊列中在峰值情況下會積攢多少寫請求,如果過多,需要考慮隊列優化的方式和相應的解決方案;
2)多個隊列分散壓力:可以根據數據項通過hash等路由方式,創建多個隊列并行執行來提升系統吞吐量;
3)操作復雜需要考慮全面:由于采用隊列來進行串行化,那么要考慮隊列的可用性,隊列阻塞以及服務掛掉后的容災恢復策略是否健壯等等,相對而言整體的方案需要考慮的點會有很多;
這種方式可以做到數據強一致性,由于串行化系統的吞吐量會下降很多并且操作復雜,畢竟任何方案都會有利弊權衡的過程,需要根據業務場景選擇合適的技術方案。針對數據強一致性很有很多方案,但基本上操作設計都很復雜,在大多數業務場景滿足數據最終一致性即可。
當然除了以上這三種通用的方法外,為緩存設置過期時間以及定時全量同步,也是接近最終一致性的最簡單以及有效的方式。
5. 常見的幾個場景問題
在分析數據更新的策略后發現正確使用緩存是一件很不容易的事情,在實際使用緩存時,還會有很多有意思的場景(”坑“),在這里進行一下總結:
1)過期還是不過期緩存數據:針對緩存數據是否需要設置過期時間也需要結合場景來進行分析,一些長尾商品,大多數數據在業務中都是讀場景更多,并且緩存空間很大的話,就可以考慮不過期數據。那是否就意味著這就是一份靜態數據了?當緩存空間已滿時,數據會根據淘汰策略移除緩存,另外數據更新時也可以通過Binlog等其他方式進行異步失效緩存。
如果系統通過消息異步更新操作成本過高或者依賴于外部系統無法進行訂閱binlog異步更新的話,就需要來采用過期緩存數據來保障數據最終一致性。
2)維度化緩存與增量更新:如果一個實體包含多個屬性,在實體發生變更時,如果將所有的屬性全部更新一遍,這個成本就很高,況且只是其中的幾個屬性發生變化。因此,將多個屬性進行各個維度化進行拆解,按照多維度進行緩存,更新時只需要增強更新對應維度即可;
3)大value:大value的問題要時刻警惕,可以考慮將value進行壓縮,以及緩存時進行拆解,然后在業務服務中進行數據聚合來避免大value的問題;
4)熱點緩存問題:針對熱點數據如果每次都從遠程緩存去獲取,會給緩存系統帶來過多的負載,會導致獲取緩存數據響應過慢,可以使用緩存集群,掛載更多的從緩存,讀取數據從從緩存中獲取。針對熱點數據可以使用應用本地緩存來減少對遠程緩存的請求負載;
5)數據預熱:可以預先將數據加載到緩存中,方式緩存數據為空,大量的請求回源到db。如果容量很高可以考慮全量預熱,如果容量優先,就只能選擇高頻熱點數據進行數據預熱,還需要關注是否有批量操作以及慢sql帶來的性能問題,在整個數據預熱過程中需要有可靠的監控機制來保障;
6)非預期熱點數據:針對業務預估不足的熱點數據,需要有熱點發現系統來統計熱點key,實時監控非預期的熱點數據,可以將這些key推到本地緩存中,防止預估不足的熱點key拖垮遠程緩存服務。
7)緩存實例故障快速恢復:當某一個緩存實例故障后,緩存一般是采用分片實例存儲,假設緩存key路由策略采用的取模機制的話,會導致當前實例的流量迅速到達db層,這種情況可以采用主從機制,當一個實例故障后其他實例可以使用,但是這種方式的問題在于水平擴展不夠,如果分片實例上增加一個節點的話,會導致緩存命中率迅速下降。
如果key路由策略采用的一致性哈希的話,某一個實例節點故障,只會導致哈希環上的部分緩存不命中不會導致大量請求到達db,但是針對熱點數據的話,可能會導致改節點負載過高成為系統瓶頸。針對實例故障恢復的方式有:1. 主從機制,對數據進行備份,盡可能保障有可用數據;2. 服務降低,新增緩存實例然后異步線程預熱數據;3. 可以先采用一致性哈希路由策略,當出現熱點數據時到達某個閾值時降級為取模的策略。
6. 幾個影響因素
影響緩存整體的性能會有很多大大小小的影響因素,比如語言本身的特性的影響,例如Java需要考慮GC的影響。還需要盡可能的提升緩存命中率等等多個方面,總結下來,核心的幾個影響因素如下:
1)提升緩存命中率:影響緩存命中率的幾個因素:
❶ 業務時效性要求:緩存適合"讀多寫少"的業務場景,并且業務性質決定了時效性要求,不同的時效性要求決定了緩存的更新策略以及過期時間,對時效性也低的業務越適合使用緩存,并且緩存命中率越高;
❷ 緩存粒度設計:通常而言,緩存對象粒度越小就越適合使用緩存,不會導致頻繁更新導致緩存命中率下降;
❸ 緩存淘汰策略:如果緩存空間有限,不同的緩存淘汰策略也會影響緩存命中率,如果淘汰的緩存數據后續被大量使用,無疑就會降低緩存命中率;
❹ 緩存部署方式:在使用分布式緩存時,要做好容量規劃以及容災策略,方式緩存實例故障后造成大規模緩存失效;
❺ Key路由策略:不同路由策略會在節點實例故障后帶來不同的影響,如果采用取模的方式水平擴展時則會降低緩存命中率。通過這些分析,提高緩存命中率沒有放之四海而皆準的統一規則,需要從這些角度去思考,盡可能的在高頻訪問且時效性不是很高的業務數據上使用緩存。
2)序列化方式:使用遠程緩存服務免不了需要經過序列化后在網絡中進行數據傳輸,那么選擇不同的序列化方式對緩存性能會有影響。選擇序列化方式時需要考慮序列化耗時、序列化后在網絡傳輸中包大小以及序列化的計算開銷。
3)GC影響:采用多級緩存以及大value時會采用應用本地緩存,對于java應用,就需要考慮大對象帶來的GC影響。
4)緩存協議:了解不同的緩存協議的優缺點比如Redis以及Memcached協議,根據業務場景進行選擇。
5)緩存連接池:為提升訪問性能,需要合理的設置緩存連接池。
6)完善的監控平臺:需要考慮是否有一套緩存的監控平臺,能夠追蹤緩存使用情況、緩存服務整體的性能以及一些非預期熱點數據的發現策略等等,這樣才能綜合整體的保障緩存服務的可用以及性能。
7. 多級緩存設計案例
從用戶發出請求到到最底層的數據庫實際上會經歷很多節點,因此在整個鏈路上都可以設置緩存,并且按照緩存最近原則將緩存放置在里用戶最近的地方提升系統響應的效果最為明顯,相應的提升系統吞吐量的效果就越為顯著,通過能夠大大降低對后端的壓力。在整個鏈路流程里可以添加緩存的地方有:發起請求-->瀏覽器/客戶端緩存-->邊緣緩存/CDN-->反向代理(Nginx)緩存-->遠程緩存-->進程內緩存-->數據庫緩存。服務端多級緩存設計通用的技術方案如下:
主要流程為:
1)請求先達到Nginx,先讀取Nginx本地緩存,如果命中緩存則返回緩存數據。這里的負載均衡路由策略,采用輪詢的方式相對而言訪問壓力分布的更加均衡,一致性哈希方式能夠提升緩存命中率,但是同時也會存在單點壓力過大的問題,可以考慮使用一致性哈希策略時流量達到一定閾值的時候切換成輪詢的方式;
2)如果沒有命中Nginx緩存,則讀取分布式緩存,為了高可用以及提升系統吞吐量,一般遠程分布式緩存會采用主從結構,這里讀取的就是從緩存服務集群數據,如果命中緩存則返回數據;
3)如果從緩存沒有命中緩存,則讀取應用本地緩存(堆內/堆外緩存),這里的路由策略同樣可以采用輪詢或者一致性哈希。如果命中,則返回數據,并回寫到Nginx緩存中;為避免由于從緩存服務出現問題,造成過大的流量沖垮數據庫,這里可以嘗試讀取主緩存服務;
4)如果所有緩存沒有命中,則查詢數據庫并返回數據,并異步回寫到主緩存以及應用本地緩存中。主緩存通過主從同步機制同步到從緩存服務集群中。這里會寫到主緩存的時候需要考慮多個應用實例在異步寫,需要考慮數據是否會亂序的問題。
另外,對于一些非預期熱點數據比如微博中”某某明星結婚“等等熱門話題帶來的訪問流量瞬間沖擊到后端,針對以上多級緩存設計,可以通過引入熱點發現系統來發現非預期的熱點數據,利用flume訂閱Nginx日志,然后通過消息進行消費,最后通過storm等實時計算框架進行熱點數據的統計,當監控發現到熱點數據,將其推送到各個緩存節點上,整體的緩存設計如下:
8. 總結
為了追求高性能,每個開發者最先使用的就是緩存,也在潛意識里將緩存作為了系統性能瓶頸的一劑良藥,經過系統化的總結和分析緩存后,就可以發現緩存如果使用不當真的就會事與愿違,成為毒藥,并不會系統迭代出那個局部最優解。如果貿然的使用緩存,需要考慮的地方真的很多稍有不注意,反而會讓系統投入更多的維護成本,陡增更高的復雜度。那是不是就不使用緩存呢?也不是,緩存在高并發的情況下通過IO高速的緩存獲取數據能使得每個請求能夠快速響應,并且能夠大大提升系統吞吐量以及支撐更高的并發用戶數,在現有的高并發大流量的互聯網應用中應用緩存的例子太多了,也足以證明緩存在優化系統整體性能是一種行之有效的方案。
作為開發者不是每個人都有機會和機遇去挑戰高并發的互聯網架構以及高量級的訪問流量和應用規模的,那是不是就意味著這些通用的技術方案就不用深刻分析呢?很顯然不是,單從緩存使用中就會發現在高并發下讀寫帶來的數據不一致性分析下來就會有很多并發場景,單線程下都是正常的,但在并發下就會出現很多意想不到的case,而這些分析的思路是最核心的,也是開發者逐漸形成自己的方法論的有效訓練途徑。在系統化學習每一種技術組件時,業界的通用解決方案都是經過歷史經驗慢慢沉淀下來的智慧,如同品酒,是需要靜下心來好好去品的。
技術最終是服務于業務價值,而業務規模擴張會反哺技術的創新,要設計出一套適應于業務的合理的技術方案,需要很深的內功,需要既懂技術又要對業務理解十分深刻才行,懂業務而不懂技術,很難知道每種技術方案的局限性,也就是經常所說的PPT架構師,PPT很炫酷,一頓操作猛如虎但是并不是最適合業務的那個解,反而就像是跳梁小丑一樣自嗨或者帶著功利心去急于變現,只有業務與技術結合能夠得到最大價值的那個解就是最合適的方案,需要在優與劣的trade-off上做出權衡。如果很懂技術,但是不懂業務,同樣的就是廢銅爛鐵沒辦法發揮出功力。在不同的職業生涯階段,每個人的精力有限,投入技術以及業務的精力分配也是不同的,專注的點會有所不同,就像業務與技術一樣,在人生的賽道中在不同階段也需要迭代出那個最合適的局部最優解,至于什么最合適,答案在每個人心中!