01 前言
我們將先從Redis、Nginx+Lua等技術(shù)點出發(fā),了解緩存應(yīng)用的場景衣陶。通過使用緩存相關(guān)技術(shù)柄瑰,解決高并發(fā)的業(yè)務(wù)場景案例,來深入理解一套成熟的企業(yè)級緩存架構(gòu)是如何設(shè)計的剪况。
02 Redis基礎(chǔ)
2.1 簡介
Redis是一個開源的使用ANSI C語言編寫教沾、遵守BSD協(xié)議、支持網(wǎng)絡(luò)译断、可基于內(nèi)存亦可持久化的日志型授翻、Key-Value數(shù)據(jù)庫,并提供多種語言的API孙咪。
它通常被稱為數(shù)據(jù)結(jié)構(gòu)服務(wù)器堪唐,因為值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等類型。
Redis 與其他 key - value 緩存產(chǎn)品有以下三個特點:
Redis支持?jǐn)?shù)據(jù)的持久化翎蹈,可以將內(nèi)存中的數(shù)據(jù)保存在磁盤中淮菠,重啟的時候可以再次加載進(jìn)行使用。
Redis不僅僅支持簡單的key-value類型的數(shù)據(jù)荤堪,同時還提供list合陵,set,zset逞力,hash等數(shù)據(jù)結(jié)構(gòu)的存儲曙寡。
Redis支持?jǐn)?shù)據(jù)的備份,即master-slave模式的數(shù)據(jù)備份寇荧。
優(yōu)勢
性能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。
豐富的數(shù)據(jù)類型 – Redis支持二進(jìn)制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 數(shù)據(jù)類型操作执隧。
原子 – Redis的所有操作都是原子性的揩抡,意思就是要么成功執(zhí)行要么失敗完全不執(zhí)行户侥。單個操作是原子性的。多個操作也支持事務(wù)峦嗤,即原子性蕊唐,通過MULTI和EXEC指令包起來。
豐富的特性 – Redis還支持 publish/subscribe, 通知, key 過期等等特性烁设。
2.2 數(shù)據(jù)類型
2.2.1 String(字符串)
string 是 redis 最基本的類型替梨,你可以理解成與 Memcached 一模一樣的類型,一個 key 對應(yīng)一個 value装黑。
string 類型是二進(jìn)制安全的副瀑。意思是 redis 的 string 可以包含任何數(shù)據(jù)。比如jpg圖片或者序列化的對象恋谭。
string 類型是 Redis 最基本的數(shù)據(jù)類型糠睡,string 類型的值最大能存儲 512MB。
redis 127.0.0.1:6379> SET runoob "laowang"
OK
redis 127.0.0.1:6379> GET runoob
"laowang"
2.2.2 Hash(哈希)
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"
2.2.3 List(列表)
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"
2.2.4 Set(集合)
Redis 的 Set 是 string 類型的無序集合。
集合是通過哈希表實現(xiàn)的清寇,所以添加喘漏,刪除,查找的復(fù)雜度都是 O(1)华烟。
sadd 命令 :添加一個 string 元素到 key 對應(yīng)的 set 集合中翩迈,成功返回 1,如果元素已經(jīng)在集合中返回 0盔夜。
集合中最大的成員數(shù)為 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"
2.2.5 zset(sorted set:有序集合)
Redis zset 和 set 一樣也是string類型元素的集合,且不允許重復(fù)的成員。
不同的是每個元素都會關(guān)聯(lián)一個double類型的分?jǐn)?shù)喂链。redis正是通過分?jǐn)?shù)來為集合中的成員進(jìn)行從小到大的排序返十。
zset的成員是唯一的,但分?jǐn)?shù)(score)卻可以重復(fù)。
zadd 命令 :添加元素到集合椭微,元素在集合中存在則更新對應(yīng)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"
03 Redis深入:帶著問題出發(fā)洞坑?
3.1 如果讓你設(shè)計一個KV數(shù)據(jù)庫,該如何設(shè)計
對這個問題的思考,將有助于我們從整體架構(gòu)上去學(xué)習(xí)Redis。
假設(shè)現(xiàn)在我們已經(jīng)設(shè)計好了一個KV數(shù)據(jù)庫蝇率,首先如果我們要使用迟杂,是不是得有入口刽沾,我們是通過動態(tài)鏈接庫還是通過網(wǎng)絡(luò)socket對外提供訪問入口,這就涉及到了訪問模塊排拷。Redis就是通過
通過訪問模塊訪問KV數(shù)據(jù)庫之后侧漓,我們的數(shù)據(jù)存儲在哪里?為了保證訪問的高性能监氢,我們選擇存儲在內(nèi)存中布蔗,這又需要有存儲模塊。存在內(nèi)存中的數(shù)據(jù)浪腐,雖然訪問速度快纵揍,但存在的的問題就是斷電后,無法恢復(fù)數(shù)據(jù)牛欢,所以我們還需要支持持久化操作骡男。
有了存儲模塊,我們還需要考慮傍睹,數(shù)據(jù)是以什么樣的形式存儲隔盛?怎樣設(shè)計才能讓數(shù)據(jù)操作更優(yōu),這就設(shè)計到了拾稳,數(shù)據(jù)類型的支持吮炕,索引模塊。 索引的作用是讓鍵值數(shù)據(jù)庫根據(jù) key 找到相應(yīng) value 的存儲位置访得,進(jìn)而執(zhí)行操作龙亲。
有了以上模塊的只是,我們是不是要對數(shù)據(jù)進(jìn)行操作了悍抑?比如往KV數(shù)據(jù)庫中插入或更新一條數(shù)據(jù)鳄炉,刪除和查詢,這就是需要有操作模塊了搜骡。
至此我們已經(jīng)構(gòu)造出了一個KV數(shù)據(jù)庫的基本框架了拂盯,帶著這些架構(gòu),我們再深入到每個點中去探究记靡,這樣就會輕松很多谈竿,不會迷失在末枝細(xì)節(jié)中了。
3.2 Redis為什么那么快摸吠?
我們都知道Redis訪問快空凸,這是因為redis的操作都是在內(nèi)存上的,內(nèi)存的訪問本身就很快寸痢,另外Redis底層的數(shù)據(jù)結(jié)構(gòu)也對“快”起到了至關(guān)重要的作用呀洲。
我們平常所以所說Redis的5種數(shù)據(jù)結(jié)構(gòu):String、Hash、Set两嘴、ZSet和List指的只是鍵值對中值的數(shù)據(jù)結(jié)構(gòu)丛楚,而我這里所說的數(shù)據(jù)結(jié)構(gòu)族壳,指的是它們底層實現(xiàn)憔辫。
Redis的底層數(shù)據(jù)結(jié)構(gòu)有:簡單動態(tài)字符串、整數(shù)數(shù)組仿荆、壓縮列表贰您、跳表、hash表拢操、雙向列表6種锦亦。
簡單動態(tài)數(shù)組:就是String的底層實現(xiàn)
其中整數(shù)數(shù)組、hash表令境、雙向列表都是我們常見的數(shù)據(jù)結(jié)構(gòu)
壓縮列表和跳表屬于特殊的數(shù)據(jù)結(jié)構(gòu)
壓縮列表是Redis實現(xiàn)的特殊的數(shù)組:它本質(zhì)就是一個數(shù)組,只不過,我們常見的數(shù)組的每個元素分配的空間大小是一致的,這樣就會導(dǎo)致有多余的內(nèi)存空間被浪費了杠园。壓縮列表就是為了解決這樣的問題,它的每個元素大小是按實際大小分配的舔庶,避免了內(nèi)存的浪費抛蚁,同時在壓縮列表的表頭還存了關(guān)于該列表的相關(guān)屬性:用于記錄列表個數(shù)zllen,表尾偏移量zltail和列表長度zlbytes惕橙。表尾還有一個zlend標(biāo)記列表的結(jié)束瞧甩。
跳表:有序鏈表查詢元素只能逐一查詢,跳表本質(zhì)上就是鏈表的基礎(chǔ)上加了多級索引弥鹦,通過多級索引的幾個跳轉(zhuǎn)肚逸,快遞定位到元素所在位置。
不同數(shù)據(jù)結(jié)構(gòu)的查詢時間復(fù)雜度
上面從存儲方面解釋了,redis為什么快.
3.2.1 為什么用單線程彬坏?
逆向思維可以說為什么不用多線程朦促,這個我們得先看下多線程存在哪些問題?在正常應(yīng)用操作中栓始,使用多線程可以大大提高處理的時間务冕。那是不是可以無限地加大線程數(shù)量,以獲取更快的處理速度混滔?實際試驗后洒疚,發(fā)現(xiàn)在機(jī)器資源有限的情況下,不斷增加線程處理時間坯屿,并沒有像我們想象的那樣成線性增長油湖,而是到達(dá)一定階段就趨于平衡,甚至有下降的趨勢领跛,這是為什么呢乏德?
其實主要有兩個方面,我們知道線程是CPU調(diào)度的最小單元,當(dāng)線程多的時候喊括,CPU需要不停的切換線程胧瓜,線程切換是需要消耗時間的,當(dāng)大量線程需要來回切換郑什,那么CPU在這切換的損耗了很多時間府喳。
另外當(dāng)多個線程,需要對共享資源進(jìn)行操作的時候蘑拯,為了保證并發(fā)安全性钝满,需要有額外的機(jī)制保證,比如加鎖申窘。這樣就使得當(dāng)多個線程在操作共享數(shù)據(jù)時弯蚜,變成了串行。
所以為了避免這些問題剃法,Redis采用了單線程操作數(shù)據(jù)碎捺。
3.2.2 單線程為什么還真這么快?
我們知道Redis單線程操作的,但是只是指的Redis對外提供鍵值對存儲服務(wù)是單線程的贷洲。Redis的其他功能并不是收厨,比如持久化,異步刪除恩脂,集群同步等帽氓,都是由額外的線程去執(zhí)行的。
除了上面說的俩块,Redis的大部分操作都是在內(nèi)存上完成的黎休,加上高效的數(shù)據(jù)結(jié)構(gòu),是他實現(xiàn)高性能的一方面玉凯。另外一方面Redis采用的多路復(fù)用機(jī)制势腮,使其在網(wǎng)絡(luò)IO操作中能并發(fā)處理大量的客戶端請求。
在網(wǎng)絡(luò) IO 操作中漫仆,有潛在的阻塞點捎拯,分別是 accept() 和 recv()。當(dāng) Redis 監(jiān)聽到一個客戶端有連接請求盲厌,但一直未能成功建立起連接時署照,會阻塞在 accept() 函數(shù)這里,導(dǎo)致其他客戶端無法和 Redis 建立連接吗浩。類似的建芙,當(dāng) Redis 通過 recv() 從一個客戶端讀取數(shù)據(jù)時,如果數(shù)據(jù)一直沒有到達(dá)懂扼,Redis 也會一直阻塞在 recv()禁荸。 這就導(dǎo)致 Redis 整個線程阻塞右蒲,無法處理其他客戶端請求,效率很低赶熟。不過瑰妄,幸運的是,socket 網(wǎng)絡(luò)模型本身支持非阻塞模式映砖。
Socket 網(wǎng)絡(luò)模型的非阻塞模式設(shè)置间坐,主要體現(xiàn)在三個關(guān)鍵的函數(shù)調(diào)用上,如果想要使用 socket 非阻塞模式啊央,就必須要了解這三個函數(shù)的調(diào)用返回類型和設(shè)置模式眶诈。接下來,我們就重點學(xué)習(xí)下它們瓜饥。在 socket 模型中,不同操作調(diào)用后會返回不同的套接字類型浴骂。socket() 方法會返回主動套接字乓土,然后調(diào)用 listen() 方法,將主動套接字轉(zhuǎn)化為監(jiān)聽套接字溯警,此時趣苏,可以監(jiān)聽來自客戶端的連接請求。最后梯轻,調(diào)用 accept() 方法接收到達(dá)的客戶端連接食磕,并返回已連接套接字。
針對監(jiān)聽套接字喳挑,我們可以設(shè)置非阻塞模式:當(dāng) Redis 調(diào)用 accept() 但一直未有連接請求到達(dá)時彬伦,Redis 線程可以返回處理其他操作,而不用一直等待伊诵。但是单绑,你要注意的是,調(diào)用 accept() 時曹宴,已經(jīng)存在監(jiān)聽套接字了搂橙。
類似的,我們也可以針對已連接套接字設(shè)置非阻塞模式:Redis 調(diào)用 recv() 后笛坦,如果已連接套接字上一直沒有數(shù)據(jù)到達(dá)区转,Redis 線程同樣可以返回處理其他操作。我們也需要有機(jī)制繼續(xù)監(jiān)聽該已連接套接字版扩,并在有數(shù)據(jù)達(dá)到時通知 Redis废离。這樣才能保證 Redis 線程,既不會像基本 IO 模型中一直在阻塞點等待资厉,也不會導(dǎo)致 Redis 無法處理實際到達(dá)的連接請求或數(shù)據(jù)厅缺。
Linux 中的 IO 多路復(fù)用機(jī)制是指一個線程處理多個 IO 流,就是我們經(jīng)常聽到的 select/epoll 機(jī)制。簡單來說湘捎,在 Redis 只運行單線程的情況下诀豁,該機(jī)制允許內(nèi)核中,同時存在多個監(jiān)聽套接字和已連接套接字窥妇。內(nèi)核會一直監(jiān)聽這些套接字上的連接請求或數(shù)據(jù)請求舷胜。一旦有請求到達(dá),就會交給 Redis 線程處理活翩,這就實現(xiàn)了一個 Redis 線程處理多個 IO 流的效果烹骨。為了在請求到達(dá)時能通知到 Redis 線程,select/epoll 提供了基于事件的回調(diào)機(jī)制材泄,即針對不同事件的發(fā)生沮焕,調(diào)用相應(yīng)的處理函數(shù)。
3.3 Redis是如何保證數(shù)據(jù)不丟失的拉宗?
因為Redis是操作是基于內(nèi)存的峦树,所有一點系統(tǒng)宕機(jī)存在內(nèi)存中的數(shù)據(jù)就會丟失,為了實現(xiàn)數(shù)據(jù)的持久化旦事,Redis中存在兩個持久化機(jī)制AOF和RBD魁巩。
3.3.1 AOF(Append Only File)介紹
AOF的原理就是,通過記錄下Redis的所有命令操作姐浮,在需要數(shù)據(jù)恢復(fù)的時候谷遂,再按照順序把所有命令執(zhí)行一次,從而恢復(fù)數(shù)據(jù)卖鲤。
但跟數(shù)據(jù)庫的寫前日志不同的肾扰,AOF采用的寫后日志,也就是在Redis執(zhí)行過操作之后扫尖,再寫入AOF日志白对。之所以為什么采用寫后日志,可以避免因為寫日志的占用redis調(diào)用的時間换怖,另外為了保證Redis的高性能甩恼,在寫aof日志的時候,不會做校驗沉颂,若采用寫前日志条摸,如果命令是錯誤非法的,在恢復(fù)數(shù)據(jù)的時候就會出現(xiàn)異常铸屉。采用寫后日志钉蒲,只有命令執(zhí)行成功的才會被保存。
3.3.2 AOF策略
AOF的執(zhí)行策略有三種
all:每次寫入/刪除命令都會被寫入日志文件中,保證了數(shù)據(jù)可靠性,但是寫入日志,涉及到了磁盤的IO,必然會影響性能
everysec:每秒鐘執(zhí)行一次日志寫入,在一秒之內(nèi)的命令操作會記錄在aof內(nèi)存緩沖區(qū),每一秒會寫回到日志文件中,相對于每次寫入性能得以提升,但是在aof緩沖區(qū)沒有來得及回寫到日志文件中時,系統(tǒng)發(fā)生宕機(jī)就會丟失這部分?jǐn)?shù)據(jù)彻坛。
no:內(nèi)存緩沖區(qū)的命令記錄不會不主動寫回到日志文件中,而交給操作系統(tǒng)決定顷啼。這種策略性能最高踏枣,但是丟失數(shù)據(jù)的風(fēng)險也最大。
3.3.3 AOF重寫機(jī)制
但是AOF文件過大钙蒙,會帶來性能問題茵瀑,所有AOF重寫機(jī)制就登場了。
AOF重寫的原理是躬厌,將多個命令對同一個key的操作合并成一個马昨,因為數(shù)據(jù)恢復(fù)時,我們只要關(guān)心數(shù)據(jù)最后的狀態(tài)就可以了扛施。
需要注意的是鸿捧,與AOF日志由主線程寫回不同,重寫過程是由后臺子線程bgwriteaof來完成的,這個避免阻塞主線程,導(dǎo)致數(shù)據(jù)庫性能下降疙渣。
每次 AOF 重寫時匙奴,Redis 會先執(zhí)行一個內(nèi)存拷貝,用于重寫昌阿;然后饥脑,使用兩個日志保證在重寫過程中,新寫入的數(shù)據(jù)不會丟失懦冰。而且,因為 Redis 采用額外的線程進(jìn)行數(shù)據(jù)重寫谣沸,所以刷钢,這個過程并不會阻塞主線程。
3.4 內(nèi)存快照RDB
3.4.1 RDB Redis DataBase
所謂內(nèi)存快照乳附,就是指內(nèi)存中的數(shù)據(jù)在某一個時刻的狀態(tài)記錄内地。對 Redis 來說,就是把某一時刻的狀態(tài)以文件的形式寫到磁盤上赋除。
Redis執(zhí)行RDB的策略是什么阱缓?
Redis進(jìn)行快照的時候,是進(jìn)行全量的快照举农,并且為了不阻塞主線程荆针,會默認(rèn)使用bgsave命令創(chuàng)建一個子線程,專門用于寫入RDB文件。
快照期間數(shù)據(jù)還能修改嗎?
如果不能修改颁糟,那么在快照期間航背,這塊數(shù)據(jù)就會只能讀取不能修改,那么必然影響使用棱貌。如果可以修改玖媚,那么Redis是如何實現(xiàn)的?其實Redis是借助操作系統(tǒng)的寫時復(fù)制婚脱,在執(zhí)行快照期間今魔,讓修改的數(shù)據(jù)勺像,會在內(nèi)存中拷貝出一份副本,副本的數(shù)據(jù)可以被寫入rdb文件中错森,而主線程仍然可以修改原數(shù)據(jù)吟宦。
多久執(zhí)行一次呢?
跟aof同樣的問題问词,如果快照頻率低督函,那么在兩次快照期間出現(xiàn)宕機(jī),就會出現(xiàn)數(shù)據(jù)不完整的情況激挪,如果快照頻率過快辰狡,那么又會出現(xiàn)兩個問題,一個是不停的對磁盤寫出垄分,增大磁盤壓力宛篇,可能上一次寫入還沒完成,新的快照又來了薄湿,造成惡性循環(huán).另外雖然執(zhí)行快照是主線程fork出來的,但是不停的fork的過程是阻塞主線程的。
那么如何配置才合適呢豺瘤?
其實我們只需要第一次全量快照吆倦,后續(xù)只快照有數(shù)據(jù)變動的地方就可以大大降低快照的資源損耗了,那么如何記錄這變動的數(shù)據(jù)呢坐求,這里我們可以想到aof具有這樣的功能蚕泽。Redis4.0就提使用RDB+AOF混合模式來完成Redis的持久化。簡單來說桥嗤,內(nèi)存快照以一定的頻率執(zhí)行须妻,在兩次快照之間,使用 AOF 日志記錄這期間的所有命令操作泛领。
3.5 主從庫是如何實現(xiàn)數(shù)據(jù)一致的荒吏?
前面我們通過Redis的持久化機(jī)制,來保證服務(wù)器宕機(jī)之后渊鞋,通過回放日志和重新讀取RDB文件恢復(fù)數(shù)據(jù),減少數(shù)據(jù)丟失的風(fēng)險绰更。 但是在單臺及其的情況下,機(jī)器發(fā)生宕機(jī)篓像,就無法對外提供服務(wù)了动知。我們所說的Redis具有高可靠性,指的一是员辩,數(shù)據(jù)盡量少丟失盒粮,之前持久化機(jī)制就解決了這一問題,另一個是服務(wù)盡量少中斷奠滑,Redis的做法是增加副本冗余量丹皱。Redis提供的主從模式妒穴,主從庫之間采用了讀寫分離的方式。
從庫只讀取摊崭,主庫執(zhí)行讀與寫讼油,寫的數(shù)據(jù)主庫會同步給從庫。之所以只讓主庫寫呢簸,是因為矮台,如果從庫也寫,那么當(dāng)客戶端對一個數(shù)據(jù)修改了3次,為了保證數(shù)據(jù)的正確性,就要設(shè)法讓主從庫對于寫操作協(xié)同甸昏,這會帶來巨額的開銷。
主從庫間如何進(jìn)行第一次同步的确虱?
當(dāng)我們啟動多個 Redis 實例的時候,它們相互之間就可以通過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關(guān)系替裆,之后會按照三個階段完成數(shù)據(jù)的第一次同步校辩。
主庫收到 psync 命令后,會用 FULLRESYNC 響應(yīng)命令帶上兩個參數(shù):主庫 runID 和主庫目前的復(fù)制進(jìn)度 offset辆童,返回給從庫宜咒。從庫收到響應(yīng)后,會記錄下這兩個參數(shù)把鉴。
這里有個地方需要注意荧呐,F(xiàn)ULLRESYNC 響應(yīng)表示第一次復(fù)制采用的全量復(fù)制,也就是說纸镊,主庫會把當(dāng)前所有的數(shù)據(jù)都復(fù)制給從庫。
在第二階段概疆,主庫將所有數(shù)據(jù)同步給從庫逗威。從庫收到數(shù)據(jù)后,在本地完成數(shù)據(jù)加載岔冀。這個過程依賴于內(nèi)存快照生成的 RDB 文件凯旭。
具體來說,主庫執(zhí)行 bgsave 命令使套,生成 RDB 文件罐呼,接著將文件發(fā)給從庫。從庫接收到 RDB 文件后侦高,會先清空當(dāng)前數(shù)據(jù)庫嫉柴,然后加載 RDB 文件。這是因為從庫在通過 replicaof 命令開始和主庫同步前奉呛,可能保存了其他數(shù)據(jù)计螺。為了避免之前數(shù)據(jù)的影響夯尽,從庫需要先把當(dāng)前數(shù)據(jù)庫清空。
在主庫將數(shù)據(jù)同步給從庫的過程中登馒,主庫不會被阻塞匙握,仍然可以正常接收請求。否則陈轿,Redis 的服務(wù)就被中斷了圈纺。但是,這些請求中的寫操作并沒有記錄到剛剛生成的 RDB 文件中麦射。為了保證主從庫的數(shù)據(jù)一致性蛾娶,主庫會在內(nèi)存中用專門的 replication buffer,記錄 RDB 文件生成后收到的所有寫操作法褥。
最后茫叭,也就是第三個階段,主庫會把第二階段執(zhí)行過程中新收到的寫命令半等,再發(fā)送給從庫揍愁。具體的操作是,當(dāng)主庫完成 RDB 文件發(fā)送后杀饵,就會把此時 replication buffer 中的修改操作發(fā)給從庫莽囤,從庫再重新執(zhí)行這些操作。這樣一來切距,主從庫就實現(xiàn)同步了朽缎。
3.6 Redis如何保證高可用的
3.6.1 主庫掛了之后,還能接收寫操作嗎?
Redis在有了主從集群后,如果從庫掛了谜悟,Redis對外提供服務(wù)不受影響话肖,主庫和其他從庫,依然可以提供讀寫服務(wù)葡幸,但是當(dāng)主庫掛了之后最筒,因為是讀寫分離的,如果此時有寫的請求蔚叨,那么就無法處理了床蜘。Redis是如果解決這樣的問題的呢,這就要引入哨兵機(jī)制了蔑水。
當(dāng)主庫掛了邢锯,我們需要從從庫中選出一個當(dāng)做主庫,這樣就可以正常對外提供服務(wù)了搀别。哨兵的本質(zhì)就是一個Redis示例丹擎,只不過它是運行在特殊模式下的Redis進(jìn)程。它主要有三個作用:監(jiān)控领曼、選舉鸥鹉、通知蛮穿。
哨兵在監(jiān)控到主庫下線的時候,會從從庫中通過一定的規(guī)則毁渗,選舉出適合的從庫當(dāng)主庫践磅,并通知其他從庫變更主庫的信息,讓他們執(zhí)行replicaof命令灸异,和新主庫建立連接府适,并進(jìn)行數(shù)據(jù)復(fù)制。那么具體每一步都是怎么做的呢肺樟?
監(jiān)控:哨兵會周期性向主從庫發(fā)送PING命令檐春,檢測主庫是否正常運行,如果主從庫沒有在規(guī)定的時間內(nèi)回應(yīng)哨兵的PING命令么伯,則會被判定為“下線狀態(tài)”疟暖,如果是主庫下線,則開始自動切換主庫的流程田柔。但是一般如果只有一個哨兵俐巴,那么它的判斷可能不具有可靠性,所以一般哨兵都是采用集群模式部署硬爆,稱為哨兵集群欣舵。單多個哨兵均判斷該主庫下線了,那么可能他就真的下線了缀磕,這是一個少數(shù)服從多數(shù)的規(guī)則缘圈。
選舉: 哨兵選擇新主庫的過程稱為“篩選 + 打分”。簡單來說袜蚕,我們在多個從庫中糟把,先按照一定的篩選條件,把不符合條件的從庫去掉牲剃。然后糊饱,我們再按照一定的規(guī)則,給剩下的從庫逐個打分颠黎,將得分最高的從庫選為新主庫,如下圖所示:
1滞项、排除那些已經(jīng)下線的從庫狭归,以及連接不穩(wěn)定的從庫。連接不穩(wěn)定是通過配置項down-after-milliseconds文判,當(dāng)主從連接超時達(dá)到一定閾值过椎,就會被記錄下來,比如設(shè)置的10次戏仓,那么就會標(biāo)記該從庫網(wǎng)絡(luò)不好疚宇,不適合做為主庫亡鼠。
2、篩選出從庫后敷待,第二部就要開始打分了间涵,主要從三方面打分,
1.從庫優(yōu)先級榜揖,這是可以通過slave-property設(shè)置的勾哩,設(shè)置的高,打分的就高举哟,就會被選為主庫思劳,比如你可以給從庫中內(nèi)存帶寬資源充足設(shè)置高優(yōu)先級,當(dāng)主庫掛了之后被優(yōu)先選舉為主庫妨猩。
2.從庫與舊主庫之間的復(fù)制進(jìn)度潜叛,之前我們知道主從之間增量復(fù)制,有個參數(shù)slave-repl-offset記錄當(dāng)前的復(fù)制進(jìn)度壶硅。這個數(shù)值越大威兜,說明與主庫復(fù)制進(jìn)度越靠近,打分也會越高森瘪。
3.每個從庫創(chuàng)建實例的時候牡属,會隨機(jī)生成一個id,id越小的得分越高扼睬。
通知:哨兵提升一個從庫為新主庫后逮栅,哨兵會把新主庫的地址寫入自己實例的pubsub(switch-master)中〈坝睿客戶端需要訂閱這個pubsub措伐,當(dāng)這個pubsub有數(shù)據(jù)時,客戶端就能感知到主庫發(fā)生變更军俊,同時可以拿到最新的主庫地址侥加,然后把寫請求寫到這個新主庫即可,這種機(jī)制屬于哨兵主動通知客戶端粪躬。
如果客戶端因為某些原因錯過了哨兵的通知担败,或者哨兵通知后客戶端處理失敗了,安全起見镰官,客戶端也需要支持主動去獲取最新主從的地址進(jìn)行訪問提前。
所以,客戶端需要訪問主從庫時泳唠,不能直接寫死主從庫的地址了狈网,而是需要從哨兵集群中獲取最新的地址(sentinel get-master-addr-by-name命令),這樣當(dāng)實例異常時,哨兵切換后或者客戶端斷開重連拓哺,都可以從哨兵集群中拿到最新的實例地址勇垛。
3.6.2 哨兵集群
部署哨兵集群的時候,我們知道只需要配置:sentinel monitor 跟主庫通信就可以了士鸥,并不知道其他哨兵的信息闲孤,那么是如何知道的呢?
Redis有提供了pub/sub機(jī)制础淤,哨兵跟主庫建立了連接之后崭放,將自己的信息發(fā)布到 “sentinel:hello”頻道上,其他哨兵發(fā)布并訂閱了該頻道鸽凶,就可以獲取其他哨兵的信息币砂,那么哨兵之間就可以相互通信了决摧。
那么哨兵如何知道從庫的連接信息呢掌桩,那是因為INFO命令,哨兵向主庫發(fā)送該命令后姑食,獲得了所有從庫的連接信息波岛,就能分從庫建立連接,并進(jìn)行監(jiān)控了音半。
從本質(zhì)上說曹鸠,哨兵就是一個運行在特定模式下的 Redis 實例煌茬,只不過它并不服務(wù)請求操作,只是完成監(jiān)控邻眷、選主和通知的任務(wù)眠屎。所以,每個哨兵實例也提供 pub/sub 機(jī)制肆饶,客戶端可以從哨兵訂閱消息组力。哨兵提供的消息訂閱頻道有很多,不同頻道包含了主從庫切換過程中的不同關(guān)鍵事件抖拴。
3.6.3 切片集群
與mysql一樣,當(dāng)一張表的數(shù)據(jù)很大時,查詢耗時可能就會越來越大阿宅,我們采取的措施是分表分庫候衍。同樣的Redis也樣,當(dāng)數(shù)據(jù)量很大時洒放,比如高達(dá)25G蛉鹿,在單分片下,我們需要機(jī)器有32G的內(nèi)存往湿。但是我們會發(fā)現(xiàn)妖异,有時候redis響應(yīng)會變得很慢,通過INFO查詢Redis的latest_fork_usec指標(biāo),最近fork耗時,發(fā)現(xiàn)耗時很大,快到秒級別了,fork這個動作會阻塞主線程领追,于是就導(dǎo)致了Redis變慢了他膳。
于是就有redis分片集群, 啟動多個 Redis 實例組成一個集群绒窑,然后按照一定的規(guī)則棕孙,把收到的數(shù)據(jù)劃分成多份,每一份用一個實例來保存些膨◇翱。回到我們剛剛的場景中,如果把 25GB 的數(shù)據(jù)平均分成 5 份(當(dāng)然订雾,也可以不做均分)肢预,使用 5 個實例來保存,每個實例只需要保存 5GB 數(shù)據(jù)洼哎。
那么烫映,在切片集群中,實例在為 5GB 數(shù)據(jù)生成 RDB 時谱净,數(shù)據(jù)量就小了很多窑邦,fork 子進(jìn)程一般不會給主線程帶來較長時間的阻塞。采用多個實例保存數(shù)據(jù)切片后壕探,我們既能保存 25GB 數(shù)據(jù)冈钦,又避免了 fork 子進(jìn)程阻塞主線程而導(dǎo)致的響應(yīng)突然變慢。
那么數(shù)據(jù)是如何決定存在在哪個分片上的呢李请?
Redis Cluster 方案采用哈希槽(Hash Slot瞧筛,接下來我會直接稱之為 Slot),來處理數(shù)據(jù)和實例之間的映射關(guān)系导盅。在 Redis Cluster 方案中较幌,一個切片集群共有 16384 個哈希槽,這些哈希槽類似于數(shù)據(jù)分區(qū)白翻,每個鍵值對都會根據(jù)它的 key乍炉,被映射到一個哈希槽中绢片。具體的映射過程分為兩大步:首先根據(jù)鍵值對的 key,按照CRC16 算法計算一個 16 bit 的值岛琼;然后底循,再用這個 16bit 值對 16384 取模,得到 0~16383 范圍內(nèi)的模數(shù)槐瑞,每個模數(shù)代表一個相應(yīng)編號的哈希槽熙涤。
我們在部署 Redis Cluster 方案時,可以使用 cluster create 命令創(chuàng)建集群困檩,此時祠挫,Redis 會自動把這些槽平均分布在集群實例上。例如悼沿,如果集群中有 N 個實例等舔,那么,每個實例上的槽個數(shù)為 16384/N 個显沈。 也可以使用 cluster meet 命令手動建立實例間的連接软瞎,形成集群,再使用 cluster addslots 命令拉讯,指定每個實例上的哈希槽個數(shù)涤浇。
前面介紹了Redis相關(guān)知識,了解了Redis的高可用魔慷,高性能的原因只锭。很多人認(rèn)為提到緩存,就局限于Redis院尔,其實緩存的應(yīng)用不僅僅在于Redis的使用蜻展,比如還有Nginx緩存,緩存隊列等等邀摆。下面我們會將講解Nginx+Lua實現(xiàn)多級緩存方法纵顾,來解決高并發(fā)訪問的場景。
04 緩存的應(yīng)用
我們來看一張微服務(wù)架構(gòu)緩存的使用
我們可以看到微服務(wù)架構(gòu)中栋盹,會大量使用到緩存
1.客戶端緩存(手機(jī)施逾、PC) 2.Nginx緩存 3.微服務(wù)網(wǎng)關(guān)限流令牌緩存 4.Nacos緩存服務(wù)列表、配置文件 5.各大微服務(wù)自身也具有緩存 6.數(shù)據(jù)庫查詢Query Cache 7.Redis集群緩存 8.Kafka也屬于緩存
應(yīng)對高并發(fā)的最有效手段之一就是分布式緩存例获,分布式緩存不僅僅是緩存要顯示的數(shù)據(jù)這么簡單汉额,還可以在限流、隊列削峰榨汤、高速讀寫蠕搜、分布式鎖等場景發(fā)揮重大作用。分布式緩存可以說是解決高并發(fā)場景的有效利器收壕。以以下場景為例:
1妓灌、凌晨突然涌入的巨大流量轨蛤。【隊列術(shù)】【限流術(shù)】
2虫埂、高并發(fā)場景秒殺俱萍、搶紅包、搶優(yōu)惠券告丢,快速存取∷鹎【緩存取代MySQL操作】
3岖免、高并發(fā)場景超賣、超額搶紅包照捡÷妫【Redis單線程取代數(shù)據(jù)庫操作】
4、高并發(fā)場景重復(fù)搶單栗精〈巢危【Redis搶單計數(shù)器】
一談到緩存架構(gòu),很多人想到的是Redis悲立,但其實整套體系的緩存架構(gòu)并非只有Redis鹿寨,而應(yīng)該是多個層面多個軟 件結(jié)合形成一套非常良性的緩存體系。比如咱們的緩存架構(gòu)設(shè)計就涉及到了多個層面的緩存軟件薪夕。
1脚草、HTML頁面做緩存,瀏覽器端可以緩存HTML頁面和其他靜態(tài)資源原献,防止用戶頻繁刷新對后端造成巨大壓力
2馏慨、Lvs實現(xiàn)記錄不同協(xié)議以及不同用戶請求鏈路緩存
3、Nginx這里會做HTML頁面緩存配置以及Nginx自身緩存配置
4姑隅、數(shù)據(jù)查找這里用Lua取代了其他語言查找写隶,提高了處理的性能效率,并發(fā)處理能力將大大提升
5讲仰、數(shù)據(jù)緩存采用了Redis集群+主從架構(gòu)慕趴,并實現(xiàn)緩存讀寫分離操作
6、集成Canal實現(xiàn)數(shù)據(jù)庫數(shù)據(jù)增量實時同步Redis
05 Nginx緩存
5.1 瀏覽器緩存
客戶端側(cè)緩存一般指的是瀏覽器緩存叮盘、app緩存等等秩贰,目的就是加速各種靜態(tài)資源的訪問,降低服務(wù)器壓力柔吼。我們通過配置Nginx設(shè)置網(wǎng)頁緩存信息毒费,從而降低用戶對服務(wù)器頻繁訪問造成的巨大壓力。
HTTP 中最基本的緩存機(jī)制愈魏,涉及到的 HTTP 頭字段觅玻,包括 Cache‐Control, Last‐Modified, If‐Modified‐Since,
Etag,If‐None‐Match 等想际。
Last‐Modified/If‐Modified‐Since
Etag是服務(wù)端的一個資源的標(biāo)識,在 HTTP 響應(yīng)頭中將其傳送到客戶端溪厘。所謂的服務(wù)端資源可以是一個Web頁面胡本,也可
以是JSON或XML等。服務(wù)器單獨負(fù)責(zé)判斷記號是什么及其含義畸悬,并在HTTP響應(yīng)頭中將其傳送到客戶端侧甫。比如,瀏覽器第
一次請求一個資源的時候蹋宦,服務(wù)端給予返回披粟,并且返回了ETag: "50b1c1d4f775c61:df3" 這樣的字樣給瀏覽器,當(dāng)瀏
覽器再次請求這個資源的時候冷冗,瀏覽器會將If‐None‐Match: W/"50b1c1d4f775c61:df3" 傳輸給服務(wù)端守屉,服務(wù)端拿到
該ETAG,對比資源是否發(fā)生變化蒿辙,如果資源未發(fā)生改變拇泛,則返回304HTTP狀態(tài)碼,不返回具體的資源思灌。
Last‐Modified :標(biāo)示這個響應(yīng)資源的最后修改時間俺叭。web服務(wù)器在響應(yīng)請求時,告訴瀏覽器資源的最后修改時間习瑰。
If‐Modified‐Since :當(dāng)資源過期時(使用Cache‐Control標(biāo)識的max‐age)绪颖,發(fā)現(xiàn)資源具有 Last‐Modified 聲
明,則再次向web服務(wù)器請求時帶上頭甜奄。
If‐Modified‐Since 柠横,表示請求時間。web服務(wù)器收到請求后發(fā)現(xiàn)有頭 If‐Modified‐Since 則與被請求資源的最后修
改時間進(jìn)行比對课兄。若最后修改時間較新牍氛,說明資源有被改動過,則響應(yīng)整片資源內(nèi)容(寫在響應(yīng)消息包體內(nèi))烟阐,HTTP
200搬俊;若最后修改時間較舊,說明資源無新修改蜒茄,則響應(yīng) HTTP 304 (無需包體唉擂,節(jié)省瀏覽),告知瀏覽器繼續(xù)使用所保
存的 cache 檀葛。
Pragma行是為了兼容 HTTP1.0 玩祟,作用與 Cache‐Control: no‐cache 是一樣的
Etag/If‐None‐Match
Etag :web服務(wù)器響應(yīng)請求時,告訴瀏覽器當(dāng)前資源在服務(wù)器的唯一標(biāo)識(生成規(guī)則由服務(wù)器決定),如果給定URL中的
資源修改屿聋,則一定要生成新的Etag值空扎。
If‐None‐Match :當(dāng)資源過期時(使用Cache‐Control標(biāo)識的max‐age)藏鹊,發(fā)現(xiàn)資源具有Etage聲明,則再次向web服
務(wù)器請求時帶上頭 If‐None‐Match (Etag的值)转锈。web服務(wù)器收到請求后發(fā)現(xiàn)有頭 If‐None‐Match 則與被請求資源
的相應(yīng)校驗串進(jìn)行比對盘寡,決定返回200或304。
Etag:
Last‐Modified 標(biāo)注的最后修改只能精確到秒級撮慨,如果某些文件在1秒鐘以內(nèi)竿痰,被修改多次的話,它將不能準(zhǔn)確標(biāo)注文
件的修改時間砌溺,如果某些文件會被定期生成菇曲,當(dāng)有時內(nèi)容并沒有任何變化,但 Last‐Modified 卻改變了抚吠,導(dǎo)致文件沒
法使用緩存有可能存在服務(wù)器沒有準(zhǔn)確獲取文件修改時間,或者與代理服務(wù)器時間不一致等情形 Etag是服務(wù)器自動生成
或者由開發(fā)者生成的對應(yīng)資源在服務(wù)器端的唯一標(biāo)識符弟胀,能夠更加準(zhǔn)確的控制緩存楷力。 Last‐Modified 與 ETag 是可以
一起使用的,服務(wù)器會優(yōu)先驗證 ETag 孵户,一致的情況下萧朝,才會繼續(xù)比對 Last‐Modified ,最后才決定是否返回304夏哭。
5.2 代理緩存
用戶如果請求獲取的數(shù)據(jù)不是需要后端服務(wù)器處理返回检柬,如果我們需要對數(shù)據(jù)做緩存來提高服務(wù)器的處理能力,我們可以按照如下步驟實現(xiàn):
1竖配、請求Nginx何址,Nginx將請求路由給后端服務(wù)
2、后端服務(wù)查詢Redis或者M(jìn)ySQL进胯,再將返回結(jié)果給Nginx
3用爪、Nginx將結(jié)果存入到Nginx緩存,并將結(jié)果返回給用戶
4胁镐、用戶下次執(zhí)行同樣請求偎血,直接在Nginx中獲取緩存數(shù)據(jù)
06 多級緩存架構(gòu)
具體流程
1、用戶請求經(jīng)過Nginx
2盯漂、Nginx檢查是否有緩存颇玷,如果Nginx有緩存,直接響應(yīng)用戶數(shù)據(jù)
3就缆、Nginx如果沒有緩存帖渠,則將請求路由給后端Java服務(wù)
4、Java服務(wù)查詢Redis緩存违崇,如果有數(shù)據(jù)阿弃,則將數(shù)據(jù)直接響應(yīng)給Nginx诊霹,并將數(shù)據(jù)存入緩存,Nginx將數(shù)據(jù)響應(yīng)給用戶
5渣淳、如果Redis沒有緩存脾还,則使用Java程序查詢MySQL,并將數(shù)據(jù)存入到Reids入愧,再將數(shù)據(jù)存入到Nginx中
優(yōu)缺點
優(yōu)點:
1鄙漏、采用了Nginx緩存,減少了數(shù)據(jù)加載的路徑棺蛛,從而提升站點數(shù)據(jù)加載效率
2怔蚌、多級緩存有效防止了緩存擊穿、緩存穿透問題
缺點
Tomcat并發(fā)量偏低旁赊,導(dǎo)致緩存同步并發(fā)量失衡桦踊,緩存首次加載效率偏低,Tomcat 大規(guī)模集群占用資源高
優(yōu)點
1终畅、采用了Nginx緩存籍胯,減少了數(shù)據(jù)加載的路徑,從而提升站點數(shù)據(jù)加載效率
2离福、多級緩存有效防止了緩存擊穿杖狼、緩存穿透問題
3、使用了Nginx+Lua集成妖爷,無論是哪次緩存加載蝶涩,效率都高
4、Nginx并發(fā)量高絮识,Nginx+Lua集成绿聘,大幅提升了并發(fā)能力
6.1 搶紅包案例架構(gòu)設(shè)計分享
上面我們已經(jīng)分析過紅包雨的特點,要想實現(xiàn)一套高效的紅包雨系統(tǒng)次舌,緩存架構(gòu)是關(guān)鍵斜友。我們根據(jù)紅包雨的特點設(shè)計了如上圖所示的紅包雨緩存架構(gòu)體系。
1垃它、紅包雨分批次導(dǎo)入到Redis緩存而不要每次操作數(shù)據(jù)庫
2鲜屏、很多用戶搶紅包的時候,為了避免1個紅包被多人搶到国拇,我們要采用Redis的隊列存儲紅包
3洛史、追加紅包的時候,可以追加延時發(fā)放紅包酱吝,也可以直接追加立即發(fā)放紅包
4也殖、用戶搶購紅包的時候,會先經(jīng)過Nginx,通過Lua腳本查看緩存中是否存在紅包忆嗜,如果不存在紅包己儒,則直接終止搶紅包
5、如果還存在紅包捆毫,為了避免后臺同時處理很多請求闪湾,這里采用隊列術(shù)緩存用戶請求,后端通過消費隊列執(zhí)行搶紅包
6.2 緩存隊列使用場景
1绩卤、隊列控制并發(fā)溢出:并發(fā)量非常大的系統(tǒng)途样,例如秒殺、搶紅包濒憋、搶票等操作何暇,都是存在溢出現(xiàn)象,比如秒殺超賣凛驮、搶紅包超額裆站、一票多單等溢出現(xiàn)象,如果采用數(shù)據(jù)庫鎖來控制溢出問題黔夭,效率非常低遏插,在高并發(fā)場景下,很有可能直接導(dǎo)致數(shù)據(jù)庫崩潰纠修,因此針對高并發(fā)場景下數(shù)據(jù)溢出解決方案我們可以采用Redis緩存提升效率。
2厂僧、隊列限流:解決大量并發(fā)用戶蜂擁而上的方法可以采用隊列術(shù)將用戶的請求用隊列緩存起來扣草,后端服務(wù)從隊列緩存中有序消費,可以防止后端服務(wù)同時面臨處理大量請求颜屠。緩存用戶請求可以用RabbitMQ、Kafka、RocketMQ巩割、ActiveMQ等种冬。用戶搶紅包的時候,我們用Lua腳本實現(xiàn)將用戶搶紅包的信息以生產(chǎn)者角色將消息發(fā)給RabbitMQ粗井,后端應(yīng)用服務(wù)以消費者身份從RabbitMQ獲取消息并搶紅包尔破,再將搶紅包信息以WebSocket方式通知給用戶。
6.3 Nginx限流
nginx提供兩種限流的方式:一是控制速率浇衬,二是控制并發(fā)連接數(shù)懒构。
1、速率限流
控制速率的方式之一就是采用漏桶算法耘擂。具體配置如下:
2胆剧、控制并發(fā)量
ngx_http_limit_conn_module 提供了限制連接數(shù)的能力。主要是利用limit_conn_zone和limit_conn兩個指令醉冤。利用連接數(shù)限制 某一個用戶的ip連接的數(shù)量來控制流量秩霍。
(1)配置限制固定連接數(shù) 如下篙悯,配置如下: 配置限流緩存空間:
根據(jù)IP地址來限制,存儲內(nèi)存大小10M
limit_conn_zone $binary_remote_addr zone=addr:1m;
location配置:
limit_conn addr 2;
參數(shù)說明:
limit_conn_zone $binary_remote_addr zone=addr:10m; 表示限制根據(jù)用戶的IP地址來顯示铃绒,設(shè)置存儲地址為的
內(nèi)存大小10M
limit_conn addr 2; 表示 同一個地址只允許連接2次鸽照。
(2)限制每個客戶端IP與服務(wù)器的連接數(shù),同時限制與虛擬服務(wù)器的連接總數(shù)匿垄。 限流緩存空間配置:
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
location配置
limit_conn perip 10;#單個客戶端ip與服務(wù)器的連接數(shù)
limit_conn perserver 100; #限制與服務(wù)器的總連接數(shù)
每個IP限流 3個 總量5個
07 緩存災(zāi)難問題如何解決
7.1 緩存穿透
產(chǎn)生原因
當(dāng)我們查詢一個緩存不存在的數(shù)據(jù)移宅,就去查數(shù)據(jù)庫,但此時如果數(shù)據(jù)庫也沒有這個數(shù)據(jù)椿疗,后面繼續(xù)訪問依然會再次查詢數(shù)據(jù)庫漏峰,當(dāng)有用戶大量請求不存在的數(shù)據(jù),必然會導(dǎo)致數(shù)據(jù)庫的壓力升高届榄,甚至崩潰浅乔。
如何解決
1、當(dāng)查詢到不存在的數(shù)據(jù)铝条,也將對應(yīng)的key放入緩存靖苇,值為nul,這樣再次查詢會直接返回null班缰,如果后面新增了該key的數(shù)據(jù)贤壁,就覆蓋即可。
2埠忘、使用布隆過濾器脾拆。布隆過濾器主要是解決大規(guī)模數(shù)據(jù)下不需要精確過濾的業(yè)務(wù)場景,如檢查垃圾郵件地址莹妒,爬蟲URL地址去重名船,解決緩存穿透問題等。
7.2 緩存擊穿
產(chǎn)生原因
當(dāng)緩存在某一刻過期了旨怠,一般如果再查詢這個緩存渠驼,會從數(shù)據(jù)庫去查詢一次再放到緩存,如果正好這一刻鉴腻,大量的請求該緩存迷扇,那么請求都會打到數(shù)據(jù)庫中,可能導(dǎo)致數(shù)據(jù)庫打垮爽哎。
如何解決
1谋梭、盡量避免緩存過期時間都在同一時間。
2倦青、定時任務(wù)主動刷新更新緩存瓮床,或者設(shè)置緩存不過去,適合那種key相對固定,粒度較大的業(yè)務(wù)。
分享下我在公司的負(fù)責(zé)的系統(tǒng)是如何防止緩存擊穿的隘庄,由于業(yè)務(wù)場景踢步,緩存的數(shù)據(jù)都是當(dāng)天有效的,當(dāng)天查詢的只查當(dāng)日有效的數(shù)據(jù)丑掺,所以當(dāng)時數(shù)據(jù)都是設(shè)置當(dāng)天凌晨過期获印,并且緩存是懶加載,這樣導(dǎo)致0點高峰期數(shù)據(jù)庫壓力明顯增大街州。后來改造了下兼丰,做了個定時任務(wù),每天凌晨3點唆缴,跑第二天生效的數(shù)據(jù)鳍征,并且設(shè)置失效時間延長一天。有效解決了該問題面徽,相當(dāng)于緩存預(yù)熱艳丛。
3、多級緩存
采用多級緩存也可以有效防止擊穿現(xiàn)象趟紊,首先通過程序?qū)⒕彺娲嫒氲絉edis緩存氮双,且永不過期,用戶查詢的時候霎匈,先查詢Nginx緩存戴差,如果Nginx緩存沒有,則查詢Redis緩存铛嘱,并將Redis緩存存入到Nginx一級緩存中暖释,并設(shè)置更新時間。這種方案不僅可以提升查詢速度弄痹,同時又能防止擊穿問題,并且提升了程序的抗壓能力嵌器。
4肛真、分布式鎖與隊列。解決思路主要是防止多請求同時打過去爽航。分布式鎖蚓让,推薦使用Redisson。隊列方案可以使用nginx緩存隊列讥珍,配置如下历极。
7.3 緩存雪崩
產(chǎn)生原因
緩存雪崩是指,由于緩存層承載著大量請求衷佃,有效的保護(hù)了存儲層趟卸,但是如果緩存層由于某些原因整體不能提供服 務(wù),于是所有的請求都會達(dá)到存儲層,存儲層的調(diào)用量會暴增锄列,造成存儲層也會掛掉的情況图云。
如何解決
1、做緩存集群邻邮。即使個別節(jié)點竣况、個別機(jī)器、甚至是機(jī)房宕掉筒严,依然可以提供服務(wù)丹泉,比如 Redis Sentinel 和 Redis Cluster 都實現(xiàn)了高可用。
2鸭蛙、做好限流摹恨。微服務(wù)網(wǎng)關(guān)或者Nginx做好限流操作,防止大量請求直接進(jìn)入后端规惰,使后端載荷過重最后宕機(jī)睬塌。
3、緩存預(yù)熱歇万。預(yù)先去更新緩存揩晴,再即將發(fā)生大并發(fā)訪問前手動觸發(fā)加載緩存不同的key,設(shè)置不同的過期時間贪磺,讓緩存失效的時間點盡量均勻硫兰,不要同時失效。
4寒锚、加鎖劫映。數(shù)據(jù)操作,如果是帶有緩存查詢的刹前,均使用分布式鎖泳赋,防止大量請求直接操作數(shù)據(jù)庫。
5喇喉、多級緩存祖今。采用多級緩存,Nginx+Redis+MyBatis二級緩存拣技,當(dāng)Nginx緩存失效時千诬,查找Redis緩存,Redis緩存失效查找MyBatis二級緩存膏斤。
7.4 緩存一致性
問題描述
數(shù)據(jù)的在增量數(shù)據(jù)徐绑,未同步到緩存。導(dǎo)致緩存與數(shù)據(jù)庫數(shù)據(jù)不一致莫辨。
解決方案Canal
用戶每次操作數(shù)據(jù)庫的時候傲茄,使用Canal監(jiān)聽數(shù)據(jù)庫指定表的增量變化毅访,在Java程序中消費Canal監(jiān)聽到的增量變化,并在Java程序中實現(xiàn)對Redis緩存或者Nginx緩存的更新烫幕。 用戶查詢的時候俺抽,先通過Lua查詢Nginx的緩存,如果Nginx緩存沒有數(shù)據(jù)较曼,則查詢Redis緩存磷斧,Redis緩存如果也沒有數(shù)據(jù),可以去數(shù)據(jù)庫查詢捷犹。