者:kevinylzhao,騰訊音樂前端開發工程師
瀏覽器緩存策略對于前端開發同學來說不陌生,大家都有一定的了解,但如果沒有系統的歸納總結,可能三言兩語很難說明白,甚至說錯,尤其在面試過程中感觸頗深,很多候選人對這類基礎知識竟然都是一知半解,說出幾個概念就沒了,所以重新歸納總結下,溫故而知新。
瀏覽器緩存一般分為兩類:強緩存(也稱本地緩存)和協商緩存(也稱弱緩存)。
瀏覽器發送請求前,會先去緩存里查看是否命中強緩存,如果命中,則直接從緩存中讀取資源,不會發送請求到服務器。否則,進入下一步。
當強緩存沒有命中時,瀏覽器一定會向服務器發起請求。服務器會根據 Request Header 中的一些字段來判斷是否命中協商緩存。如果命中,服務器會返回 304 響應,但是不會攜帶任何響應實體,只是告訴瀏覽器可以直接從瀏覽器緩存中獲取這個資源。如果本地緩存和協商緩存都沒有命中,則從直接從服務器加載資源。
按照本地緩存階段和協商緩存階段分類:
上述代碼的作用是告訴瀏覽器當前頁面不被緩存,事實上這種禁用緩存的形式用處很有限:
a. 僅有 IE 才能識別這段 meta 標簽含義,其它主流瀏覽器僅識別“Cache-Control: no-store”的 meta 標簽。
b. 在 IE 中識別到該 meta 標簽含義,并不一定會在請求字段加上 Pragma,但的確會讓當前頁面每次都發新請求(僅限頁面,頁面上的資源則不受影響)。
在 HTTP 請求和響應的消息報頭中,常見的與緩存有關的消息報頭有:
上圖中只是常用的消息報頭,下面來看下不同字段之間的關系和區別:
a. Last-Modified 標注的最后修改只能精確到秒級,如果某些文件在 1 秒鐘以內,被修改多次的話,它將不能準確標注文件的新鮮度;
b. 某些文件也許會周期性的更改,但是它的內容并不改變(僅僅改變的修改時間),但 Last-Modified 卻改變了,導致文件沒法使用緩存;
c. 有可能存在服務器沒有準確獲取文件修改時間,或者與代理服務器時間不一致等情形。
瀏覽器可以在內存、硬盤中開辟一個空間用于保存請求資源副本。我們經常調試時在 DevTools Network 里看到 Memory Cache(內存緩存)和 Disk Cache(硬盤緩存),指的就是緩存所在的位置。請求一個資源時,會按照優先級(Service Worker -> Memory Cache -> Disk Cache -> Push Cache)依次查找緩存,如果命中則使用緩存,否則發起請求。這里先介紹 Memory Cache 和 Disk Cache。
200 from memory cache
表示不訪問服務器,直接從內存中讀取緩存。因為緩存的資源保存在內存中,所以讀取速度較快,但是關閉進程后,緩存資源也會隨之銷毀,一般來說,系統不會給內存分配較大的容量,因此內存緩存一般用于存儲較小文件。同時內存緩存在有時效性要求的場景下也很有用(比如瀏覽器的隱私模式)。
200 from disk cache
表示不訪問服務器,直接從硬盤中讀取緩存。與內存相比,硬盤的讀取速度相對較慢,但硬盤緩存持續的時間更長,關閉進程之后,緩存的資源仍然存在。由于硬盤的容量較大,因此一般用于存儲大文件。
下圖可清晰看出差別:
200 from prefetch cache
在 preload 或 prefetch 的資源加載時,兩者也是均存儲在 http cache,當資源加載完成后,如果資源是可以被緩存的,那么其被存儲在 http cache 中等待后續使用;如果資源不可被緩存,那么其在被使用前均存儲在 memory cache。
CDN Cache
以騰訊 CDN 為例:X-Cache-Lookup:Hit From MemCache 表示命中 CDN 節點的內存;X-Cache-Lookup:Hit From Disktank 表示命中 CDN 節點的磁盤;X-Cache-Lookup:Hit From Upstream 表示沒有命中 CDN。
從上圖能感受到整個流程,比如常見兩種刷新場景:
IndexedDB 就是瀏覽器提供的本地數據庫,能夠在客戶端存儲可觀數量的結構化數據,并且在這些數據上使用索引進行高性能檢索的 API。
異步 API 方法調用完后會立即返回,而不會阻塞調用線程。要異步訪問數據庫,要調用 window 對象 indexedDB 屬性的 open() 方法。該方法返回一個 IDBRequest 對象 (IDBOpenDBRequest);異步操作通過在 IDBRequest 對象上觸發事件來和調用程序進行通信。
常用異步 API 如下:
在 16 年曾基于 IndexDB 做過一整套緩存策略,有不錯的優化效果:
SW 從 2014 年提出的草案到現在已經發展很成熟了,基于 SW 做離線緩存,讓用戶能夠進行離線體驗,消息推送體驗,離線緩存能力涉及到 Cache 和 CacheStorage 的概念,篇幅有限,不展開了。
localStorage 屬性允許你訪問一個 Document 源(origin)的對象 Storage 用于存儲當前源的數據,除非用戶人為清除(調用 localStorage api 或者清除瀏覽器數據), 否則存儲在 localStorage 的數據將被長期保留。
sessionStorage 屬性允許你訪問一個 session Storage 對象,用于存儲當前會話的數據,存儲在 sessionStorage 里面的數據在頁面會話結束時會被清除。頁面會話在瀏覽器打開期間一直保持,并且重新加載或恢復頁面仍會保持原來的頁面會話。
通過了解瀏覽器各種緩存機制和存儲能力特點,結合業務制定合適的緩存策略,善用緩存是基本功,可以用于時常審查負責的業務,可能就會發現個別業務并沒有運用到位,共勉。
我們將先從Redis、Nginx+Lua等技術點出發,了解緩存應用的場景。通過使用緩存相關技術,解決高并發的業務場景案例,來深入理解一套成熟的企業級緩存架構是如何設計的。
Redis是一個開源的使用ANSI C語言編寫、遵守BSD協議、支持網絡、可基于內存亦可持久化的日志型、Key-Value數據庫,并提供多種語言的API。
它通常被稱為數據結構服務器,因為值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等類型。
Redis 與其他 key - value 緩存產品有以下三個特點:
優勢
string 是 redis 最基本的類型,你可以理解成與 Memcached 一模一樣的類型,一個 key 對應一個 value。
string 類型是二進制安全的。意思是 redis 的 string 可以包含任何數據。比如jpg圖片或者序列化的對象。
string 類型是 Redis 最基本的數據類型,string 類型的值最大能存儲 512MB。
redis 127.0.0.1:6379> SET runoob "laowang"
OK
redis 127.0.0.1:6379> GET runoob
"laowang"
Redis hash 是一個鍵值(key=>value)對集合。
Redis hash 是一個 string 類型的 field 和 value 的映射表,hash 特別適合用于存儲對象。
每個 hash 可以存儲 2^32 -1 鍵值對(40多億)。
redis 127.0.0.1:6379> HMSET runoob field1 "Hello" field2 "World"
"OK"
redis 127.0.0.1:6379> HGET runoob field1
"Hello"
redis 127.0.0.1:6379> HGET runoob field2
"World"
Redis 列表是簡單的字符串列表,按照插入順序排序。你可以添加一個元素到列表的頭部(左邊)或者尾部(右邊)。
列表最多可存儲 2^32 - 1 元素 (4294967295, 每個列表可存儲40多億)。
redis 127.0.0.1:6379> lpush runoob redis
(integer) 1
redis 127.0.0.1:6379> lpush runoob mongodb
(integer) 2
redis 127.0.0.1:6379> lpush runoob rabitmq
(integer) 3
redis 127.0.0.1:6379> lrange runoob 0 10
1) "rabitmq"
2) "mongodb"
3) "redis"
Redis 的 Set 是 string 類型的無序集合。
集合是通過哈希表實現的,所以添加,刪除,查找的復雜度都是 O(1)。
sadd 命令 :添加一個 string 元素到 key 對應的 set 集合中,成功返回 1,如果元素已經在集合中返回 0。
集合中最大的成員數為 2^32 - 1(4294967295, 每個集合可存儲40多億個成員)。
redis 127.0.0.1:6379> DEL runoob
redis 127.0.0.1:6379> sadd runoob redis
(integer) 1
redis 127.0.0.1:6379> sadd runoob mongodb
(integer) 1
redis 127.0.0.1:6379> sadd runoob rabitmq
(integer) 1
redis 127.0.0.1:6379> sadd runoob rabitmq
(integer) 0
redis 127.0.0.1:6379> smembers runoob
1) "redis"
2) "rabitmq"
3) "mongodb"
Redis zset 和 set 一樣也是string類型元素的集合,且不允許重復的成員。
不同的是每個元素都會關聯一個double類型的分數。redis正是通過分數來為集合中的成員進行從小到大的排序。
zset的成員是唯一的,但分數(score)卻可以重復。
zadd 命令 :添加元素到集合,元素在集合中存在則更新對應score
redis 127.0.0.1:6379> zadd runoob 0 redis
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 mongodb
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 rabitmq
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 rabitmq
(integer) 0
redis 127.0.0.1:6379> > ZRANGEBYSCORE runoob 0 1000
1) "mongodb"
2) "rabitmq"
3) "redis"
對這個問題的思考,將有助于我們從整體架構上去學習Redis。
假設現在我們已經設計好了一個KV數據庫,首先如果我們要使用,是不是得有入口,我們是通過動態鏈接庫還是通過網絡socket對外提供訪問入口,這就涉及到了訪問模塊。Redis就是通過
通過訪問模塊訪問KV數據庫之后,我們的數據存儲在哪里?為了保證訪問的高性能,我們選擇存儲在內存中,這又需要有存儲模塊。存在內存中的數據,雖然訪問速度快,但存在的的問題就是斷電后,無法恢復數據,所以我們還需要支持持久化操作。
有了存儲模塊,我們還需要考慮,數據是以什么樣的形式存儲?怎樣設計才能讓數據操作更優,這就設計到了,數據類型的支持,索引模塊。 索引的作用是讓鍵值數據庫根據 key 找到相應 value 的存儲位置,進而執行操作。
有了以上模塊的只是,我們是不是要對數據進行操作了?比如往KV數據庫中插入或更新一條數據,刪除和查詢,這就是需要有操作模塊了。
至此我們已經構造出了一個KV數據庫的基本框架了,帶著這些架構,我們再深入到每個點中去探究,這樣就會輕松很多,不會迷失在末枝細節中了。
我們都知道Redis訪問快,這是因為redis的操作都是在內存上的,內存的訪問本身就很快,另外Redis底層的數據結構也對“快”起到了至關重要的作用。
我們平常所以所說Redis的5種數據結構:String、Hash、Set、ZSet和List指的只是鍵值對中值的數據結構,而我這里所說的數據結構,指的是它們底層實現。
Redis的底層數據結構有:簡單動態字符串、整數數組、壓縮列表、跳表、hash表、雙向列表6種。
簡單動態數組:就是String的底層實現
其中整數數組、hash表、雙向列表都是我們常見的數據結構
壓縮列表和跳表屬于特殊的數據結構
壓縮列表是Redis實現的特殊的數組:它本質就是一個數組,只不過,我們常見的數組的每個元素分配的空間大小是一致的,這樣就會導致有多余的內存空間被浪費了。壓縮列表就是為了解決這樣的問題,它的每個元素大小是按實際大小分配的,避免了內存的浪費,同時在壓縮列表的表頭還存了關于該列表的相關屬性:用于記錄列表個數zllen,表尾偏移量zltail和列表長度zlbytes。表尾還有一個zlend標記列表的結束。
跳表:有序鏈表查詢元素只能逐一查詢,跳表本質上就是鏈表的基礎上加了多級索引,通過多級索引的幾個跳轉,快遞定位到元素所在位置。
不同數據結構的查詢時間復雜度
上面從存儲方面解釋了,redis為什么快.
逆向思維可以說為什么不用多線程,這個我們得先看下多線程存在哪些問題?在正常應用操作中,使用多線程可以大大提高處理的時間。那是不是可以無限地加大線程數量,以獲取更快的處理速度?實際試驗后,發現在機器資源有限的情況下,不斷增加線程處理時間,并沒有像我們想象的那樣成線性增長,而是到達一定階段就趨于平衡,甚至有下降的趨勢,這是為什么呢?
其實主要有兩個方面,我們知道線程是CPU調度的最小單元,當線程多的時候,CPU需要不停的切換線程,線程切換是需要消耗時間的,當大量線程需要來回切換,那么CPU在這切換的損耗了很多時間。
另外當多個線程,需要對共享資源進行操作的時候,為了保證并發安全性,需要有額外的機制保證,比如加鎖。這樣就使得當多個線程在操作共享數據時,變成了串行。
所以為了避免這些問題,Redis采用了單線程操作數據。
我們知道Redis單線程操作的,但是只是指的Redis對外提供鍵值對存儲服務是單線程的。Redis的其他功能并不是,比如持久化,異步刪除,集群同步等,都是由額外的線程去執行的。
除了上面說的,Redis的大部分操作都是在內存上完成的,加上高效的數據結構,是他實現高性能的一方面。另外一方面Redis采用的多路復用機制,使其在網絡IO操作中能并發處理大量的客戶端請求。
在網絡 IO 操作中,有潛在的阻塞點,分別是 accept() 和 recv()。當 Redis 監聽到一個客戶端有連接請求,但一直未能成功建立起連接時,會阻塞在 accept() 函數這里,導致其他客戶端無法和 Redis 建立連接。類似的,當 Redis 通過 recv() 從一個客戶端讀取數據時,如果數據一直沒有到達,Redis 也會一直阻塞在 recv()。 這就導致 Redis 整個線程阻塞,無法處理其他客戶端請求,效率很低。不過,幸運的是,socket 網絡模型本身支持非阻塞模式。
Socket 網絡模型的非阻塞模式設置,主要體現在三個關鍵的函數調用上,如果想要使用 socket 非阻塞模式,就必須要了解這三個函數的調用返回類型和設置模式。接下來,我們就重點學習下它們。在 socket 模型中,不同操作調用后會返回不同的套接字類型。socket() 方法會返回主動套接字,然后調用 listen() 方法,將主動套接字轉化為監聽套接字,此時,可以監聽來自客戶端的連接請求。最后,調用 accept() 方法接收到達的客戶端連接,并返回已連接套接字。
針對監聽套接字,我們可以設置非阻塞模式:當 Redis 調用 accept() 但一直未有連接請求到達時,Redis 線程可以返回處理其他操作,而不用一直等待。但是,你要注意的是,調用 accept() 時,已經存在監聽套接字了。
類似的,我們也可以針對已連接套接字設置非阻塞模式:Redis 調用 recv() 后,如果已連接套接字上一直沒有數據到達,Redis 線程同樣可以返回處理其他操作。我們也需要有機制繼續監聽該已連接套接字,并在有數據達到時通知 Redis。這樣才能保證 Redis 線程,既不會像基本 IO 模型中一直在阻塞點等待,也不會導致 Redis 無法處理實際到達的連接請求或數據。
Linux 中的 IO 多路復用機制是指一個線程處理多個 IO 流,就是我們經常聽到的 select/epoll 機制。簡單來說,在 Redis 只運行單線程的情況下,該機制允許內核中,同時存在多個監聽套接字和已連接套接字。內核會一直監聽這些套接字上的連接請求或數據請求。一旦有請求到達,就會交給 Redis 線程處理,這就實現了一個 Redis 線程處理多個 IO 流的效果。為了在請求到達時能通知到 Redis 線程,select/epoll 提供了基于事件的回調機制,即針對不同事件的發生,調用相應的處理函數。
因為Redis是操作是基于內存的,所有一點系統宕機存在內存中的數據就會丟失,為了實現數據的持久化,Redis中存在兩個持久化機制AOF和RBD。
AOF的原理就是,通過記錄下Redis的所有命令操作,在需要數據恢復的時候,再按照順序把所有命令執行一次,從而恢復數據。
但跟數據庫的寫前日志不同的,AOF采用的寫后日志,也就是在Redis執行過操作之后,再寫入AOF日志。之所以為什么采用寫后日志,可以避免因為寫日志的占用redis調用的時間,另外為了保證Redis的高性能,在寫aof日志的時候,不會做校驗,若采用寫前日志,如果命令是錯誤非法的,在恢復數據的時候就會出現異常。采用寫后日志,只有命令執行成功的才會被保存。
AOF的執行策略有三種
all:每次寫入/刪除命令都會被寫入日志文件中,保證了數據可靠性,但是寫入日志,涉及到了磁盤的IO,必然會影響性能
everysec:每秒鐘執行一次日志寫入,在一秒之內的命令操作會記錄在aof內存緩沖區,每一秒會寫回到日志文件中,相對于每次寫入性能得以提升,但是在aof緩沖區沒有來得及回寫到日志文件中時,系統發生宕機就會丟失這部分數據。
no:內存緩沖區的命令記錄不會不主動寫回到日志文件中,而交給操作系統決定。這種策略性能最高,但是丟失數據的風險也最大。
但是AOF文件過大,會帶來性能問題,所有AOF重寫機制就登場了。
AOF重寫的原理是,將多個命令對同一個key的操作合并成一個,因為數據恢復時,我們只要關心數據最后的狀態就可以了。
需要注意的是,與AOF日志由主線程寫回不同,重寫過程是由后臺子線程bgwriteaof來完成的,這個避免阻塞主線程,導致數據庫性能下降。
每次 AOF 重寫時,Redis 會先執行一個內存拷貝,用于重寫;然后,使用兩個日志保證在重寫過程中,新寫入的數據不會丟失。而且,因為 Redis 采用額外的線程進行數據重寫,所以,這個過程并不會阻塞主線程。
所謂內存快照,就是指內存中的數據在某一個時刻的狀態記錄。對 Redis 來說,就是把某一時刻的狀態以文件的形式寫到磁盤上。
Redis執行RDB的策略是什么?
Redis進行快照的時候,是進行全量的快照,并且為了不阻塞主線程,會默認使用bgsave命令創建一個子線程,專門用于寫入RDB文件。
快照期間數據還能修改嗎?
如果不能修改,那么在快照期間,這塊數據就會只能讀取不能修改,那么必然影響使用。如果可以修改,那么Redis是如何實現的?其實Redis是借助操作系統的寫時復制,在執行快照期間,讓修改的數據,會在內存中拷貝出一份副本,副本的數據可以被寫入rdb文件中,而主線程仍然可以修改原數據。
多久執行一次呢?
跟aof同樣的問題,如果快照頻率低,那么在兩次快照期間出現宕機,就會出現數據不完整的情況,如果快照頻率過快,那么又會出現兩個問題,一個是不停的對磁盤寫出,增大磁盤壓力,可能上一次寫入還沒完成,新的快照又來了,造成惡性循環.另外雖然執行快照是主線程fork出來的,但是不停的fork的過程是阻塞主線程的。
那么如何配置才合適呢?
其實我們只需要第一次全量快照,后續只快照有數據變動的地方就可以大大降低快照的資源損耗了,那么如何記錄這變動的數據呢,這里我們可以想到aof具有這樣的功能。Redis4.0就提使用RDB+AOF混合模式來完成Redis的持久化。簡單來說,內存快照以一定的頻率執行,在兩次快照之間,使用 AOF 日志記錄這期間的所有命令操作。
前面我們通過Redis的持久化機制,來保證服務器宕機之后,通過回放日志和重新讀取RDB文件恢復數據,減少數據丟失的風險。
但是在單臺及其的情況下,機器發生宕機,就無法對外提供服務了。我們所說的Redis具有高可靠性,指的一是,數據盡量少丟失,之前持久化機制就解決了這一問題,另一個是服務盡量少中斷,Redis的做法是增加副本冗余量。Redis提供的主從模式,主從庫之間采用了讀寫分離的方式。
從庫只讀取,主庫執行讀與寫,寫的數據主庫會同步給從庫。之所以只讓主庫寫,是因為,如果從庫也寫,那么當客戶端對一個數據修改了3次,為了保證數據的正確性,就要設法讓主從庫對于寫操作協同,這會帶來巨額的開銷。
主從庫間如何進行第一次同步的?
當我們啟動多個 Redis 實例的時候,它們相互之間就可以通過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關系,之后會按照三個階段完成數據的第一次同步。
主庫收到 psync 命令后,會用 FULLRESYNC 響應命令帶上兩個參數:主庫 runID 和主庫目前的復制進度 offset,返回給從庫。從庫收到響應后,會記錄下這兩個參數。
這里有個地方需要注意,FULLRESYNC 響應表示第一次復制采用的全量復制,也就是說,主庫會把當前所有的數據都復制給從庫。
在第二階段,主庫將所有數據同步給從庫。從庫收到數據后,在本地完成數據加載。這個過程依賴于內存快照生成的 RDB 文件。
具體來說,主庫執行 bgsave 命令,生成 RDB 文件,接著將文件發給從庫。從庫接收到 RDB 文件后,會先清空當前數據庫,然后加載 RDB 文件。這是因為從庫在通過 replicaof 命令開始和主庫同步前,可能保存了其他數據。為了避免之前數據的影響,從庫需要先把當前數據庫清空。
在主庫將數據同步給從庫的過程中,主庫不會被阻塞,仍然可以正常接收請求。否則,Redis 的服務就被中斷了。但是,這些請求中的寫操作并沒有記錄到剛剛生成的 RDB 文件中。為了保證主從庫的數據一致性,主庫會在內存中用專門的 replication buffer,記錄 RDB 文件生成后收到的所有寫操作。
最后,也就是第三個階段,主庫會把第二階段執行過程中新收到的寫命令,再發送給從庫。具體的操作是,當主庫完成 RDB 文件發送后,就會把此時 replication buffer 中的修改操作發給從庫,從庫再重新執行這些操作。這樣一來,主從庫就實現同步了。
Redis在有了主從集群后,如果從庫掛了,Redis對外提供服務不受影響,主庫和其他從庫,依然可以提供讀寫服務,但是當主庫掛了之后,因為是讀寫分離的,如果此時有寫的請求,那么就無法處理了。Redis是如果解決這樣的問題的呢,這就要引入哨兵機制了。
當主庫掛了,我們需要從從庫中選出一個當做主庫,這樣就可以正常對外提供服務了。哨兵的本質就是一個Redis示例,只不過它是運行在特殊模式下的Redis進程。它主要有三個作用:監控、選舉、通知。
哨兵在監控到主庫下線的時候,會從從庫中通過一定的規則,選舉出適合的從庫當主庫,并通知其他從庫變更主庫的信息,讓他們執行replicaof命令,和新主庫建立連接,并進行數據復制。那么具體每一步都是怎么做的呢?
監控:哨兵會周期性向主從庫發送PING命令,檢測主庫是否正常運行,如果主從庫沒有在規定的時間內回應哨兵的PING命令,則會被判定為“下線狀態”,如果是主庫下線,則開始自動切換主庫的流程。但是一般如果只有一個哨兵,那么它的判斷可能不具有可靠性,所以一般哨兵都是采用集群模式部署,稱為哨兵集群。單多個哨兵均判斷該主庫下線了,那么可能他就真的下線了,這是一個少數服從多數的規則。
選舉: 哨兵選擇新主庫的過程稱為“篩選 + 打分”。簡單來說,我們在多個從庫中,先按照一定的篩選條件,把不符合條件的從庫去掉。然后,我們再按照一定的規則,給剩下的從庫逐個打分,將得分最高的從庫選為新主庫,如下圖所示:
1、排除那些已經下線的從庫,以及連接不穩定的從庫。連接不穩定是通過配置項down-after-milliseconds,當主從連接超時達到一定閾值,就會被記錄下來,比如設置的10次,那么就會標記該從庫網絡不好,不適合做為主庫。
2、篩選出從庫后,第二部就要開始打分了,主要從三方面打分,
1.從庫優先級,這是可以通過slave-property設置的,設置的高,打分的就高,就會被選為主庫,比如你可以給從庫中內存帶寬資源充足設置高優先級,當主庫掛了之后被優先選舉為主庫。
2.從庫與舊主庫之間的復制進度,之前我們知道主從之間增量復制,有個參數slave-repl-offset記錄當前的復制進度。這個數值越大,說明與主庫復制進度越靠近,打分也會越高。
? 3.每個從庫創建實例的時候,會隨機生成一個id,id越小的得分越高。
通知:哨兵提升一個從庫為新主庫后,哨兵會把新主庫的地址寫入自己實例的pubsub(switch-master)中??蛻舳诵枰嗛嗊@個pubsub,當這個pubsub有數據時,客戶端就能感知到主庫發生變更,同時可以拿到最新的主庫地址,然后把寫請求寫到這個新主庫即可,這種機制屬于哨兵主動通知客戶端。
如果客戶端因為某些原因錯過了哨兵的通知,或者哨兵通知后客戶端處理失敗了,安全起見,客戶端也需要支持主動去獲取最新主從的地址進行訪問。
所以,客戶端需要訪問主從庫時,不能直接寫死主從庫的地址了,而是需要從哨兵集群中獲取最新的地址(sentinel get-master-addr-by-name命令),這樣當實例異常時,哨兵切換后或者客戶端斷開重連,都可以從哨兵集群中拿到最新的實例地址。
部署哨兵集群的時候,我們知道只需要配置:sentinel monitor 跟主庫通信就可以了,并不知道其他哨兵的信息,那么是如何知道的呢?
Redis有提供了pub/sub機制,哨兵跟主庫建立了連接之后,將自己的信息發布到 “sentinel:hello”頻道上,其他哨兵發布并訂閱了該頻道,就可以獲取其他哨兵的信息,那么哨兵之間就可以相互通信了。
那么哨兵如何知道從庫的連接信息呢,那是因為INFO命令,哨兵向主庫發送該命令后,獲得了所有從庫的連接信息,就能分從庫建立連接,并進行監控了。
從本質上說,哨兵就是一個運行在特定模式下的 Redis 實例,只不過它并不服務請求操作,只是完成監控、選主和通知的任務。所以,每個哨兵實例也提供 pub/sub 機制,客戶端可以從哨兵訂閱消息。哨兵提供的消息訂閱頻道有很多,不同頻道包含了主從庫切換過程中的不同關鍵事件。
與mysql一樣,當一張表的數據很大時,查詢耗時可能就會越來越大,我們采取的措施是分表分庫。同樣的Redis也樣,當數據量很大時,比如高達25G,在單分片下,我們需要機器有32G的內存。但是我們會發現,有時候redis響應會變得很慢,通過INFO查詢Redis的latest_fork_usec指標,最近fork耗時,發現耗時很大,快到秒級別了,fork這個動作會阻塞主線程,于是就導致了Redis變慢了。
于是就有redis分片集群, 啟動多個 Redis 實例組成一個集群,然后按照一定的規則,把收到的數據劃分成多份,每一份用一個實例來保存?;氐轿覀儎倓偟膱鼍爸?,如果把 25GB 的數據平均分成 5 份(當然,也可以不做均分),使用 5 個實例來保存,每個實例只需要保存 5GB 數據。
那么,在切片集群中,實例在為 5GB 數據生成 RDB 時,數據量就小了很多,fork 子進程一般不會給主線程帶來較長時間的阻塞。采用多個實例保存數據切片后,我們既能保存 25GB 數據,又避免了 fork 子進程阻塞主線程而導致的響應突然變慢。
那么數據是如何決定存在在哪個分片上的呢?
Redis Cluster 方案采用哈希槽(Hash Slot,接下來我會直接稱之為 Slot),來處理數據和實例之間的映射關系。在 Redis Cluster 方案中,一個切片集群共有 16384 個哈希槽,這些哈希槽類似于數據分區,每個鍵值對都會根據它的 key,被映射到一個哈希槽中。具體的映射過程分為兩大步:首先根據鍵值對的 key,按照CRC16 算法計算一個 16 bit 的值;然后,再用這個 16bit 值對 16384 取模,得到 0~16383 范圍內的模數,每個模數代表一個相應編號的哈希槽。
我們在部署 Redis Cluster 方案時,可以使用 cluster create 命令創建集群,此時,Redis 會自動把這些槽平均分布在集群實例上。例如,如果集群中有 N 個實例,那么,每個實例上的槽個數為 16384/N 個。 也可以使用 cluster meet 命令手動建立實例間的連接,形成集群,再使用 cluster addslots 命令,指定每個實例上的哈希槽個數。
前面介紹了Redis相關知識,了解了Redis的高可用,高性能的原因。很多人認為提到緩存,就局限于Redis,其實緩存的應用不僅僅在于Redis的使用,比如還有Nginx緩存,緩存隊列等等。下面我們會將講解Nginx+Lua實現多級緩存方法,來解決高并發訪問的場景。
我們來看一張微服務架構緩存的使用
我們可以看到微服務架構中,會大量使用到緩存
1.客戶端緩存(手機、PC)
2.Nginx緩存
3.微服務網關限流令牌緩存
4.Nacos緩存服務列表、配置文件
5.各大微服務自身也具有緩存
6.數據庫查詢Query Cache
7.Redis集群緩存
8.Kafka也屬于緩存
應對高并發的最有效手段之一就是分布式緩存,分布式緩存不僅僅是緩存要顯示的數據這么簡單,還可以在限流、隊列削峰、高速讀寫、分布式鎖等場景發揮重大作用。分布式緩存可以說是解決高并發場景的有效利器。以以下場景為例:
1、凌晨突然涌入的巨大流量?!娟犃行g】【限流術】
2、高并發場景秒殺、搶紅包、搶優惠券,快速存取?!揪彺嫒〈鶰ySQL操作】
3、高并發場景超賣、超額搶紅包。【Redis單線程取代數據庫操作】
4、高并發場景重復搶單?!綬edis搶單計數器】
一談到緩存架構,很多人想到的是Redis,但其實整套體系的緩存架構并非只有Redis,而應該是多個層面多個軟
件結合形成一套非常良性的緩存體系。比如咱們的緩存架構設計就涉及到了多個層面的緩存軟件。
1、HTML頁面做緩存,瀏覽器端可以緩存HTML頁面和其他靜態資源,防止用戶頻繁刷新對后端造成巨大壓力
2、Lvs實現記錄不同協議以及不同用戶請求鏈路緩存
3、Nginx這里會做HTML頁面緩存配置以及Nginx自身緩存配置
4、數據查找這里用Lua取代了其他語言查找,提高了處理的性能效率,并發處理能力將大大提升
5、數據緩存采用了Redis集群+主從架構,并實現緩存讀寫分離操作
6、集成Canal實現數據庫數據增量實時同步Redis
客戶端側緩存一般指的是瀏覽器緩存、app緩存等等,目的就是加速各種靜態資源的訪問,降低服務器壓力。我們通過配置Nginx設置網頁緩存信息,從而降低用戶對服務器頻繁訪問造成的巨大壓力。
HTTP 中最基本的緩存機制,涉及到的 HTTP 頭字段,包括 Cache‐Control, Last‐Modified, If‐Modified‐Since,
Etag,If‐None‐Match 等。
Last‐Modified/If‐Modified‐Since
Etag是服務端的一個資源的標識,在 HTTP 響應頭中將其傳送到客戶端。所謂的服務端資源可以是一個Web頁面,也可
以是JSON或XML等。服務器單獨負責判斷記號是什么及其含義,并在HTTP響應頭中將其傳送到客戶端。比如,瀏覽器第
一次請求一個資源的時候,服務端給予返回,并且返回了ETag: "50b1c1d4f775c61:df3" 這樣的字樣給瀏覽器,當瀏
覽器再次請求這個資源的時候,瀏覽器會將If‐None‐Match: W/"50b1c1d4f775c61:df3" 傳輸給服務端,服務端拿到
該ETAG,對比資源是否發生變化,如果資源未發生改變,則返回304HTTP狀態碼,不返回具體的資源。
Last‐Modified :標示這個響應資源的最后修改時間。web服務器在響應請求時,告訴瀏覽器資源的最后修改時間。
If‐Modified‐Since :當資源過期時(使用Cache‐Control標識的max‐age),發現資源具有 Last‐Modified 聲
明,則再次向web服務器請求時帶上頭。
If‐Modified‐Since ,表示請求時間。web服務器收到請求后發現有頭 If‐Modified‐Since 則與被請求資源的最后修
改時間進行比對。若最后修改時間較新,說明資源有被改動過,則響應整片資源內容(寫在響應消息包體內),HTTP
200;若最后修改時間較舊,說明資源無新修改,則響應 HTTP 304 (無需包體,節省瀏覽),告知瀏覽器繼續使用所保
存的 cache 。
Pragma行是為了兼容 HTTP1.0 ,作用與 Cache‐Control: no‐cache 是一樣的
Etag/If‐None‐Match
Etag :web服務器響應請求時,告訴瀏覽器當前資源在服務器的唯一標識(生成規則由服務器決定),如果給定URL中的
資源修改,則一定要生成新的Etag值。
If‐None‐Match :當資源過期時(使用Cache‐Control標識的max‐age),發現資源具有Etage聲明,則再次向web服
務器請求時帶上頭 If‐None‐Match (Etag的值)。web服務器收到請求后發現有頭 If‐None‐Match 則與被請求資源
的相應校驗串進行比對,決定返回200或304。
Etag:
Last‐Modified 標注的最后修改只能精確到秒級,如果某些文件在1秒鐘以內,被修改多次的話,它將不能準確標注文
件的修改時間,如果某些文件會被定期生成,當有時內容并沒有任何變化,但 Last‐Modified 卻改變了,導致文件沒
法使用緩存有可能存在服務器沒有準確獲取文件修改時間,或者與代理服務器時間不一致等情形 Etag是服務器自動生成
或者由開發者生成的對應資源在服務器端的唯一標識符,能夠更加準確的控制緩存。 Last‐Modified 與 ETag 是可以
一起使用的,服務器會優先驗證 ETag ,一致的情況下,才會繼續比對 Last‐Modified ,最后才決定是否返回304。
用戶如果請求獲取的數據不是需要后端服務器處理返回,如果我們需要對數據做緩存來提高服務器的處理能力,我們可以按照如下步驟實現:
1、請求Nginx,Nginx將請求路由給后端服務
2、后端服務查詢Redis或者MySQL,再將返回結果給Nginx
3、Nginx將結果存入到Nginx緩存,并將結果返回給用戶
4、用戶下次執行同樣請求,直接在Nginx中獲取緩存數據
具體流程
1、用戶請求經過Nginx
2、Nginx檢查是否有緩存,如果Nginx有緩存,直接響應用戶數據
3、Nginx如果沒有緩存,則將請求路由給后端Java服務
4、Java服務查詢Redis緩存,如果有數據,則將數據直接響應給Nginx,并將數據存入緩存,Nginx將數據響應給用戶
5、如果Redis沒有緩存,則使用Java程序查詢MySQL,并將數據存入到Reids,再將數據存入到Nginx中
優缺點
優點:
1、采用了Nginx緩存,減少了數據加載的路徑,從而提升站點數據加載效率
2、多級緩存有效防止了緩存擊穿、緩存穿透問題
缺點
Tomcat并發量偏低,導致緩存同步并發量失衡,緩存首次加載效率偏低,Tomcat 大規模集群占用資源高
優點
1、采用了Nginx緩存,減少了數據加載的路徑,從而提升站點數據加載效率
2、多級緩存有效防止了緩存擊穿、緩存穿透問題
3、使用了Nginx+Lua集成,無論是哪次緩存加載,效率都高
4、Nginx并發量高,Nginx+Lua集成,大幅提升了并發能力
上面我們已經分析過紅包雨的特點,要想實現一套高效的紅包雨系統,緩存架構是關鍵。我們根據紅包雨的特點設計了如上圖所示的紅包雨緩存架構體系。
1、紅包雨分批次導入到Redis緩存而不要每次操作數據庫
2、很多用戶搶紅包的時候,為了避免1個紅包被多人搶到,我們要采用Redis的隊列存儲紅包
3、追加紅包的時候,可以追加延時發放紅包,也可以直接追加立即發放紅包
4、用戶搶購紅包的時候,會先經過Nginx,通過Lua腳本查看緩存中是否存在紅包,如果不存在紅包,則直接終止搶紅包
5、如果還存在紅包,為了避免后臺同時處理很多請求,這里采用隊列術緩存用戶請求,后端通過消費隊列執行搶紅包
1、隊列控制并發溢出:并發量非常大的系統,例如秒殺、搶紅包、搶票等操作,都是存在溢出現象,比如秒殺超賣、搶紅包超額、一票多單等溢出現象,如果采用數據庫鎖來控制溢出問題,效率非常低,在高并發場景下,很有可能直接導致數據庫崩潰,因此針對高并發場景下數據溢出解決方案我們可以采用Redis緩存提升效率。
2、隊列限流:解決大量并發用戶蜂擁而上的方法可以采用隊列術將用戶的請求用隊列緩存起來,后端服務從隊列緩存中有序消費,可以防止后端服務同時面臨處理大量請求。緩存用戶請求可以用RabbitMQ、Kafka、RocketMQ、ActiveMQ等。用戶搶紅包的時候,我們用Lua腳本實現將用戶搶紅包的信息以生產者角色將消息發給RabbitMQ,后端應用服務以消費者身份從RabbitMQ獲取消息并搶紅包,再將搶紅包信息以WebSocket方式通知給用戶。
nginx提供兩種限流的方式:一是控制速率,二是控制并發連接數。
1、速率限流
控制速率的方式之一就是采用漏桶算法。具體配置如下:
2、控制并發量
ngx_http_limit_conn_module 提供了限制連接數的能力。主要是利用limit_conn_zone和limit_conn兩個指令。利用連接數限制 某一個用戶的ip連接的數量來控制流量。
(1)配置限制固定連接數
如下,配置如下:
配置限流緩存空間:
根據IP地址來限制,存儲內存大小10M
limit_conn_zone $binary_remote_addr zone=addr:1m;
location配置:
limit_conn addr 2;
參數說明:
limit_conn_zone $binary_remote_addr zone=addr:10m; 表示限制根據用戶的IP地址來顯示,設置存儲地址為的
內存大小10M
limit_conn addr 2; 表示 同一個地址只允許連接2次。
(2)限制每個客戶端IP與服務器的連接數,同時限制與虛擬服務器的連接總數。
限流緩存空間配置:
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
location配置
limit_conn perip 10;#單個客戶端ip與服務器的連接數
limit_conn perserver 100; #限制與服務器的總連接數
每個IP限流 3個
總量5個
產生原因
當我們查詢一個緩存不存在的數據,就去查數據庫,但此時如果數據庫也沒有這個數據,后面繼續訪問依然會再次查詢數據庫,當有用戶大量請求不存在的數據,必然會導致數據庫的壓力升高,甚至崩潰。
如何解決
1、當查詢到不存在的數據,也將對應的key放入緩存,值為nul,這樣再次查詢會直接返回null,如果后面新增了該key的數據,就覆蓋即可。
2、使用布隆過濾器。布隆過濾器主要是解決大規模數據下不需要精確過濾的業務場景,如檢查垃圾郵件地址,爬蟲URL地址去重,解決緩存穿透問題等。
產生原因
當緩存在某一刻過期了,一般如果再查詢這個緩存,會從數據庫去查詢一次再放到緩存,如果正好這一刻,大量的請求該緩存,那么請求都會打到數據庫中,可能導致數據庫打垮。
如何解決
1、盡量避免緩存過期時間都在同一時間。
2、定時任務主動刷新更新緩存,或者設置緩存不過去,適合那種key相對固定,粒度較大的業務。
? 分享下我在公司的負責的系統是如何防止緩存擊穿的,由于業務場景,緩存的數據都是當天有效的,當天查詢的只查當日有效的數據,所以當時數據都是設置當天凌晨過期,并且緩存是懶加載,這樣導致0點高峰期數據庫壓力明顯增大。后來改造了下,做了個定時任務,每天凌晨3點,跑第二天生效的數據,并且設置失效時間延長一天。有效解決了該問題,相當于緩存預熱。
3、多級緩存
采用多級緩存也可以有效防止擊穿現象,首先通過程序將緩存存入到Redis緩存,且永不過期,用戶查詢的時候,先查詢Nginx緩存,如果Nginx緩存沒有,則查詢Redis緩存,并將Redis緩存存入到Nginx一級緩存中,并設置更新時間。這種方案不僅可以提升查詢速度,同時又能防止擊穿問題,并且提升了程序的抗壓能力。
4、分布式鎖與隊列。解決思路主要是防止多請求同時打過去。分布式鎖,推薦使用Redisson。隊列方案可以使用nginx緩存隊列,配置如下。
產生原因
緩存雪崩是指,由于緩存層承載著大量請求,有效的保護了存儲層,但是如果緩存層由于某些原因整體不能提供服
務,于是所有的請求都會達到存儲層,存儲層的調用量會暴增,造成存儲層也會掛掉的情況。
如何解決
1、做緩存集群。即使個別節點、個別機器、甚至是機房宕掉,依然可以提供服務,比如 Redis Sentinel 和 Redis Cluster 都實現了高可用。
2、做好限流。微服務網關或者Nginx做好限流操作,防止大量請求直接進入后端,使后端載荷過重最后宕機。
3、緩存預熱。預先去更新緩存,再即將發生大并發訪問前手動觸發加載緩存不同的key,設置不同的過期時間,讓緩存失效的時間點盡量均勻,不要同時失效。
4、加鎖。數據操作,如果是帶有緩存查詢的,均使用分布式鎖,防止大量請求直接操作數據庫。
5、多級緩存。采用多級緩存,Nginx+Redis+MyBatis二級緩存,當Nginx緩存失效時,查找Redis緩存,Redis緩存失效查找MyBatis二級緩存。
問題描述
數據的在增量數據,未同步到緩存。導致緩存與數據庫數據不一致。
解決方案Canal
用戶每次操作數據庫的時候,使用Canal監聽數據庫指定表的增量變化,在Java程序中消費Canal監聽到的增量變化,并在Java程序中實現對Redis緩存或者Nginx緩存的更新。
用戶查詢的時候,先通過Lua查詢Nginx的緩存,如果Nginx緩存沒有數據,則查詢Redis緩存,Redis緩存如果也沒有數據,可以去數據庫查詢。
原文鏈接:https://www.cnblogs.com/whgk/p/13995972.html
分為強緩存和協商緩存
1、瀏覽器在加載資源時,先根據這個資源的一些http header判斷它是否命中強緩存,強緩存如果命中,瀏覽器直接從自己的緩存中讀取資源,不會發請求到服務器。比如某個css文件,如果瀏覽器在加載它所在的網頁時,這個css文件的緩存配置命中了強緩存,瀏覽器就直接從緩存中加載這個css,連請求都不會發送到網頁所在服務器。
2、當強緩存沒有命中的時候,瀏覽器一定會發送一個請求到服務器,通過服務器端依據資源的另外一些http header驗證這個資源是否命中協商緩存,如果協商緩存命中,服務器會將這個請求返回,但是不會返回這個資源的數據,而是告訴客戶端可以直接從緩存中加載這個資源,于是瀏覽器就又會從自己的緩存中去加載這個資源。
強緩存與協商緩存的共同點是:如果命中,都是從客戶端緩存中加載資源,而不是從服務器加載資源數據;區別是:強緩存不發請求到服務器,協商緩存會發請求到服務器。
當協商緩存也沒有命中的時候,瀏覽器直接從服務器加載資源數據。
2.1 介紹
當瀏覽器對某個資源的請求命中了強緩存時,返回的http狀態為200,在chrome的開發者工具的network里面size會顯示為from cache,比如京東的首頁里就有很多靜態資源配置了強緩存,用chrome打開幾次,再用f12查看network,可以看到有不少請求就是從緩存中加載的
強緩存是利用Expires或者Cache-Control這兩個http response header實現的,它們都用來表示資源在客戶端緩存的有效期。
Expires是http1.0提出的一個表示資源過期時間的header,它描述的是一個絕對時間,由服務器返回,用GMT格式的字符串表示,如:Expires:Thu, 31 Dec 2037 23:55:55 GMT
2.2 Expires緩存原理
1、瀏覽器第一次跟服務器請求一個資源,服務器在返回這個資源的同時,在respone的header加上Expires,如
2、瀏覽器在接收到這個資源后,會把這個資源連同所有response header一起緩存下來(所以緩存命中的請求返回的header并不是來自服務器,而是來自之前緩存的header)
3、瀏覽器再請求這個資源時,先從緩存中尋找,找到這個資源后,拿出它的Expires跟當前的請求時間比較,如果請求時間在Expires指定的時間之前,就能命中緩存,否則就不行
4、如果緩存沒有命中,瀏覽器直接從服務器加載資源時,Expires Header在重新加載的時候會被更新
Expires是較老的強緩存管理header,由于它是服務器返回的一個絕對時間,在服務器時間與客戶端時間相差較大時,緩存管理容易出現問題,比如隨意修改下客戶端時間,就能影響緩存命中的結果。所以在http1.1的時候,提出了一個新的header,就是Cache-Control,這是一個相對時間,在配置緩存的時候,以秒為單位,用數值表示,如:Cache-Control:max-age=315360000
2.3 Cache-Control緩存原理
1、瀏覽器第一次跟服務器請求一個資源,服務器在返回這個資源的同時,在respone的header加上Cache-Control,如:
2、瀏覽器在接收到這個資源后,會把這個資源連同所有response header一起緩存下來
3、瀏覽器再請求這個資源時,先從緩存中尋找,找到這個資源后,根據它第一次的請求時間和Cache-Control設定的有效期,計算出一個資源過期時間,再拿這個過期時間跟當前的請求時間比較,如果請求時間在過期時間之前,就能命中緩存,否則就不行
4、如果緩存沒有命中,瀏覽器直接從服務器加載資源時,Cache-Control Header在重新加載的時候會被更新
Cache-Control描述的是一個相對時間,在進行緩存命中的時候,都是利用客戶端時間進行判斷,所以相比較Expires,Cache-Control的緩存管理更有效,安全一些。
這兩個header可以只啟用一個,也可以同時啟用,當response header中,Expires和Cache-Control同時存在時,Cache-Control優先級高于Expires:
前面介紹的是強緩存的原理,在實際應用中我們會碰到需要強緩存的場景和不需要強緩存的場景,通常有2種方式來設置是否啟用強緩存
1、通過代碼的方式,在web服務器返回的響應中添加Expires和Cache-Control Header
2、通過配置web服務器的方式,讓web服務器在響應資源的時候統一添加Expires和Cache-Control Header
比如在javaweb里面,我們可以使用代碼設置強緩存
還可以通過java代碼設置不啟用強緩存
nginx和apache作為專業的web服務器,都有專門的配置文件,可以配置expires和cache-control,這方面的知識,如果你對運維感興趣的話,可以在百度上搜索nginx 設置 expires cache-control或 apache 設置 expires cache-control 都能找到不少相關的文章。
由于在開發的時候不會專門去配置強緩存,而瀏覽器又默認會緩存圖片,css和js等靜態資源,所以開發環境下經常會因為強緩存導致資源沒有及時更新而看不到最新的效果,解決這個問題的方法有很多,常用的有以下幾種
處理緩存帶來的問題
1、直接ctrl+f5,這個辦法能解決頁面直接引用的資源更新的問題
2、使用瀏覽器的隱私模式開發
3、如果用的是chrome,可以f12在network那里把緩存給禁掉(這是個非常有效的方法)
4、在開發階段,給資源加上一個動態的參數,如css/index.css?v=0.0001,由于每次資源的修改都要更新引用的位置,同時修改參數的值,所以操作起來不是很方便,除非你是在動態頁面比如jsp里開發就可以用服務器變量來解決(v=${sysRnd}),或者你能用一些前端的構建工具來處理這個參數修改的問題
5、如果資源引用的頁面,被嵌入到了一個iframe里面,可以在iframe的區域右鍵單擊重新加載該頁面,以chrome為例
6、如果緩存問題出現在ajax請求中,最有效的解決辦法就是ajax的請求地址追加隨機數
7、還有一種情況就是動態設置iframe的src時,有可能也會因為緩存問題,導致看不到最新的效果,這時候在要設置的src后面添加隨機數也能解決問題
8、如果你用的是grunt和gulp、webpack這種前端工具開發,通過它們的插件比如grunt-contrib-connect來啟動一個靜態服務器,則完全不用擔心開發階段的資源更新問題,因為在這個靜態服務器下的所有資源返回的respone header中,cache-control始終被設置為不緩存
強緩存是前端性能優化最有力的工具,沒有之一,對于有大量靜態資源的網頁,一定要利用強緩存,提高響應速度。通常的做法是,為這些靜態資源全部配置一個超時時間超長的Expires或Cache-Control,這樣用戶在訪問網頁時,只會在第一次加載時從服務器請求靜態資源,其它時候只要緩存沒有失效并且用戶沒有強制刷新的條件下都會從自己的緩存中加載,比如前面提到過的京東首頁緩存的資源,它的緩存過期時間都設置到了2026年
然而這種緩存配置方式會帶來一個新的問題,就是發布時資源更新的問題,比如某一張圖片,在用戶訪問第一個版本的時候已經緩存到了用戶的電腦上,當網站發布新版本,替換了這個圖片時,已經訪問過第一個版本的用戶由于緩存的設置,導致在默認的情況下不會請求服務器最新的圖片資源,除非他清掉或禁用緩存或者強制刷新,否則就看不到最新的圖片效果
這個問題已經有成熟的解決方案,具體內容可閱讀知乎這篇文章詳細了解:http://www.zhihu.com/question/20790576
文章提到的東西都屬于理論上的解決方案,不過現在已經有很多前端工具能夠實際地解決這個問題,由于每個工具涉及到的內容細節都有很多,本文沒有辦法一一深入介紹。有興趣的可以去了解下grunt gulp webpack fis 還有edp這幾個工具,基于這幾個工具都能解決這個問題,尤其是fis和edp是百度推出的前端開發平臺,有現成的文檔可以參考:
http://fis.baidu.com/fis3/api/index.html
http://ecomfe.github.io/edp/doc/initialization/install/
強緩存還有一點需要注意的是,通常都是針對靜態資源使用,動態資源需要慎用,除了服務端頁面可以看作動態資源外,那些引用靜態資源的html也可以看作是動態資源,如果這種html也被緩存,當這些html更新之后,可能就沒有機制能夠通知瀏覽器這些html有更新,尤其是前后端分離的應用里,頁面都是純html頁面,每個訪問地址可能都是直接訪問html頁面,這些頁面通常不加強緩存,以保證瀏覽器訪問這些頁面時始終請求服務器最新的資源
5.1 介紹
當瀏覽器對某個資源的請求沒有命中強緩存,就會發一個請求到服務器,驗證協商緩存是否命中,如果協商緩存命中,請求響應返回的http狀態為304并且會顯示一個Not Modified的字符串,比如你打開京東的首頁,按f12打開開發者工具,再按f5刷新頁面,查看network,可以看到有不少請求就是命中了協商緩存的
查看單個請求的Response Header,也能看到304的狀態碼和Not Modified的字符串,只要看到這個就可說明這個資源是命中了協商緩存,然后從客戶端緩存中加載的,而不是服務器最新的資源
5.2 Last-Modified,If-Modified-Since控制協商緩存
1、瀏覽器第一次跟服務器請求一個資源,服務器在返回這個資源的同時,在respone的header加上Last-Modified的header,這個header表示這個資源在服務器上的最后修改時間
2、瀏覽器再次跟服務器請求這個資源時,在request的header上加上If-Modified-Since的header,這個header的值就是上一次請求時返回的Last-Modified的值
3、服務器再次收到資源請求時,根據瀏覽器傳過來If-Modified-Since和資源在服務器上的最后修改時間判斷資源是否有變化,如果沒有變化則返回304 Not Modified,但是不會返回資源內容;如果有變化,就正常返回資源內容。當服務器返回304 Not Modified的響應時,response header中不會再添加Last-Modified的header,因為既然資源沒有變化,那么Last-Modified也就不會改變,這是服務器返回304時的response header
4、瀏覽器收到304的響應后,就會從緩存中加載資源
5、如果協商緩存沒有命中,瀏覽器直接從服務器加載資源時,Last-Modified Header在重新加載的時候會被更新,下次請求時,If-Modified-Since會啟用上次返回的Last-Modified值
【Last-Modified,If-Modified-Since】都是根據服務器時間返回的header,一般來說,在沒有調整服務器時間和篡改客戶端緩存的情況下,這兩個header配合起來管理協商緩存是非??煽康?,但是有時候也會服務器上資源其實有變化,但是最后修改時間卻沒有變化的情況,而這種問題又很不容易被定位出來,而當這種情況出現的時候,就會影響協商緩存的可靠性。所以就有了另外一對header來管理協商緩存,這對header就是【ETag、If-None-Match】。它們的緩存管理的方式是
5.3 ETag、If-None-Match控制協商緩存
1、瀏覽器第一次跟服務器請求一個資源,服務器在返回這個資源的同時,在respone的header加上ETag的header,這個header是服務器根據當前請求的資源生成的一個唯一標識,這個唯一標識是一個字符串,只要資源有變化這個串就不同,跟最后修改時間沒有關系,所以能很好的補充Last-Modified的問題
2、瀏覽器再次跟服務器請求這個資源時,在request的header上加上If-None-Match的header,這個header的值就是上一次請求時返回的ETag的值
3、服務器再次收到資源請求時,根據瀏覽器傳過來If-None-Match和然后再根據資源生成一個新的ETag,如果這兩個值相同就說明資源沒有變化,否則就是有變化;如果沒有變化則返回304 Not Modified,但是不會返回資源內容;如果有變化,就正常返回資源內容。與Last-Modified不一樣的是,當服務器返回304 Not Modified的響應時,由于ETag重新生成過,response header中還會把這個ETag返回,即使這個ETag跟之前的沒有變化
4、瀏覽器收到304的響應后,就會從緩存中加載資源。
協商緩存跟強緩存不一樣,強緩存不發請求到服務器,所以有時候資源更新了瀏覽器還不知道,但是協商緩存會發請求到服務器,所以資源是否更新,服務器肯定知道。大部分web服務器都默認開啟協商緩存,而且是同時啟用【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】,比如apache:
如果沒有協商緩存,每個到服務器的請求,就都得返回資源內容,這樣服務器的性能會極差。
【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】一般都是同時啟用,這是為了處理Last-Modified不可靠的情況。
有一種場景需要注意
分布式系統里多臺機器間文件的Last-Modified必須保持一致,以免負載均衡到不同機器導致比對失?。?/p>
分布式系統盡量關閉掉ETag(每臺機器生成的ETag都會不一樣);
京東頁面的資源請求,返回的repsones header就只有Last-Modified,沒有ETag:
協商緩存需要配合強緩存使用,你看前面這個截圖中,除了Last-Modified這個header,還有強緩存的相關header,因為如果不啟用強緩存的話,協商緩存根本沒有意義
如果資源已經被瀏覽器緩存下來,在緩存失效之前,再次請求時,默認會先檢查是否命中強緩存,如果強緩存命中則直接讀取緩存,如果強緩存沒有命中則發請求到服務器檢查是否命中協商緩存,如果協商緩存命中,則告訴瀏覽器還是可以從緩存讀取,否則才從服務器返回最新的資源。這是默認的處理方式,這個方式可能被瀏覽器的行為改變:
1、當ctrl+f5強制刷新網頁時,直接從服務器加載,跳過強緩存和協商緩存;
2、當f5刷新網頁時,跳過強緩存,但是會檢查協商緩存
*請認真填寫需求信息,我們會在24小時內與您取得聯系。