硬核 Redis 高頻面試題解析
1、Redis 是單線程還是多線程?
這個(gè)問(wèn)題應(yīng)該已經(jīng)看到過(guò)無(wú)數(shù)次了,最近 redis 6 出來(lái)之后又被翻出來(lái)了。
redis 4.0 之前,redis 是完全單線程的。
redis 4.0 時(shí),redis 引入了多線程,但是額外的線程只是用于后臺(tái)處理,例如:刪除對(duì)象,核心流程還是完全單線程的。這也是為什么有些人說(shuō) 4.0 是單線程的,因?yàn)樗麄冎傅氖呛诵牧鞒淌菃尉€程的。
這邊的核心流程指的是 redis 正常處理客戶(hù)端請(qǐng)求的流程,通常包括:接收命令、解析命令、執(zhí)行命令、返回結(jié)果等。
而在最近,redis 6.0 版本又一次引入了多線程概念,與 4.0 不同的是,這次的多線程會(huì)涉及到上述的核心流程。
redis 6.0 中,多線程主要用于網(wǎng)絡(luò) I/O 階段,也就是接收命令和寫(xiě)回結(jié)果階段,而在執(zhí)行命令階段,還是由單線程串行執(zhí)行。由于執(zhí)行時(shí)還是串行,因此無(wú)需考慮并發(fā)安全問(wèn)題。
值得注意的時(shí),redis 中的多線程組不會(huì)同時(shí)存在“讀”和“寫(xiě)”,這個(gè)多線程組只會(huì)同時(shí)“讀”或者同時(shí)“寫(xiě)”。
redis 6.0 加入多線程 I/O 之后,處理命令的核心流程如下:
1、當(dāng)有讀事件到來(lái)時(shí),主線程將該客戶(hù)端連接放到全局等待讀隊(duì)列
2、讀取數(shù)據(jù):1)主線程將等待讀隊(duì)列的客戶(hù)端連接通過(guò)輪詢(xún)調(diào)度算法分配給 I/O 線程處理;2)同時(shí)主線程也會(huì)自己負(fù)責(zé)處理一個(gè)客戶(hù)端連接的讀事件;3)當(dāng)主線程處理完該連接的讀事件后,會(huì)自旋等待所有 I/O 線程處理完畢
3、命令執(zhí)行:主線程按照事件被加入全局等待讀隊(duì)列的順序(這邊保證了執(zhí)行順序是正確的),串行執(zhí)行客戶(hù)端命令,然后將客戶(hù)端連接放到全局等待寫(xiě)隊(duì)列
4、寫(xiě)回結(jié)果:跟等待讀隊(duì)列處理類(lèi)似,主線程將等待寫(xiě)隊(duì)列的客戶(hù)端連接使用輪詢(xún)調(diào)度算法分配給 I/O 線程處理,同時(shí)自己也會(huì)處理一個(gè),當(dāng)主線程處理完畢后,會(huì)自旋等待所有 I/O 線程處理完畢,最后清空隊(duì)列。
大致流程圖如下:
2、為什么 Redis 是單線程?
在 redis 6.0 之前,redis 的核心操作是單線程的。
因?yàn)?redis 是完全基于內(nèi)存操作的,通常情況下CPU不會(huì)是redis的瓶頸,redis 的瓶頸最有可能是機(jī)器內(nèi)存的大小或者網(wǎng)絡(luò)帶寬。
既然CPU不會(huì)成為瓶頸,那就順理成章地采用單線程的方案了,因?yàn)槿绻褂枚嗑€程的話會(huì)更復(fù)雜,同時(shí)需要引入上下文切換、加鎖等等,會(huì)帶來(lái)額外的性能消耗。
而隨著近些年互聯(lián)網(wǎng)的不斷發(fā)展,大家對(duì)于緩存的性能要求也越來(lái)越高了,因此 redis 也開(kāi)始在逐漸往多線程方向發(fā)展。
最近的 6.0 版本就對(duì)核心流程引入了多線程,主要用于解決 redis 在網(wǎng)絡(luò) I/O 上的性能瓶頸。而對(duì)于核心的命令執(zhí)行階段,目前還是單線程的。
3、Redis 為什么使用單進(jìn)程、單線程也很快
主要有以下幾點(diǎn):
1、基于內(nèi)存的操作
2、使用了 I/O 多路復(fù)用模型,select、epoll 等,基于 reactor 模式開(kāi)發(fā)了自己的網(wǎng)絡(luò)事件處理器
3、單線程可以避免不必要的上下文切換和競(jìng)爭(zhēng)條件,減少了這方面的性能消耗。
4、以上這三點(diǎn)是 redis 性能高的主要原因,其他的還有一些小優(yōu)化,例如:對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行了優(yōu)化,簡(jiǎn)單動(dòng)態(tài)字符串、壓縮列表等。
4、Redis 在項(xiàng)目中的使用場(chǎng)景
緩存(核心)、分布式鎖(set + lua 腳本)、排行榜(zset)、計(jì)數(shù)(incrby)、消息隊(duì)列(stream)、地理位置(geo)、訪客統(tǒng)計(jì)(hyperloglog)等。
5、Redis 常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)
基礎(chǔ)的5種:
- String:字符串,最基礎(chǔ)的數(shù)據(jù)類(lèi)型。
- List:列表。
- Hash:哈希對(duì)象。
- Set:集合。
- Sorted Set:有序集合,Set 的基礎(chǔ)上加了個(gè)分值。
高級(jí)的4種:
- HyperLogLog:通常用于基數(shù)統(tǒng)計(jì)。使用少量固定大小的內(nèi)存,來(lái)統(tǒng)計(jì)集合中唯一元素的數(shù)量。統(tǒng)計(jì)結(jié)果不是精確值,而是一個(gè)帶有0.81%標(biāo)準(zhǔn)差(standard error)的近似值。所以,HyperLogLog適用于一些對(duì)于統(tǒng)計(jì)結(jié)果精確度要求不是特別高的場(chǎng)景,例如網(wǎng)站的UV統(tǒng)計(jì)。
- Geo:redis 3.2 版本的新特性??梢詫⒂脩?hù)給定的地理位置信息儲(chǔ)存起來(lái), 并對(duì)這些信息進(jìn)行操作:獲取2個(gè)位置的距離、根據(jù)給定地理位置坐標(biāo)獲取指定范圍內(nèi)的地理位置集合。
- Bitmap:位圖。
- Stream:主要用于消息隊(duì)列,類(lèi)似于 kafka,可以認(rèn)為是 pub/sub 的改進(jìn)版。提供了消息的持久化和主備復(fù)制功能,可以讓任何客戶(hù)端訪問(wèn)任何時(shí)刻的數(shù)據(jù),并且能記住每一個(gè)客戶(hù)端的訪問(wèn)位置,還能保證消息不丟失。
6、Redis 的字符串(SDS)和C語(yǔ)言的字符串區(qū)別
C字符串 |
SDS |
---|---|
獲取字符串長(zhǎng)度的復(fù)雜度為O(N) |
獲取字符串長(zhǎng)度的復(fù)雜度為O(1) |
API是不安全的,可能會(huì)造成緩沖區(qū)溢出 |
API是安全的,不會(huì)造成緩沖區(qū)溢出 |
修改字符串長(zhǎng)度N次必然需要執(zhí)行N次內(nèi)存重分配 |
修改字符串長(zhǎng)度N次最多需要執(zhí)行N次內(nèi)存重分配 |
只能保存文本數(shù)據(jù) |
可以保存文本數(shù)據(jù)或者二進(jìn)制數(shù)據(jù) |
可以使用所有的<string.h>庫(kù)中的函數(shù) |
可以使用一部分<string.h>庫(kù)中的函數(shù) |
7、Sorted Set底層數(shù)據(jù)結(jié)構(gòu)
Sorted Set(有序集合)當(dāng)前有兩種編碼:ziplist、skiplist
ziplist:使用壓縮列表實(shí)現(xiàn),當(dāng)保存的元素長(zhǎng)度都小于64字節(jié),同時(shí)數(shù)量小于128時(shí),使用該編碼方式,否則會(huì)使用 skiplist。這兩個(gè)參數(shù)可以通過(guò) zset-max-ziplist-entries、zset-max-ziplist-value 來(lái)自定義修改。
skiplist:zset實(shí)現(xiàn),一個(gè)zset同時(shí)包含一個(gè)字典(dict)和一個(gè)跳躍表(zskiplist)
8、Sorted Set 為什么同時(shí)使用字典和跳躍表?
主要是為了提升性能。
單獨(dú)使用字典:在執(zhí)行范圍型操作,比如 zrank、zrange,字典需要進(jìn)行排序,至少需要 O(NlogN) 的時(shí)間復(fù)雜度及額外 O(N) 的內(nèi)存空間。
單獨(dú)使用跳躍表:根據(jù)成員查找分值操作的復(fù)雜度從 O(1) 上升為 O(logN)。
9、Sorted Set 為什么使用跳躍表,而不是紅黑樹(shù)?
主要有以下幾個(gè)原因:
1)跳表的性能和紅黑樹(shù)差不多。
2)跳表更容易實(shí)現(xiàn)和調(diào)試。
網(wǎng)上有同學(xué)說(shuō)是因?yàn)樽髡卟粫?huì)紅黑樹(shù),我覺(jué)得挺有可能的。
10、Hash 對(duì)象底層結(jié)構(gòu)
Hash 對(duì)象當(dāng)前有兩種編碼:ziplist、hashtable
ziplist:使用壓縮列表實(shí)現(xiàn),每當(dāng)有新的鍵值對(duì)要加入到哈希對(duì)象時(shí),程序會(huì)先將保存了鍵的節(jié)點(diǎn)推入到壓縮列表的表尾,然后再將保存了值的節(jié)點(diǎn)推入到壓縮列表表尾。
因此:1)保存了同一鍵值對(duì)的兩個(gè)節(jié)點(diǎn)總是緊挨在一起,保存鍵的節(jié)點(diǎn)在前,保存值的節(jié)點(diǎn)在后;2)先添加到哈希對(duì)象中的鍵值對(duì)會(huì)被放在壓縮列表的表頭方向,而后來(lái)添加的會(huì)被放在表尾方向。
hashtable:使用字典作為底層實(shí)現(xiàn),哈希對(duì)象中的每個(gè)鍵值對(duì)都使用一個(gè)字典鍵值來(lái)保存,跟 java 中的 HashMap 類(lèi)似。
11、Hash 對(duì)象的擴(kuò)容流程
hash 對(duì)象在擴(kuò)容時(shí)使用了一種叫“漸進(jìn)式 rehash”的方式,步驟如下:
1)計(jì)算新表 size、掩碼,為新表 ht[1] 分配空間,讓字典同時(shí)持有 ht[0] 和 ht[1] 兩個(gè)哈希表。
2)將 rehash 索引計(jì)數(shù)器變量 rehashidx 的值設(shè)置為0,表示 rehash 正式開(kāi)始。
3)在 rehash 進(jìn)行期間,每次對(duì)字典執(zhí)行添加、刪除、査找、更新操作時(shí),程序除了執(zhí)行指定的操作以外,還會(huì)觸發(fā)額外的 rehash 操作,在源碼中的 _dictRehashStep 方法。
_dictRehashStep:從名字也可以看出來(lái),大意是 rehash 一步,也就是 rehash 一個(gè)索引位置。
該方法會(huì)從 ht[0] 表的 rehashidx 索引位置上開(kāi)始向后查找,找到第一個(gè)不為空的索引位置,將該索引位置的所有節(jié)點(diǎn) rehash 到 ht[1],當(dāng)本次 rehash 工作完成之后,將 ht[0] 索引位置為 rehashidx 的節(jié)點(diǎn)清空,同時(shí)將 rehashidx 屬性的值加一。
4)將 rehash 分?jǐn)偟矫總€(gè)操作上確實(shí)是非常妙的方式,但是萬(wàn)一此時(shí)服務(wù)器比較空閑,一直沒(méi)有什么操作,難道 redis 要一直持有兩個(gè)哈希表嗎?
答案當(dāng)然不是的。我們知道,redis 除了文件事件外,還有時(shí)間事件,redis 會(huì)定期觸發(fā)時(shí)間事件,這些時(shí)間事件用于執(zhí)行一些后臺(tái)操作,其中就包含 rehash 操作:當(dāng) redis 發(fā)現(xiàn)有字典正在進(jìn)行 rehash 操作時(shí),會(huì)花費(fèi)1毫秒的時(shí)間,一起幫忙進(jìn)行 rehash。
5)隨著操作的不斷執(zhí)行,最終在某個(gè)時(shí)間點(diǎn)上,ht[0] 的所有鍵值對(duì)都會(huì)被 rehash 至 ht[1],此時(shí) rehash 流程完成,會(huì)執(zhí)行最后的清理工作:釋放 ht[0] 的空間、將 ht[0] 指向 ht[1]、重置 ht[1]、重置 rehashidx 的值為 -1。
12、漸進(jìn)式 rehash 的優(yōu)點(diǎn)
漸進(jìn)式 rehash 的好處在于它采取分而治之的方式,將 rehash 鍵值對(duì)所需的計(jì)算工作均攤到對(duì)字典的每個(gè)添加、刪除、查找和更新操作上,從而避免了集中式 rehash 而帶來(lái)的龐大計(jì)算量。
在進(jìn)行漸進(jìn)式 rehash 的過(guò)程中,字典會(huì)同時(shí)使用 ht[0] 和 ht[1] 兩個(gè)哈希表, 所以在漸進(jìn)式 rehash 進(jìn)行期間,字典的刪除、査找、更新等操作會(huì)在兩個(gè)哈希表上進(jìn)行。例如,要在字典里面査找一個(gè)鍵的話,程序會(huì)先在 ht[0] 里面進(jìn)行査找,如果沒(méi)找到的話,就會(huì)繼續(xù)到 ht[1] 里面進(jìn)行査找,諸如此類(lèi)。
另外,在漸進(jìn)式 rehash 執(zhí)行期間,新增的鍵值對(duì)會(huì)被直接保存到 ht[1], ht[0] 不再進(jìn)行任何添加操作,這樣就保證了 ht[0] 包含的鍵值對(duì)數(shù)量會(huì)只減不增,并隨著 rehash 操作的執(zhí)行而最終變成空表。
13、rehash 流程在數(shù)據(jù)量大的時(shí)候會(huì)有什么問(wèn)題嗎(Hash 對(duì)象的擴(kuò)容流程在數(shù)據(jù)量大的時(shí)候會(huì)有什么問(wèn)題嗎)
1)擴(kuò)容期開(kāi)始時(shí),會(huì)先給 ht[1] 申請(qǐng)空間,所以在整個(gè)擴(kuò)容期間,會(huì)同時(shí)存在 ht[0] 和 ht[1],會(huì)占用額外的空間。
2)擴(kuò)容期間同時(shí)存在 ht[0] 和 ht[1],查找、刪除、更新等操作有概率需要操作兩張表,耗時(shí)會(huì)增加。
3)redis 在內(nèi)存使用接近 maxmemory 并且有設(shè)置驅(qū)逐策略的情況下,出現(xiàn) rehash 會(huì)使得內(nèi)存占用超過(guò) maxmemory,觸發(fā)驅(qū)逐淘汰操作,導(dǎo)致 master/slave 均有有大量的 key 被驅(qū)逐淘汰,從而出現(xiàn) master/slave 主從不一致。
14、Redis 的網(wǎng)絡(luò)事件處理器(Reactor 模式)
redis 基于 reactor 模式開(kāi)發(fā)了自己的網(wǎng)絡(luò)事件處理器,由4個(gè)部分組成:套接字、I/O 多路復(fù)用程序、文件事件分派器(dispatcher)、以及事件處理器。
套接字:socket 連接,也就是客戶(hù)端連接。當(dāng)一個(gè)套接字準(zhǔn)備好執(zhí)行連接、寫(xiě)入、讀取、關(guān)閉等操作時(shí), 就會(huì)產(chǎn)生一個(gè)相應(yīng)的文件事件。因?yàn)橐粋€(gè)服務(wù)器通常會(huì)連接多個(gè)套接字, 所以多個(gè)文件事件有可能會(huì)并發(fā)地出現(xiàn)。
I/O 多路復(fù)用程序:提供 select、epoll、evport、kqueue 的實(shí)現(xiàn),會(huì)根據(jù)當(dāng)前系統(tǒng)自動(dòng)選擇最佳的方式。負(fù)責(zé)監(jiān)聽(tīng)多個(gè)套接字,當(dāng)套接字產(chǎn)生事件時(shí),會(huì)向文件事件分派器傳送那些產(chǎn)生了事件的套接字。當(dāng)多個(gè)文件事件并發(fā)出現(xiàn)時(shí), I/O 多路復(fù)用程序會(huì)將所有產(chǎn)生事件的套接字都放到一個(gè)隊(duì)列里面,然后通過(guò)這個(gè)隊(duì)列,以有序、同步、每次一個(gè)套接字的方式向文件事件分派器傳送套接字:當(dāng)上一個(gè)套接字產(chǎn)生的事件被處理完畢之后,才會(huì)繼續(xù)傳送下一個(gè)套接字。
文件事件分派器:接收 I/O 多路復(fù)用程序傳來(lái)的套接字, 并根據(jù)套接字產(chǎn)生的事件的類(lèi)型, 調(diào)用相應(yīng)的事件處理器。
事件處理器:事件處理器就是一個(gè)個(gè)函數(shù), 定義了某個(gè)事件發(fā)生時(shí), 服務(wù)器應(yīng)該執(zhí)行的動(dòng)作。例如:建立連接、命令查詢(xún)、命令寫(xiě)入、連接關(guān)閉等等。
15、Redis 刪除過(guò)期鍵的策略(緩存失效策略、數(shù)據(jù)過(guò)期策略)
定時(shí)刪除:在設(shè)置鍵的過(guò)期時(shí)間的同時(shí),創(chuàng)建一個(gè)定時(shí)器,讓定時(shí)器在鍵的過(guò)期時(shí)間來(lái)臨時(shí),立即執(zhí)行對(duì)鍵的刪除操作。對(duì)內(nèi)存最友好,對(duì) CPU 時(shí)間最不友好。
惰性刪除:放任鍵過(guò)期不管,但是每次獲取鍵時(shí),都檢査鍵是否過(guò)期,如果過(guò)期的話,就刪除該鍵;如果沒(méi)有過(guò)期,就返回該鍵。對(duì) CPU 時(shí)間最優(yōu)化,對(duì)內(nèi)存最不友好。
定期刪除:每隔一段時(shí)間,默認(rèn)100ms,程序就對(duì)數(shù)據(jù)庫(kù)進(jìn)行一次檢査,刪除里面的過(guò)期鍵。至 于要?jiǎng)h除多少過(guò)期鍵,以及要檢査多少個(gè)數(shù)據(jù)庫(kù),則由算法決定。前兩種策略的折中,對(duì) CPU 時(shí)間和內(nèi)存的友好程度較平衡。
Redis 使用惰性刪除和定期刪除。
16、Redis 的內(nèi)存淘汰(驅(qū)逐)策略
當(dāng) redis 的內(nèi)存空間(maxmemory 參數(shù)配置)已經(jīng)用滿(mǎn)時(shí),redis 將根據(jù)配置的驅(qū)逐策略(maxmemory-policy 參數(shù)配置),進(jìn)行相應(yīng)的動(dòng)作。
網(wǎng)上很多資料都是寫(xiě) 6 種,但是其實(shí)當(dāng)前 redis 的淘汰策略已經(jīng)有 8 種了,多余的兩種是 Redis 4.0 新增的,基于 LFU(Least Frequently Used)算法實(shí)現(xiàn)的。
- noeviction:默認(rèn)策略,不淘汰任何 key,直接返回錯(cuò)誤
- allkeys-lru:在所有的 key 中,使用 LRU 算法淘汰部分
- keyallkeys-lfu:在所有的 key 中,使用 LFU 算法淘汰部分 key,該算法于 Redis 4.0 新增
- allkeys-random:在所有的 key 中,隨機(jī)淘汰部分
- keyvolatile-lru:在設(shè)置了過(guò)期時(shí)間的 key 中,使用 LRU 算法淘汰部分
- keyvolatile-lfu:在設(shè)置了過(guò)期時(shí)間的 key 中,使用 LFU 算法淘汰部分 key,該算法于 Redis 4.0 新增
- volatile-random:在設(shè)置了過(guò)期時(shí)間的 key 中,隨機(jī)淘汰部分 keyvolatile-ttl:在設(shè)置了過(guò)期時(shí)間的 key 中,挑選 TTL(time to live,剩余時(shí)間)短的 key 淘汰
17、Redis 的 LRU 算法怎么實(shí)現(xiàn)的?
Redis 在 redisObject 結(jié)構(gòu)體中定義了一個(gè)長(zhǎng)度 24 bit 的 unsigned 類(lèi)型的字段(unsigned lru:LRU_BITS),在 LRU 算法中用來(lái)存儲(chǔ)對(duì)象最后一次被命令程序訪問(wèn)的時(shí)間。
具體的 LRU 算法經(jīng)歷了兩個(gè)版本。
版本1:隨機(jī)選取 N 個(gè)淘汰法。
最初 Redis 是這樣實(shí)現(xiàn)的:隨機(jī)選 N(默認(rèn)5) 個(gè) key,把空閑時(shí)間(idle time)最大的那個(gè) key 移除。這邊的 N 可通過(guò) maxmemory-samples 配置項(xiàng)修改。
就是這么簡(jiǎn)單,簡(jiǎn)單得讓人不敢相信了,而且十分有效。
但是這個(gè)算法有個(gè)明顯的缺點(diǎn):每次都是隨機(jī)從 N 個(gè)里選擇 1 個(gè),并沒(méi)有利用前一輪的歷史信息。其實(shí)在上一輪移除 key 的過(guò)程中,其實(shí)是知道了 N 個(gè) key 的 idle time 的情況的,那在下一輪移除 key 時(shí),其實(shí)可以利用上一輪的這些信息。這也是 Redis 3.0 的優(yōu)化思想。
版本2:Redis 3.0 對(duì) LRU 算法進(jìn)行改進(jìn),引入了緩沖池(pool,默認(rèn)16)的概念。
當(dāng)每一輪移除 key 時(shí),拿到了 N(默認(rèn)5)個(gè) key 的 idle time,遍歷處理這 N 個(gè) key,如果 key 的 idle time 比 pool 里面的 key 的 idle time 還要大,就把它添加到 pool 里面去。
當(dāng) pool 放滿(mǎn)之后,每次如果有新的 key 需要放入,需要將 pool 中 idle time 最小的一個(gè) key 移除。這樣相當(dāng)于 pool 里面始終維護(hù)著還未被淘汰的 idle time 最大的 16 個(gè) key。
當(dāng)我們每輪要淘汰的時(shí)候,直接從 pool 里面取出 idle time 最大的 key(只取1個(gè)),將之淘汰掉。
整個(gè)流程相當(dāng)于隨機(jī)取 5 個(gè) key 放入 pool,然后淘汰 pool 中空閑時(shí)間最大的 key,然后再隨機(jī)取 5 個(gè) key放入 pool,繼續(xù)淘汰 pool 中空閑時(shí)間最大的 key,一直持續(xù)下去。
在進(jìn)入淘汰前會(huì)計(jì)算出需要釋放的內(nèi)存大小,然后就一直循環(huán)上述流程,直至釋放足夠的內(nèi)存。
18、Redis 的持久化機(jī)制有哪幾種,各自的實(shí)現(xiàn)原理和優(yōu)缺點(diǎn)?
Redis 的持久化機(jī)制有:RDB、AOF、混合持久化(RDB+AOF,Redis 4.0引入)。
1)RDB
描述:類(lèi)似于快照。在某個(gè)時(shí)間點(diǎn),將 Redis 在內(nèi)存中的數(shù)據(jù)庫(kù)狀態(tài)(數(shù)據(jù)庫(kù)的鍵值對(duì)等信息)保存到磁盤(pán)里面。RDB 持久化功能生成的 RDB 文件是經(jīng)過(guò)壓縮的二進(jìn)制文件。
命令:有兩個(gè) Redis 命令可以用于生成 RDB 文件,一個(gè)是 SAVE,另一個(gè)是 BGSAVE。
開(kāi)啟:使用 save point 配置,滿(mǎn)足 save point 條件后會(huì)觸發(fā) BGSAVE 來(lái)存儲(chǔ)一次快照,這邊的 save point 檢查就是在上文提到的 serverCron 中進(jìn)行。
save point 格式:save <seconds> <changes>,含義是 Redis 如果在 seconds 秒內(nèi)數(shù)據(jù)發(fā)生了 changes 次改變,就保存快照文件。例如 Redis 默認(rèn)就配置了以下3個(gè):
save 900 1 #900秒內(nèi)有1個(gè)key發(fā)生了變化,則觸發(fā)保存RDB文件 save 300 10 #300秒內(nèi)有10個(gè)key發(fā)生了變化,則觸發(fā)保存RDB文件 save 60 10000 #60秒內(nèi)有10000個(gè)key發(fā)生了變化,則觸發(fā)保存RDB文件
關(guān)閉:1)注釋掉所有save point 配置可以關(guān)閉 RDB 持久化。2)在所有 save point 配置后增加:save "",該配置可以刪除所有之前配置的 save point。
save ""
SAVE:生成 RDB 快照文件,但是會(huì)阻塞主進(jìn)程,服務(wù)器將無(wú)法處理客戶(hù)端發(fā)來(lái)的命令請(qǐng)求,所以通常不會(huì)直接使用該命令。
BGSAVE:fork 子進(jìn)程來(lái)生成 RDB 快照文件,阻塞只會(huì)發(fā)生在 fork 子進(jìn)程的時(shí)候,之后主進(jìn)程可以正常處理請(qǐng)求,詳細(xì)過(guò)程如下圖:
fork:在 Linux 系統(tǒng)中,調(diào)用 fork() 時(shí),會(huì)創(chuàng)建出一個(gè)新進(jìn)程,稱(chēng)為子進(jìn)程,子進(jìn)程會(huì)拷貝父進(jìn)程的 page table。如果進(jìn)程占用的內(nèi)存越大,進(jìn)程的 page table 也會(huì)越大,那么 fork 也會(huì)占用更多的時(shí)間。如果 Redis 占用的內(nèi)存很大,那么在 fork 子進(jìn)程時(shí),則會(huì)出現(xiàn)明顯的停頓現(xiàn)象。
RDB 的優(yōu)點(diǎn)
1)RDB 文件是是經(jīng)過(guò)壓縮的二進(jìn)制文件,占用空間很小,它保存了 Redis 某個(gè)時(shí)間點(diǎn)的數(shù)據(jù)集,很適合用于做備份。 比如說(shuō),你可以在最近的 24 小時(shí)內(nèi),每小時(shí)備份一次 RDB 文件,并且在每個(gè)月的每一天,也備份一個(gè) RDB 文件。這樣的話,即使遇上問(wèn)題,也可以隨時(shí)將數(shù)據(jù)集還原到不同的版本。
2)RDB 非常適用于災(zāi)難恢復(fù)(disaster recovery):它只有一個(gè)文件,并且內(nèi)容都非常緊湊,可以(在加密后)將它傳送到別的數(shù)據(jù)中心。
3)RDB 可以最大化 redis 的性能。父進(jìn)程在保存 RDB 文件時(shí)唯一要做的就是 fork 出一個(gè)子進(jìn)程,然后這個(gè)子進(jìn)程就會(huì)處理接下來(lái)的所有保存工作,父進(jìn)程無(wú)須執(zhí)行任何磁盤(pán) I/O 操作。
4)RDB 在恢復(fù)大數(shù)據(jù)集時(shí)的速度比 AOF 的恢復(fù)速度要快。
RDB 的缺點(diǎn)
1)RDB 在服務(wù)器故障時(shí)容易造成數(shù)據(jù)的丟失。RDB 允許我們通過(guò)修改 save point 配置來(lái)控制持久化的頻率。但是,因?yàn)?RDB 文件需要保存整個(gè)數(shù)據(jù)集的狀態(tài), 所以它是一個(gè)比較重的操作,如果頻率太頻繁,可能會(huì)對(duì) Redis 性能產(chǎn)生影響。所以通??赡茉O(shè)置至少5分鐘才保存一次快照,這時(shí)如果 Redis 出現(xiàn)宕機(jī)等情況,則意味著最多可能丟失5分鐘數(shù)據(jù)。
2)RDB 保存時(shí)使用 fork 子進(jìn)程進(jìn)行數(shù)據(jù)的持久化,如果數(shù)據(jù)比較大的話,fork 可能會(huì)非常耗時(shí),造成 Redis 停止處理服務(wù)N毫秒。如果數(shù)據(jù)集很大且 CPU 比較繁忙的時(shí)候,停止服務(wù)的時(shí)間甚至?xí)揭幻搿?/p>
3)Linux fork 子進(jìn)程采用的是 copy-on-write 的方式。在 Redis 執(zhí)行 RDB 持久化期間,如果 client 寫(xiě)入數(shù)據(jù)很頻繁,那么將增加 Redis 占用的內(nèi)存,最壞情況下,內(nèi)存的占用將達(dá)到原先的2倍。剛 fork 時(shí),主進(jìn)程和子進(jìn)程共享內(nèi)存,但是隨著主進(jìn)程需要處理寫(xiě)操作,主進(jìn)程需要將修改的頁(yè)面拷貝一份出來(lái),然后進(jìn)行修改。極端情況下,如果所有的頁(yè)面都被修改,則此時(shí)的內(nèi)存占用是原先的2倍。
2)AOF
描述:保存 Redis 服務(wù)器所執(zhí)行的所有寫(xiě)操作命令來(lái)記錄數(shù)據(jù)庫(kù)狀態(tài),并在服務(wù)器啟動(dòng)時(shí),通過(guò)重新執(zhí)行這些命令來(lái)還原數(shù)據(jù)集。
開(kāi)啟:AOF 持久化默認(rèn)是關(guān)閉的,可以通過(guò)配置:appendonly yes 開(kāi)啟。
關(guān)閉:使用配置 appendonly no 可以關(guān)閉 AOF 持久化。
AOF 持久化功能的實(shí)現(xiàn)可以分為三個(gè)步驟:命令追加、文件寫(xiě)入、文件同步。
命令追加:當(dāng) AOF 持久化功能打開(kāi)時(shí),服務(wù)器在執(zhí)行完一個(gè)寫(xiě)命令之后,會(huì)將被執(zhí)行的寫(xiě)命令追加到服務(wù)器狀態(tài)的 aof 緩沖區(qū)(aof_buf)的末尾。
文件寫(xiě)入與文件同步:可能有人不明白為什么將 aof_buf 的內(nèi)容寫(xiě)到磁盤(pán)上需要兩步操作,這邊簡(jiǎn)單解釋一下。
Linux 操作系統(tǒng)中為了提升性能,使用了頁(yè)緩存(page cache)。當(dāng)我們將 aof_buf 的內(nèi)容寫(xiě)到磁盤(pán)上時(shí),此時(shí)數(shù)據(jù)并沒(méi)有真正的落盤(pán),而是在 page cache 中,為了將 page cache 中的數(shù)據(jù)真正落盤(pán),需要執(zhí)行 fsync / fdatasync 命令來(lái)強(qiáng)制刷盤(pán)。這邊的文件同步做的就是刷盤(pán)操作,或者叫文件刷盤(pán)可能更容易理解一些。
在文章開(kāi)頭,我們提過(guò) serverCron 時(shí)間事件中會(huì)觸發(fā) flushAppendOnlyFile 函數(shù),該函數(shù)會(huì)根據(jù)服務(wù)器配置的 appendfsync 參數(shù)值,來(lái)決定是否將 aof_buf 緩沖區(qū)的內(nèi)容寫(xiě)入和保存到 AOF 文件。
appendfsync 參數(shù)有三個(gè)選項(xiàng):
always:每處理一個(gè)命令都將 aof_buf 緩沖區(qū)中的所有內(nèi)容寫(xiě)入并同步到AOF 文件,即每個(gè)命令都刷盤(pán)。everysec:將 aof_buf 緩沖區(qū)中的所有內(nèi)容寫(xiě)入到 AOF 文件,如果上次同步 AOF 文件的時(shí)間距離現(xiàn)在超過(guò)一秒鐘, 那么再次對(duì) AOF 文件進(jìn)行同步, 并且這個(gè)同步操作是異步的,由一個(gè)后臺(tái)線程專(zhuān)門(mén)負(fù)責(zé)執(zhí)行,即每秒刷盤(pán)1次。no:將 aof_buf 緩沖區(qū)中的所有內(nèi)容寫(xiě)入到 AOF 文件, 但并不對(duì) AOF 文件進(jìn)行同步, 何時(shí)同步由操作系統(tǒng)來(lái)決定。即不執(zhí)行刷盤(pán),讓操作系統(tǒng)自己執(zhí)行刷盤(pán)。
AOF 的優(yōu)點(diǎn)
AOF 比 RDB可靠。你可以設(shè)置不同的 fsync 策略:no、everysec 和 always。默認(rèn)是 everysec,在這種配置下,redis 仍然可以保持良好的性能,并且就算發(fā)生故障停機(jī),也最多只會(huì)丟失一秒鐘的數(shù)據(jù)。AOF文件是一個(gè)純追加的日志文件。即使日志因?yàn)槟承┰蚨宋磳?xiě)入完整的命令(比如寫(xiě)入時(shí)磁盤(pán)已滿(mǎn),寫(xiě)入中途停機(jī)等等), 我們也可以使用 redis-check-aof 工具也可以輕易地修復(fù)這種問(wèn)題。當(dāng) AOF文件太大時(shí),Redis 會(huì)自動(dòng)在后臺(tái)進(jìn)行重寫(xiě):重寫(xiě)后的新 AOF 文件包含了恢復(fù)當(dāng)前數(shù)據(jù)集所需的最小命令集合。整個(gè)重寫(xiě)是絕對(duì)安全,因?yàn)橹貙?xiě)是在一個(gè)新的文件上進(jìn)行,同時(shí) Redis 會(huì)繼續(xù)往舊的文件追加數(shù)據(jù)。當(dāng)新文件重寫(xiě)完畢,Redis 會(huì)把新舊文件進(jìn)行切換,然后開(kāi)始把數(shù)據(jù)寫(xiě)到新文件上。AOF 文件有序地保存了對(duì)數(shù)據(jù)庫(kù)執(zhí)行的所有寫(xiě)入操作以 Redis 協(xié)議的格式保存, 因此 AOF 文件的內(nèi)容非常容易被人讀懂, 對(duì)文件進(jìn)行分析(parse)也很輕松。如果你不小心執(zhí)行了 FLUSHALL 命令把所有數(shù)據(jù)刷掉了,但只要 AOF 文件沒(méi)有被重寫(xiě),那么只要停止服務(wù)器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重啟 Redis , 就可以將數(shù)據(jù)集恢復(fù)到 FLUSHALL 執(zhí)行之前的狀態(tài)。
AOF 的缺點(diǎn)
對(duì)于相同的數(shù)據(jù)集,AOF 文件的大小一般會(huì)比 RDB 文件大。根據(jù)所使用的 fsync 策略,AOF 的速度可能會(huì)比 RDB 慢。通常 fsync 設(shè)置為每秒一次就能獲得比較高的性能,而關(guān)閉 fsync 可以讓 AOF 的速度和 RDB 一樣快。AOF 在過(guò)去曾經(jīng)發(fā)生過(guò)這樣的 bug :因?yàn)閭€(gè)別命令的原因,導(dǎo)致 AOF 文件在重新載入時(shí),無(wú)法將數(shù)據(jù)集恢復(fù)成保存時(shí)的原樣。(舉個(gè)例子,阻塞命令 BRPOPLPUSH 就曾經(jīng)引起過(guò)這樣的 bug ) 。雖然這種 bug 在 AOF 文件中并不常見(jiàn), 但是相較而言, RDB 幾乎是不可能出現(xiàn)這種 bug 的。
3)混合持久化
描述:混合持久化并不是一種全新的持久化方式,而是對(duì)已有方式的優(yōu)化。混合持久化只發(fā)生于 AOF 重寫(xiě)過(guò)程。使用了混合持久化,重寫(xiě)后的新 AOF 文件前半段是 RDB 格式的全量數(shù)據(jù),后半段是 AOF 格式的增量數(shù)據(jù)。
整體格式為:[RDB file][AOF tail]
開(kāi)啟:混合持久化的配置參數(shù)為 aof-use-rdb-preamble,配置為 yes 時(shí)開(kāi)啟混合持久化,在 redis 4 剛引入時(shí),默認(rèn)是關(guān)閉混合持久化的,但是在 redis 5 中默認(rèn)已經(jīng)打開(kāi)了。
關(guān)閉:使用 aof-use-rdb-preamble no 配置即可關(guān)閉混合持久化。
混合持久化本質(zhì)是通過(guò) AOF 后臺(tái)重寫(xiě)(bgrewriteaof 命令)完成的,不同的是當(dāng)開(kāi)啟混合持久化時(shí),fork 出的子進(jìn)程先將當(dāng)前全量數(shù)據(jù)以 RDB 方式寫(xiě)入新的 AOF 文件,然后再將 AOF 重寫(xiě)緩沖區(qū)(aof_rewrite_buf_blocks)的增量命令以 AOF 方式寫(xiě)入到文件,寫(xiě)入完成后通知主進(jìn)程將新的含有 RDB 格式和 AOF 格式的 AOF 文件替換舊的的 AOF 文件。
優(yōu)點(diǎn):結(jié)合 RDB 和 AOF 的優(yōu)點(diǎn), 更快的重寫(xiě)和恢復(fù)。
缺點(diǎn):AOF 文件里面的 RDB 部分不再是 AOF 格式,可讀性差。
19、為什么需要 AOF 重寫(xiě)
AOF 持久化是通過(guò)保存被執(zhí)行的寫(xiě)命令來(lái)記錄數(shù)據(jù)庫(kù)狀態(tài)的,隨著寫(xiě)入命令的不斷增加,AOF 文件中的內(nèi)容會(huì)越來(lái)越多,文件的體積也會(huì)越來(lái)越大。
如果不加以控制,體積過(guò)大的 AOF 文件可能會(huì)對(duì) Redis 服務(wù)器、甚至整個(gè)宿主機(jī)造成影響,并且 AOF 文件的體積越大,使用 AOF 文件來(lái)進(jìn)行數(shù)據(jù)還原所需的時(shí)間就越多。
舉個(gè)例子, 如果你對(duì)一個(gè)計(jì)數(shù)器調(diào)用了 100 次 INCR , 那么僅僅是為了保存這個(gè)計(jì)數(shù)器的當(dāng)前值, AOF 文件就需要使用 100 條記錄。
然而在實(shí)際上, 只使用一條 SET 命令已經(jīng)足以保存計(jì)數(shù)器的當(dāng)前值了, 其余 99 條記錄實(shí)際上都是多余的。
為了處理這種情況, Redis 引入了 AOF 重寫(xiě):可以在不打斷服務(wù)端處理請(qǐng)求的情況下, 對(duì) AOF 文件進(jìn)行重建(rebuild)。
20、介紹下 AOF 重寫(xiě)的過(guò)程、AOF 后臺(tái)重寫(xiě)存在的問(wèn)題、如何解決 AOF 后臺(tái)重寫(xiě)存在的數(shù)據(jù)不一致問(wèn)題
描述:Redis 生成新的 AOF 文件來(lái)代替舊 AOF 文件,這個(gè)新的 AOF 文件包含重建當(dāng)前數(shù)據(jù)集所需的最少命令。具體過(guò)程是遍歷所有數(shù)據(jù)庫(kù)的所有鍵,從數(shù)據(jù)庫(kù)讀取鍵現(xiàn)在的值,然后用一條命令去記錄鍵值對(duì),代替之前記錄這個(gè)鍵值對(duì)的多條命令。
命令:有兩個(gè) Redis 命令可以用于觸發(fā) AOF 重寫(xiě),一個(gè)是 BGREWRITEAOF 、另一個(gè)是 REWRITEAOF 命令;
開(kāi)啟:AOF 重寫(xiě)由兩個(gè)參數(shù)共同控制,auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size,同時(shí)滿(mǎn)足這兩個(gè)條件,則觸發(fā) AOF 后臺(tái)重寫(xiě) BGREWRITEAOF。
// 當(dāng)前AOF文件比上次重寫(xiě)后的AOF文件大小的增長(zhǎng)比例超過(guò)100 auto-aof-rewrite-percentage 100 // 當(dāng)前AOF文件的文件大小大于64MB auto-aof-rewrite-min-size 64mb
關(guān)閉:auto-aof-rewrite-percentage 0,指定0的百分比,以禁用自動(dòng)AOF重寫(xiě)功能。
auto-aof-rewrite-percentage 0
REWRITEAOF:進(jìn)行 AOF 重寫(xiě),但是會(huì)阻塞主進(jìn)程,服務(wù)器將無(wú)法處理客戶(hù)端發(fā)來(lái)的命令請(qǐng)求,通常不會(huì)直接使用該命令。
BGREWRITEAOF:fork 子進(jìn)程來(lái)進(jìn)行 AOF 重寫(xiě),阻塞只會(huì)發(fā)生在 fork 子進(jìn)程的時(shí)候,之后主進(jìn)程可以正常處理請(qǐng)求。
REWRITEAOF 和 BGREWRITEAOF 的關(guān)系與 SAVE 和 BGSAVE 的關(guān)系類(lèi)似。
AOF 后臺(tái)重寫(xiě)存在的問(wèn)題
AOF 后臺(tái)重寫(xiě)使用子進(jìn)程進(jìn)行從寫(xiě),解決了主進(jìn)程阻塞的問(wèn)題,但是仍然存在另一個(gè)問(wèn)題:子進(jìn)程在進(jìn)行 AOF 重寫(xiě)期間,服務(wù)器主進(jìn)程還需要繼續(xù)處理命令請(qǐng)求,新的命令可能會(huì)對(duì)現(xiàn)有的數(shù)據(jù)庫(kù)狀態(tài)進(jìn)行修改,從而使得當(dāng)前的數(shù)據(jù)庫(kù)狀態(tài)和重寫(xiě)后的 AOF 文件保存的數(shù)據(jù)庫(kù)狀態(tài)不一致。
如何解決 AOF 后臺(tái)重寫(xiě)存在的數(shù)據(jù)不一致問(wèn)題
為了解決上述問(wèn)題,Redis 引入了 AOF 重寫(xiě)緩沖區(qū)(aof_rewrite_buf_blocks),這個(gè)緩沖區(qū)在服務(wù)器創(chuàng)建子進(jìn)程之后開(kāi)始使用,當(dāng) Redis 服務(wù)器執(zhí)行完一個(gè)寫(xiě)命令之后,它會(huì)同時(shí)將這個(gè)寫(xiě)命令追加到 AOF 緩沖區(qū)和 AOF 重寫(xiě)緩沖區(qū)。
這樣一來(lái)可以保證:
1、現(xiàn)有 AOF 文件的處理工作會(huì)如常進(jìn)行。這樣即使在重寫(xiě)的中途發(fā)生停機(jī),現(xiàn)有的 AOF 文件也還是安全的。
2、從創(chuàng)建子進(jìn)程開(kāi)始,也就是 AOF 重寫(xiě)開(kāi)始,服務(wù)器執(zhí)行的所有寫(xiě)命令會(huì)被記錄到 AOF 重寫(xiě)緩沖區(qū)里面。
這樣,當(dāng)子進(jìn)程完成 AOF 重寫(xiě)工作后,父進(jìn)程會(huì)在 serverCron 中檢測(cè)到子進(jìn)程已經(jīng)重寫(xiě)結(jié)束,則會(huì)執(zhí)行以下工作:
1、將 AOF 重寫(xiě)緩沖區(qū)中的所有內(nèi)容寫(xiě)入到新 AOF 文件中,這時(shí)新 AOF 文件所保存的數(shù)據(jù)庫(kù)狀態(tài)將和服務(wù)器當(dāng)前的數(shù)據(jù)庫(kù)狀態(tài)一致。
2、對(duì)新的 AOF 文件進(jìn)行改名,原子的覆蓋現(xiàn)有的 AOF 文件,完成新舊兩個(gè) AOF 文件的替換。
之后,父進(jìn)程就可以繼續(xù)像往常一樣接受命令請(qǐng)求了。
21、RDB、AOF、混合持久,我應(yīng)該用哪一個(gè)?
一般來(lái)說(shuō), 如果想盡量保證數(shù)據(jù)安全性, 你應(yīng)該同時(shí)使用 RDB 和 AOF 持久化功能,同時(shí)可以開(kāi)啟混合持久化。
如果你非常關(guān)心你的數(shù)據(jù), 但仍然可以承受數(shù)分鐘以?xún)?nèi)的數(shù)據(jù)丟失, 那么你可以只使用 RDB 持久化。
如果你的數(shù)據(jù)是可以丟失的,則可以關(guān)閉持久化功能,在這種情況下,Redis 的性能是最高的。
使用 Redis 通常都是為了提升性能,而如果為了不丟失數(shù)據(jù)而將 appendfsync 設(shè)置為 always 級(jí)別時(shí),對(duì) Redis 的性能影響是很大的,在這種不能接受數(shù)據(jù)丟失的場(chǎng)景,其實(shí)可以考慮直接選擇 MySQL 等類(lèi)似的數(shù)據(jù)庫(kù)。
22、同時(shí)開(kāi)啟RDB和AOF,服務(wù)重啟時(shí)如何加載
簡(jiǎn)單來(lái)說(shuō),如果同時(shí)啟用了 AOF 和 RDB,Redis 重新啟動(dòng)時(shí),會(huì)使用 AOF 文件來(lái)重建數(shù)據(jù)集,因?yàn)橥ǔ?lái)說(shuō), AOF 的數(shù)據(jù)會(huì)更完整。
而在引入了混合持久化之后,使用 AOF 重建數(shù)據(jù)集時(shí),會(huì)通過(guò)文件開(kāi)頭是否為“REDIS”來(lái)判斷是否為混合持久化。
完整流程如下圖所示:
23、Redis 怎么保證高可用、有哪些集群模式
主從復(fù)制、哨兵模式、集群模式。
24、主從復(fù)制
在當(dāng)前最新的 Redis 6.0 中,主從復(fù)制的完整過(guò)程如下:
1)開(kāi)啟主從復(fù)制
通常有以下三種方式:
在 slave 直接執(zhí)行命令:slaveof <masterip> <masterport>在 slave 配置文件中加入:slaveof <masterip> <masterport>使用啟動(dòng)命令:--slaveof <masterip> <masterport>
注:在 Redis 5.0 之后,slaveof 相關(guān)命令和配置已經(jīng)被替換成 replicaof,例如 replicaof <masterip> <masterport>。為了兼容舊版本,通過(guò)配置的方式仍然支持 slaveof,但是通過(guò)命令的方式則不行了。
2)建立套接字(socket)連接
slave 將根據(jù)指定的 IP 地址和端口,向 master 發(fā)起套接字(socket)連接,master 在接受(accept) slave 的套接字連接之后,為該套接字創(chuàng)建相應(yīng)的客戶(hù)端狀態(tài),此時(shí)連接建立完成。
3)發(fā)送PING命令
slave 向 master 發(fā)送一個(gè) PING 命令,以檢査套接字的讀寫(xiě)狀態(tài)是否正常、 master 能否正常處理命令請(qǐng)求。
4)身份驗(yàn)證
slave 向 master 發(fā)送 AUTH password 命令來(lái)進(jìn)行身份驗(yàn)證。
5)發(fā)送端口信息
在身份驗(yàn)證通過(guò)后后, slave 將向 master 發(fā)送自己的監(jiān)聽(tīng)端口號(hào), master 收到后記錄在 slave 所對(duì)應(yīng)的客戶(hù)端狀態(tài)的 slave_listening_port 屬性中。
6)發(fā)送IP地址
如果配置了 slave_announce_ip,則 slave 向 master 發(fā)送 slave_announce_ip 配置的 IP 地址, master 收到后記錄在 slave 所對(duì)應(yīng)的客戶(hù)端狀態(tài)的 slave_ip 屬性。
該配置是用于解決服務(wù)器返回內(nèi)網(wǎng) IP 時(shí),其他服務(wù)器無(wú)法訪問(wèn)的情況??梢酝ㄟ^(guò)該配置直接指定公網(wǎng) IP。
7)發(fā)送CAPA
CAPA 全稱(chēng)是 capabilities,這邊表示的是同步復(fù)制的能力。slave 會(huì)在這一階段發(fā)送 capa 告訴 master 自己具備的(同步)復(fù)制能力, master 收到后記錄在 slave 所對(duì)應(yīng)的客戶(hù)端狀態(tài)的 slave_capa 屬性。
8)數(shù)據(jù)同步
slave 將向 master 發(fā)送 PSYNC 命令, master 收到該命令后判斷是進(jìn)行部分重同步還是完整重同步,然后根據(jù)策略進(jìn)行數(shù)據(jù)的同步。
9)命令傳播
當(dāng)完成了同步之后,就會(huì)進(jìn)入命令傳播階段,這時(shí) master 只要一直將自己執(zhí)行的寫(xiě)命令發(fā)送給 slave ,而 slave 只要一直接收并執(zhí)行 master 發(fā)來(lái)的寫(xiě)命令,就可以保證 master 和 slave 一直保持一致了。
以部分重同步為例,主從復(fù)制的核心步驟流程圖如下:
25、哨兵
哨兵(Sentinel) 是 Redis 的高可用性解決方案:由一個(gè)或多個(gè) Sentinel 實(shí)例組成的 Sentinel 系統(tǒng)可以監(jiān)視任意多個(gè)主服務(wù)器,以及這些主服務(wù)器屬下的所有從服務(wù)器。
Sentinel 可以在被監(jiān)視的主服務(wù)器進(jìn)入下線狀態(tài)時(shí),自動(dòng)將下線主服務(wù)器的某個(gè)從服務(wù)器升級(jí)為新的主服務(wù)器,然后由新的主服務(wù)器代替已下線的主服務(wù)器繼續(xù)處理命令請(qǐng)求。
1)哨兵故障檢測(cè)
檢查主觀下線狀態(tài)
在默認(rèn)情況下,Sentinel 會(huì)以每秒一次的頻率向所有與它創(chuàng)建了命令連接的實(shí)例(包括主服務(wù)器、從服務(wù)器、其他 Sentinel 在內(nèi))發(fā)送 PING 命令,并通過(guò)實(shí)例返回的 PING 命令回復(fù)來(lái)判斷實(shí)例是否在線。
如果一個(gè)實(shí)例在 down-after-miliseconds 毫秒內(nèi),連續(xù)向 Sentinel 返回?zé)o效回復(fù),那么 Sentinel 會(huì)修改這個(gè)實(shí)例所對(duì)應(yīng)的實(shí)例結(jié)構(gòu),在結(jié)構(gòu)的 flags 屬性中設(shè)置 SRI_S_DOWN 標(biāo)識(shí),以此來(lái)表示這個(gè)實(shí)例已經(jīng)進(jìn)入主觀下線狀態(tài)。
檢查客觀下線狀態(tài)
當(dāng) Sentinel 將一個(gè)主服務(wù)器判斷為主觀下線之后,為了確定這個(gè)主服務(wù)器是否真的下線了,它會(huì)向同樣監(jiān)視這一服務(wù)器的其他 Sentinel 進(jìn)行詢(xún)問(wèn),看它們是否也認(rèn)為主服務(wù)器已經(jīng)進(jìn)入了下線狀態(tài)(可以是主觀下線或者客觀下線)。
當(dāng) Sentinel 從其他 Sentinel 那里接收到足夠數(shù)量(quorum,可配置)的已下線判斷之后,Sentinel 就會(huì)將服務(wù)器置為客觀下線,在 flags 上打上 SRI_O_DOWN 標(biāo)識(shí),并對(duì)主服務(wù)器執(zhí)行故障轉(zhuǎn)移操作。
2)哨兵故障轉(zhuǎn)移流程
當(dāng)哨兵監(jiān)測(cè)到某個(gè)主節(jié)點(diǎn)客觀下線之后,就會(huì)開(kāi)始故障轉(zhuǎn)移流程。核心流程如下:
發(fā)起一次選舉,選舉出領(lǐng)頭 Sentinel領(lǐng)頭 Sentinel 在已下線主服務(wù)器的所有從服務(wù)器里面,挑選出一個(gè)從服務(wù)器,并將其升級(jí)為新的主服務(wù)器。領(lǐng)頭 Sentinel 將剩余的所有從服務(wù)器改為復(fù)制新的主服務(wù)器。領(lǐng)頭 Sentinel 更新相關(guān)配置信息,當(dāng)這個(gè)舊的主服務(wù)器重新上線時(shí),將其設(shè)置為新的主服務(wù)器的從服務(wù)器。
26、集群模式
哨兵模式最大的缺點(diǎn)就是所有的數(shù)據(jù)都放在一臺(tái)服務(wù)器上,無(wú)法較好的進(jìn)行水平擴(kuò)展。
為了解決哨兵模式存在的問(wèn)題,集群模式應(yīng)運(yùn)而生。在高可用上,集群基本是直接復(fù)用的哨兵模式的邏輯,并且針對(duì)水平擴(kuò)展進(jìn)行了優(yōu)化。
集群模式具備的特點(diǎn)如下:
采取去中心化的集群模式,將數(shù)據(jù)按槽存儲(chǔ)分布在多個(gè) Redis 節(jié)點(diǎn)上。集群共有 16384 個(gè)槽,每個(gè)節(jié)點(diǎn)負(fù)責(zé)處理部分槽。使用 CRC16 算法來(lái)計(jì)算 key 所屬的槽:crc16(key,keylen) & 16383。所有的 Redis 節(jié)點(diǎn)彼此互聯(lián),通過(guò) PING-PONG 機(jī)制來(lái)進(jìn)行節(jié)點(diǎn)間的心跳檢測(cè)。分片內(nèi)采用一主多從保證高可用,并提供復(fù)制和故障恢復(fù)功能。在實(shí)際使用中,通常會(huì)將主從分布在不同機(jī)房,避免機(jī)房出現(xiàn)故障導(dǎo)致整個(gè)分片出問(wèn)題,下面的架構(gòu)圖就是這樣設(shè)計(jì)的??蛻?hù)端與 Redis 節(jié)點(diǎn)直連,不需要中間代理層(proxy)??蛻?hù)端不需要連接集群所有節(jié)點(diǎn),連接集群中任何一個(gè)可用節(jié)點(diǎn)即可。
集群的架構(gòu)圖如下所示:
27、集群選舉
故障轉(zhuǎn)移的第一步就是選舉出新的主節(jié)點(diǎn),以下是集群選舉新的主節(jié)點(diǎn)的方法:
1)當(dāng)從節(jié)點(diǎn)發(fā)現(xiàn)自己正在復(fù)制的主節(jié)點(diǎn)進(jìn)入已下線狀態(tài)時(shí),會(huì)發(fā)起一次選舉:將 currentEpoch(配置紀(jì)元)加1,然后向集群廣播一條 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到這條消息、并且具有投票權(quán)的主節(jié)點(diǎn)向這個(gè)從節(jié)點(diǎn)投票。
2)其他節(jié)點(diǎn)收到消息后,會(huì)判斷是否要給發(fā)送消息的節(jié)點(diǎn)投票,判斷流程如下:
當(dāng)前節(jié)點(diǎn)是 slave,或者當(dāng)前節(jié)點(diǎn)是 master,但是不負(fù)責(zé)處理槽,則當(dāng)前節(jié)點(diǎn)沒(méi)有投票權(quán),直接返回。請(qǐng)求節(jié)點(diǎn)的 currentEpoch 小于當(dāng)前節(jié)點(diǎn)的 currentEpoch,校驗(yàn)失敗返回。因?yàn)榘l(fā)送者的狀態(tài)與當(dāng)前集群狀態(tài)不一致,可能是長(zhǎng)時(shí)間下線的節(jié)點(diǎn)剛剛上線,這種情況下,直接返回即可。當(dāng)前節(jié)點(diǎn)在該 currentEpoch 已經(jīng)投過(guò)票,校驗(yàn)失敗返回。請(qǐng)求節(jié)點(diǎn)是 master,校驗(yàn)失敗返回。請(qǐng)求節(jié)點(diǎn)的 master 為空,校驗(yàn)失敗返回。請(qǐng)求節(jié)點(diǎn)的 master 沒(méi)有故障,并且不是手動(dòng)故障轉(zhuǎn)移,校驗(yàn)失敗返回。因?yàn)槭謩?dòng)故障轉(zhuǎn)移是可以在 master 正常的情況下直接發(fā)起的。上一次為該master的投票時(shí)間,在cluster_node_timeout的2倍范圍內(nèi),校驗(yàn)失敗返回。這個(gè)用于使獲勝?gòu)墓?jié)點(diǎn)有時(shí)間將其成為新主節(jié)點(diǎn)的消息通知給其他從節(jié)點(diǎn),從而避免另一個(gè)從節(jié)點(diǎn)發(fā)起新一輪選舉又進(jìn)行一次沒(méi)必要的故障轉(zhuǎn)移請(qǐng)求節(jié)點(diǎn)宣稱(chēng)要負(fù)責(zé)的槽位,是否比之前負(fù)責(zé)這些槽位的節(jié)點(diǎn),具有相等或更大的 configEpoch,如果不是,校驗(yàn)失敗返回。
如果通過(guò)以上所有校驗(yàn),那么主節(jié)點(diǎn)將向要求投票的從節(jié)點(diǎn)返回一條 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示這個(gè)主節(jié)點(diǎn)支持從節(jié)點(diǎn)成為新的主節(jié)點(diǎn)。
3)每個(gè)參與選舉的從節(jié)點(diǎn)都會(huì)接收 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,并根據(jù)自己收到了多少條這種消息來(lái)統(tǒng)計(jì)自己獲得了多少個(gè)主節(jié)點(diǎn)的支持。
4)如果集群里有N個(gè)具有投票權(quán)的主節(jié)點(diǎn),那么當(dāng)一個(gè)從節(jié)點(diǎn)收集到大于等于N/2+1 張支持票時(shí),這個(gè)從節(jié)點(diǎn)就會(huì)當(dāng)選為新的主節(jié)點(diǎn)。因?yàn)樵诿恳粋€(gè)配置紀(jì)元里面,每個(gè)具有投票權(quán)的主節(jié)點(diǎn)只能投一次票,所以如果有 N個(gè)主節(jié)點(diǎn)進(jìn)行投票,那么具有大于等于 N/2+1 張支持票的從節(jié)點(diǎn)只會(huì)有一個(gè),這確保了新的主節(jié)點(diǎn)只會(huì)有一個(gè)。
5)如果在一個(gè)配置紀(jì)元里面沒(méi)有從節(jié)點(diǎn)能收集到足夠多的支持票,那么集群進(jìn)入一個(gè)新的配置紀(jì)元,并再次進(jìn)行選舉,直到選出新的主節(jié)點(diǎn)為止。
這個(gè)選舉新主節(jié)點(diǎn)的方法和選舉領(lǐng)頭 Sentinel 的方法非常相似,因?yàn)閮烧叨际腔?Raft 算法的領(lǐng)頭選舉(leader election)方法來(lái)實(shí)現(xiàn)的。
28、如何保證集群在線擴(kuò)容的安全性?(Redis 集群要增加分片,槽的遷移怎么保證無(wú)損)
例如:集群已經(jīng)對(duì)外提供服務(wù),原來(lái)有3分片,準(zhǔn)備新增2個(gè)分片,怎么在不下線的情況下,無(wú)損的從原有的3個(gè)分片指派若干個(gè)槽給這2個(gè)分片?
Redis 使用了 ASK 錯(cuò)誤來(lái)保證在線擴(kuò)容的安全性。
在槽的遷移過(guò)程中若有客戶(hù)端訪問(wèn),依舊先訪問(wèn)源節(jié)點(diǎn),源節(jié)點(diǎn)會(huì)先在自己的數(shù)據(jù)庫(kù)里面査找指定的鍵,如果找到的話,就直接執(zhí)行客戶(hù)端發(fā)送的命令。
如果沒(méi)找到,說(shuō)明該鍵可能已經(jīng)被遷移到目標(biāo)節(jié)點(diǎn)了,源節(jié)點(diǎn)將向客戶(hù)端返回一個(gè) ASK 錯(cuò)誤,該錯(cuò)誤會(huì)指引客戶(hù)端轉(zhuǎn)向正在導(dǎo)入槽的目標(biāo)節(jié)點(diǎn),并再次發(fā)送之前想要執(zhí)行的命令,從而獲取到結(jié)果。
ASK錯(cuò)誤
在進(jìn)行重新分片期間,源節(jié)點(diǎn)向目標(biāo)節(jié)點(diǎn)遷移一個(gè)槽的過(guò)程中,可能會(huì)出現(xiàn)這樣一種情況:屬于被遷移槽的一部分鍵值對(duì)保存在源節(jié)點(diǎn)里面,而另一部分鍵值對(duì)則保存在目標(biāo)節(jié)點(diǎn)里面。
當(dāng)客戶(hù)端向源節(jié)點(diǎn)發(fā)送一個(gè)與數(shù)據(jù)庫(kù)鍵有關(guān)的命令,并且命令要處理的數(shù)據(jù)庫(kù)鍵恰好就屬于正在被遷移的槽時(shí)。源節(jié)點(diǎn)會(huì)先在自己的數(shù)據(jù)庫(kù)里面査找指定的鍵,如果找到的話,就直接執(zhí)行客戶(hù)端發(fā)送的命令。
否則,這個(gè)鍵有可能已經(jīng)被遷移到了目標(biāo)節(jié)點(diǎn),源節(jié)點(diǎn)將向客戶(hù)端返回一個(gè) ASK 錯(cuò)誤,指引客戶(hù)端轉(zhuǎn)向正在導(dǎo)入槽的目標(biāo)節(jié)點(diǎn),并再次發(fā)送之前想要執(zhí)行的命令,從而獲取到結(jié)果。
29、Redis 事務(wù)的實(shí)現(xiàn)
一個(gè)事務(wù)從開(kāi)始到結(jié)束通常會(huì)經(jīng)歷以下3個(gè)階段:
1)事務(wù)開(kāi)始:multi 命令將執(zhí)行該命令的客戶(hù)端從非事務(wù)狀態(tài)切換至事務(wù)狀態(tài),底層通過(guò) flags 屬性標(biāo)識(shí)。
2)命令入隊(duì):當(dāng)客戶(hù)端處于事務(wù)狀態(tài)時(shí),服務(wù)器會(huì)根據(jù)客戶(hù)端發(fā)來(lái)的命令執(zhí)行不同的操作:
exec、discard、watch、multi 命令會(huì)被立即執(zhí)行其他命令不會(huì)立即執(zhí)行,而是將命令放入到一個(gè)事務(wù)隊(duì)列,然后向客戶(hù)端返回 QUEUED 回復(fù)。
3)事務(wù)執(zhí)行:當(dāng)一個(gè)處于事務(wù)狀態(tài)的客戶(hù)端向服務(wù)器發(fā)送 exec 命令時(shí),服務(wù)器會(huì)遍歷事務(wù)隊(duì)列,執(zhí)行隊(duì)列中的所有命令,最后將結(jié)果全部返回給客戶(hù)端。
不過(guò) redis 的事務(wù)并不推薦在實(shí)際中使用,如果要使用事務(wù),推薦使用 Lua 腳本,redis 會(huì)保證一個(gè) Lua 腳本里的所有命令的原子性。
30、Redis 的 Java 客戶(hù)端有哪些?官方推薦哪個(gè)?
Redis 官網(wǎng)展示的 Java 客戶(hù)端如下圖所示,其中官方推薦的是標(biāo)星的3個(gè):Jedis、Redisson 和 lettuce。
31、Redis 里面有1億個(gè) key,其中有 10 個(gè) key 是包含 java,如何將它們?nèi)空页鰜?lái)?
1)keys *java* 命令,該命令性能很好,但是在數(shù)據(jù)量特別大的時(shí)候會(huì)有性能問(wèn)題
2)scan 0 MATCH *java* 命令,基于游標(biāo)的迭代器,更好的選擇
SCAN 命令是一個(gè)基于游標(biāo)的迭代器(cursor based iterator): SCAN 命令每次被調(diào)用之后, 都會(huì)向用戶(hù)返回一個(gè)新的游標(biāo), 用戶(hù)在下次迭代時(shí)需要使用這個(gè)新游標(biāo)作為 SCAN 命令的游標(biāo)參數(shù), 以此來(lái)延續(xù)之前的迭代過(guò)程。
當(dāng) SCAN 命令的游標(biāo)參數(shù)被設(shè)置為 0 時(shí), 服務(wù)器將開(kāi)始一次新的迭代, 而當(dāng)服務(wù)器向用戶(hù)返回值為 0 的游標(biāo)時(shí), 表示迭代已結(jié)束。
32、使用過(guò) Redis 做消息隊(duì)列么?
Redis 本身提供了一些組件來(lái)實(shí)現(xiàn)消息隊(duì)列的功能,但是多多少少都存在一些缺點(diǎn),相比于市面上成熟的消息隊(duì)列,例如 Kafka、Rocket MQ 來(lái)說(shuō)并沒(méi)有優(yōu)勢(shì),因此目前我們并沒(méi)有使用 Redis 來(lái)做消息隊(duì)列。
關(guān)于 Redis 做消息隊(duì)列的常見(jiàn)方案主要有以下:
1)Redis 5.0 之前可以使用 List(blocking)、Pub/Sub 等來(lái)實(shí)現(xiàn)輕量級(jí)的消息發(fā)布訂閱功能組件,但是這兩種實(shí)現(xiàn)方式都有很明顯的缺點(diǎn),兩者中相對(duì)完善的 Pub/Sub 的主要缺點(diǎn)就是消息無(wú)法持久化,如果出現(xiàn)網(wǎng)絡(luò)斷開(kāi)、Redis 宕機(jī)等,消息就會(huì)被丟棄。
2)為了解決 Pub/Sub 模式等的缺點(diǎn),Redis 在 5.0 引入了全新的 Stream,Stream 借鑒了很多 Kafka 的設(shè)計(jì)思想,有以下幾個(gè)特點(diǎn):
提供了消息的持久化和主備復(fù)制功能,可以讓任何客戶(hù)端訪問(wèn)任何時(shí)刻的數(shù)據(jù),并且能記住每一個(gè)客戶(hù)端的訪問(wèn)位置,還能保證消息不丟失。引入了消費(fèi)者組的概念,不同組接收到的數(shù)據(jù)完全一樣(前提是條件一樣),但是組內(nèi)的消費(fèi)者則是競(jìng)爭(zhēng)關(guān)系。
Redis Stream 相比于 pub/sub 已經(jīng)有很明顯的改善,但是相比于 Kafka,其實(shí)沒(méi)有優(yōu)勢(shì),同時(shí)存在:尚未經(jīng)過(guò)大量驗(yàn)證、成本較高、不支持分區(qū)(partition)、無(wú)法支持大規(guī)模數(shù)據(jù)等問(wèn)題。
33、Redis 和 Memcached 的比較
1)數(shù)據(jù)結(jié)構(gòu):memcached 支持簡(jiǎn)單的 key-value 數(shù)據(jù)結(jié)構(gòu),而 redis 支持豐富的數(shù)據(jù)結(jié)構(gòu):String、List、Set、Hash、SortedSet 等。
2)數(shù)據(jù)存儲(chǔ):memcached 和 redis 的數(shù)據(jù)都是全部在內(nèi)存中。
網(wǎng)上有一種說(shuō)法 “當(dāng)物理內(nèi)存用完時(shí),Redis可以將一些很久沒(méi)用到的 value 交換到磁盤(pán),同時(shí)在內(nèi)存中清除”,這邊指的是 redis 里的虛擬內(nèi)存(Virtual Memory)功能,該功能在 Redis 2.0 被引入,但是在 Redis 2.4 中被默認(rèn)關(guān)閉,并標(biāo)記為廢棄,而在后續(xù)版中被完全移除。
3)持久化:memcached 不支持持久化,redis 支持將數(shù)據(jù)持久化到磁盤(pán)
4)災(zāi)難恢復(fù):實(shí)例掛掉后,memcached 數(shù)據(jù)不可恢復(fù),redis 可通過(guò) RDB、AOF 恢復(fù),但是還是會(huì)有數(shù)據(jù)丟失問(wèn)題
5)事件庫(kù):memcached 使用 Libevent 事件庫(kù),redis 自己封裝了簡(jiǎn)易事件庫(kù) AeEvent
6)過(guò)期鍵刪除策略:memcached 使用惰性刪除,redis 使用惰性刪除+定期刪除
7)內(nèi)存驅(qū)逐(淘汰)策略:memcached 主要為 LRU 算法,redis 當(dāng)前支持8種淘汰策略,見(jiàn)本文第16題
8)性能比較
按“CPU 單核” 維度比較:由于 Redis 只使用單核,而 Memcached 可以使用多核,所以在比較上:在處理小數(shù)據(jù)時(shí),平均每一個(gè)核上 Redis 比 Memcached 性能更高,而在 100k 左右的大數(shù)據(jù)時(shí), Memcached 性能要高于 Redis。按“實(shí)例”維度進(jìn)行比較:由于 Memcached 多線程的特性,在 Redis 6.0 之前,通常情況下 Memcached 性能是要高于 Redis 的,同時(shí)實(shí)例的 CPU 核數(shù)越多,Memcached 的性能優(yōu)勢(shì)越大。至于網(wǎng)上說(shuō)的 redis 的性能比 memcached 快很多,這個(gè)說(shuō)法就離譜。
34、Redis 實(shí)現(xiàn)分布式鎖
1)加鎖
加鎖通常使用 set 命令來(lái)實(shí)現(xiàn),偽代碼如下:
set key value PX milliseconds NX
幾個(gè)參數(shù)的意義如下:
key、value:鍵值對(duì)
PX milliseconds:設(shè)置鍵的過(guò)期時(shí)間為 milliseconds 毫秒。
NX:只在鍵不存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。SET key value NX 效果等同于 SETNX key value。
PX、expireTime 參數(shù)則是用于解決沒(méi)有解鎖導(dǎo)致的死鎖問(wèn)題。因?yàn)槿绻麤](méi)有過(guò)期時(shí)間,萬(wàn)一程序員寫(xiě)的代碼有 bug 導(dǎo)致沒(méi)有解鎖操作,則就出現(xiàn)了死鎖,因此該參數(shù)起到了一個(gè)“兜底”的作用。
NX 參數(shù)用于保證在多個(gè)線程并發(fā) set 下,只會(huì)有1個(gè)線程成功,起到了鎖的“唯一”性。
2)解鎖
解鎖需要兩步操作:
1)查詢(xún)當(dāng)前“鎖”是否還是我們持有,因?yàn)榇嬖谶^(guò)期時(shí)間,所以可能等你想解鎖的時(shí)候,“鎖”已經(jīng)到期,然后被其他線程獲取了,所以我們?cè)诮怄i前需要先判斷自己是否還持有“鎖”
2)如果“鎖”還是我們持有,則執(zhí)行解鎖操作,也就是刪除該鍵值對(duì),并返回成功;否則,直接返回失敗。
由于當(dāng)前 Redis 還沒(méi)有原子命令直接支持這兩步操作,所以當(dāng)前通常是使用 Lua 腳本來(lái)執(zhí)行解鎖操作,Redis 會(huì)保證腳本里的內(nèi)容執(zhí)行是一個(gè)原子操作。
腳本代碼如下,邏輯比較簡(jiǎn)單:
public Object getData(String key) throws InterruptedException { Object value = redis.get(key); // 緩存值過(guò)期 if (value == null) { // lockRedis:專(zhuān)門(mén)用于加鎖的redis; // "empty":加鎖的值隨便設(shè)置都可以 if (lockRedis.set(key, "empty", "PX", lockExpire, "NX")) { try { // 查詢(xún)數(shù)據(jù)庫(kù),并寫(xiě)到緩存,讓其他線程可以直接走緩存 value = getDataFromDb(key); redis.set(key, value, "PX", expire); } catch (Exception e) { // 異常處理 } finally { // 釋放鎖 lockRedis.delete(key); } } else { // sleep50ms后,進(jìn)行重試 Thread.sleep(50); return getData(key); } } return value; }
兩個(gè)參數(shù)的意義如下:
KEYS[1]:我們要解鎖的 key
ARGV[1]:我們加鎖時(shí)的 value,用于判斷當(dāng)“鎖”是否還是我們持有,如果被其他線程持有了,value 就會(huì)發(fā)生變化。
上述方法是 Redis 當(dāng)前實(shí)現(xiàn)分布式鎖的主流方法,可能會(huì)有一些小優(yōu)區(qū)別,但是核心都是這個(gè)思路。看著好像沒(méi)啥毛病,但是真的是這個(gè)樣子嗎?讓我們繼續(xù)往下看。
35、Redis 分布式鎖過(guò)期了,還沒(méi)處理完怎么辦
為了防止死鎖,我們會(huì)給分布式鎖加一個(gè)過(guò)期時(shí)間,但是萬(wàn)一這個(gè)時(shí)間到了,我們業(yè)務(wù)邏輯還沒(méi)處理完,怎么辦?
首先,我們?cè)谠O(shè)置過(guò)期時(shí)間時(shí)要結(jié)合業(yè)務(wù)場(chǎng)景去考慮,盡量設(shè)置一個(gè)比較合理的值,就是理論上正常處理的話,在這個(gè)過(guò)期時(shí)間內(nèi)是一定能處理完畢的。
之后,我們?cè)賮?lái)考慮對(duì)這個(gè)問(wèn)題進(jìn)行兜底設(shè)計(jì)。
關(guān)于這個(gè)問(wèn)題,目前常見(jiàn)的解決方法有兩種:
守護(hù)線程“續(xù)命”:額外起一個(gè)線程,定期檢查線程是否還持有鎖,如果有則延長(zhǎng)過(guò)期時(shí)間。Redisson 里面就實(shí)現(xiàn)了這個(gè)方案,使用“看門(mén)狗”定期檢查(每1/3的鎖時(shí)間檢查1次),如果線程還持有鎖,則刷新過(guò)期時(shí)間。超時(shí)回滾:當(dāng)我們解鎖時(shí)發(fā)現(xiàn)鎖已經(jīng)被其他線程獲取了,說(shuō)明此時(shí)我們執(zhí)行的操作已經(jīng)是“不安全”的了,此時(shí)需要進(jìn)行回滾,并返回失敗。
同時(shí),需要進(jìn)行告警,人為介入驗(yàn)證數(shù)據(jù)的正確性,然后找出超時(shí)原因,是否需要對(duì)超時(shí)時(shí)間進(jìn)行優(yōu)化等等。
36、守護(hù)線程續(xù)命的方案有什么問(wèn)題嗎
Redisson 使用看門(mén)狗(守護(hù)線程)“續(xù)命”的方案在大多數(shù)場(chǎng)景下是挺不錯(cuò)的,也被廣泛應(yīng)用于生產(chǎn)環(huán)境,但是在極端情況下還是會(huì)存在問(wèn)題。
問(wèn)題例子如下:
線程1首先獲取鎖成功,將鍵值對(duì)寫(xiě)入 redis 的 master 節(jié)點(diǎn)在 redis 將該鍵值對(duì)同步到 slave 節(jié)點(diǎn)之前,master 發(fā)生了故障redis 觸發(fā)故障轉(zhuǎn)移,其中一個(gè) slave 升級(jí)為新的 master此時(shí)新的 master 并不包含線程1寫(xiě)入的鍵值對(duì),因此線程2嘗試獲取鎖也可以成功拿到鎖此時(shí)相當(dāng)于有兩個(gè)線程獲取到了鎖,可能會(huì)導(dǎo)致各種預(yù)期之外的情況發(fā)生,例如最常見(jiàn)的臟數(shù)據(jù)
解決方法:上述問(wèn)題的根本原因主要是由于 redis 異步復(fù)制帶來(lái)的數(shù)據(jù)不一致問(wèn)題導(dǎo)致的,因此解決的方向就是保證數(shù)據(jù)的一致。
當(dāng)前比較主流的解法和思路有兩種:
1)Redis 作者提出的 RedLock;2)Zookeeper 實(shí)現(xiàn)的分布式鎖。
37、RedLock
首先,該方案也是基于文章開(kāi)頭的那個(gè)方案(set加鎖、lua腳本解鎖)進(jìn)行改良的,所以 antirez 只描述了差異的地方,大致方案如下。
假設(shè)我們有 N 個(gè) Redis 主節(jié)點(diǎn),例如 N = 5,這些節(jié)點(diǎn)是完全獨(dú)立的,我們不使用復(fù)制或任何其他隱式協(xié)調(diào)系統(tǒng),為了取到鎖,客戶(hù)端應(yīng)該執(zhí)行以下操作:
獲取當(dāng)前時(shí)間,以毫秒為單位。依次嘗試從5個(gè)實(shí)例,使用相同的 key 和隨機(jī)值(例如UUID)獲取鎖。當(dāng)向Redis 請(qǐng)求獲取鎖時(shí),客戶(hù)端應(yīng)該設(shè)置一個(gè)超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間。例如你的鎖自動(dòng)失效時(shí)間為10秒,則超時(shí)時(shí)間應(yīng)該在 5-50 毫秒之間。這樣可以防止客戶(hù)端在試圖與一個(gè)宕機(jī)的 Redis 節(jié)點(diǎn)對(duì)話時(shí)長(zhǎng)時(shí)間處于阻塞狀態(tài)。如果一個(gè)實(shí)例不可用,客戶(hù)端應(yīng)該盡快嘗試去另外一個(gè)Redis實(shí)例請(qǐng)求獲取鎖??蛻?hù)端通過(guò)當(dāng)前時(shí)間減去步驟1記錄的時(shí)間來(lái)計(jì)算獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)從大多數(shù)(N/2+1,這里是3個(gè)節(jié)點(diǎn))的Redis節(jié)點(diǎn)都取到鎖,并且獲取鎖使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功。如果取到了鎖,其真正有效時(shí)間等于初始有效時(shí)間減去獲取鎖所使用的時(shí)間(步驟3計(jì)算的結(jié)果)。如果由于某些原因未能獲得鎖(無(wú)法在至少N/2+1個(gè)Redis實(shí)例獲取鎖、或獲取鎖的時(shí)間超過(guò)了有效時(shí)間),客戶(hù)端應(yīng)該在所有的Redis實(shí)例上進(jìn)行解鎖(即便某些Redis實(shí)例根本就沒(méi)有加鎖成功,防止某些節(jié)點(diǎn)獲取到鎖但是客戶(hù)端沒(méi)有得到響應(yīng)而導(dǎo)致接下來(lái)的一段時(shí)間不能被重新獲取鎖)。
可以看出,該方案為了解決數(shù)據(jù)不一致的問(wèn)題,直接舍棄了異步復(fù)制,只使用 master 節(jié)點(diǎn),同時(shí)由于舍棄了 slave,為了保證可用性,引入了 N 個(gè)節(jié)點(diǎn),官方建議是 5。
該方案看著挺美好的,但是實(shí)際上我所了解到的在實(shí)際生產(chǎn)上應(yīng)用的不多,主要有兩個(gè)原因:1)該方案的成本似乎有點(diǎn)高,需要使用5個(gè)實(shí)例;2)該方案一樣存在問(wèn)題。
該方案主要存以下問(wèn)題:
嚴(yán)重依賴(lài)系統(tǒng)時(shí)鐘。如果線程1從3個(gè)實(shí)例獲取到了鎖,但是這3個(gè)實(shí)例中的某個(gè)實(shí)例的系統(tǒng)時(shí)間走的稍微快一點(diǎn),則它持有的鎖會(huì)提前過(guò)期被釋放,當(dāng)他釋放后,此時(shí)又有3個(gè)實(shí)例是空閑的,則線程2也可以獲取到鎖,則可能出現(xiàn)兩個(gè)線程同時(shí)持有鎖了。如果線程1從3個(gè)實(shí)例獲取到了鎖,但是萬(wàn)一其中有1臺(tái)重啟了,則此時(shí)又有3個(gè)實(shí)例是空閑的,則線程2也可以獲取到鎖,此時(shí)又出現(xiàn)兩個(gè)線程同時(shí)持有鎖了。
針對(duì)以上問(wèn)題其實(shí)后續(xù)也有人給出一些相應(yīng)的解法,但是整體上來(lái)看還是不夠完美,所以目前實(shí)際應(yīng)用得不是那么多。
38、使用緩存時(shí),先操作數(shù)據(jù)庫(kù) or 先操作緩存
1)先操作數(shù)據(jù)庫(kù)
案例如下,有兩個(gè)并發(fā)的請(qǐng)求,一個(gè)寫(xiě)請(qǐng)求,一個(gè)讀請(qǐng)求,流程如下:
可能存在的臟數(shù)據(jù)時(shí)間范圍:更新數(shù)據(jù)庫(kù)后,失效緩存前。這個(gè)時(shí)間范圍很小,通常不會(huì)超過(guò)幾毫秒。
2)先操作緩存
案例如下,有兩個(gè)并發(fā)的請(qǐng)求,一個(gè)寫(xiě)請(qǐng)求,一個(gè)讀請(qǐng)求,流程如下:
可能存在的臟數(shù)據(jù)時(shí)間范圍:更新數(shù)據(jù)庫(kù)后,下一次對(duì)該數(shù)據(jù)的更新前。這個(gè)時(shí)間范圍不確定性很大,情況如下:
如果下一次對(duì)該數(shù)據(jù)的更新馬上就到來(lái),那么會(huì)失效緩存,臟數(shù)據(jù)的時(shí)間就很短。如果下一次對(duì)該數(shù)據(jù)的更新要很久才到來(lái),那這期間緩存保存的一直是臟數(shù)據(jù),時(shí)間范圍很長(zhǎng)。
結(jié)論:通過(guò)上述案例可以看出,先操作數(shù)據(jù)庫(kù)和先操作緩存都會(huì)存在臟數(shù)據(jù)的情況。但是相比之下,先操作數(shù)據(jù)庫(kù),再操作緩存是更優(yōu)的方式,即使在并發(fā)極端情況下,也只會(huì)出現(xiàn)很小量的臟數(shù)據(jù)。
39、為什么是讓緩存失效,而不是更新緩存
1)更新緩存
案例如下,有兩個(gè)并發(fā)的寫(xiě)請(qǐng)求,流程如下:
分析:數(shù)據(jù)庫(kù)中的數(shù)據(jù)是請(qǐng)求B的,緩存中的數(shù)據(jù)是請(qǐng)求A的,數(shù)據(jù)庫(kù)和緩存存在數(shù)據(jù)不一致。
2)失效(刪除)緩存
案例如下,有兩個(gè)并發(fā)的寫(xiě)請(qǐng)求,流程如下:
分析:由于是刪除緩存,所以不存在數(shù)據(jù)不一致的情況。
結(jié)論:通過(guò)上述案例,可以很明顯的看出,失效緩存是更優(yōu)的方式。
40、如何保證數(shù)據(jù)庫(kù)和緩存的數(shù)據(jù)一致性
在上文的案例中,無(wú)論是先操作數(shù)據(jù)庫(kù),還是先操作緩存,都會(huì)存在臟數(shù)據(jù)的情況,有辦法避免嗎?
答案是有的,由于數(shù)據(jù)庫(kù)和緩存是兩個(gè)不同的數(shù)據(jù)源,要保證其數(shù)據(jù)一致性,其實(shí)就是典型的分布式事務(wù)場(chǎng)景,可以引入分布式事務(wù)來(lái)解決,常見(jiàn)的有:2PC、TCC、MQ事務(wù)消息等。
但是引入分布式事務(wù)必然會(huì)帶來(lái)性能上的影響,這與我們當(dāng)初引入緩存來(lái)提升性能的目的是相違背的。
所以在實(shí)際使用中,通常不會(huì)去保證緩存和數(shù)據(jù)庫(kù)的強(qiáng)一致性,而是做出一定的犧牲,保證兩者數(shù)據(jù)的最終一致性。
如果是實(shí)在無(wú)法接受臟數(shù)據(jù)的場(chǎng)景,則比較合理的方式是放棄使用緩存,直接走數(shù)據(jù)庫(kù)。
保證數(shù)據(jù)庫(kù)和緩存數(shù)據(jù)最終一致性的常用方案如下:
1)更新數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)產(chǎn)生 binlog。
2)監(jiān)聽(tīng)和消費(fèi) binlog,執(zhí)行失效緩存操作。
3)如果步驟2失效緩存失敗,則引入重試機(jī)制,將失敗的數(shù)據(jù)通過(guò)MQ方式進(jìn)行重試,同時(shí)考慮是否需要引入冪等機(jī)制。
兜底:當(dāng)出現(xiàn)未知的問(wèn)題時(shí),及時(shí)告警通知,人為介入處理。
人為介入是終極大法,那些外表看著光鮮艷麗的應(yīng)用,其背后大多有一群苦逼的程序員,在不斷的修復(fù)各種臟數(shù)據(jù)和bug。
41、緩存穿透
描述:訪問(wèn)一個(gè)緩存和數(shù)據(jù)庫(kù)都不存在的 key,此時(shí)會(huì)直接打到數(shù)據(jù)庫(kù)上,并且查不到數(shù)據(jù),沒(méi)法寫(xiě)緩存,所以下一次同樣會(huì)打到數(shù)據(jù)庫(kù)上。
此時(shí),緩存起不到作用,請(qǐng)求每次都會(huì)走到數(shù)據(jù)庫(kù),流量大時(shí)數(shù)據(jù)庫(kù)可能會(huì)被打掛。此時(shí)緩存就好像被“穿透”了一樣,起不到任何作用。
解決方案:
1)接口校驗(yàn)。在正常業(yè)務(wù)流程中可能會(huì)存在少量訪問(wèn)不存在 key 的情況,但是一般不會(huì)出現(xiàn)大量的情況,所以這種場(chǎng)景最大的可能性是遭受了非法攻擊。可以在最外層先做一層校驗(yàn):用戶(hù)鑒權(quán)、數(shù)據(jù)合法性校驗(yàn)等,例如商品查詢(xún)中,商品的ID是正整數(shù),則可以直接對(duì)非正整數(shù)直接過(guò)濾等等。
2)緩存空值。當(dāng)訪問(wèn)緩存和DB都沒(méi)有查詢(xún)到值時(shí),可以將空值寫(xiě)進(jìn)緩存,但是設(shè)置較短的過(guò)期時(shí)間,該時(shí)間需要根據(jù)產(chǎn)品業(yè)務(wù)特性來(lái)設(shè)置。
3)布隆過(guò)濾器。使用布隆過(guò)濾器存儲(chǔ)所有可能訪問(wèn)的 key,不存在的 key 直接被過(guò)濾,存在的 key 則再進(jìn)一步查詢(xún)緩存和數(shù)據(jù)庫(kù)。
42、布隆過(guò)濾器
布隆過(guò)濾器的特點(diǎn)是判斷不存在的,則一定不存在;判斷存在的,大概率存在,但也有小概率不存在。并且這個(gè)概率是可控的,我們可以讓這個(gè)概率變小或者變高,取決于用戶(hù)本身的需求。
布隆過(guò)濾器由一個(gè) bitSet 和 一組 Hash 函數(shù)(算法)組成,是一種空間效率極高的概率型算法和數(shù)據(jù)結(jié)構(gòu),主要用來(lái)判斷一個(gè)元素是否在集合中存在。
在初始化時(shí),bitSet 的每一位被初始化為0,同時(shí)會(huì)定義 Hash 函數(shù),例如有3組 Hash 函數(shù):hash1、hash2、hash3。
寫(xiě)入流程
當(dāng)我們要寫(xiě)入一個(gè)值時(shí),過(guò)程如下,以“jionghui”為例:
1)首先將“jionghui”跟3組 Hash 函數(shù)分別計(jì)算,得到 bitSet 的下標(biāo)為:1、7、10。
2)將 bitSet 的這3個(gè)下標(biāo)標(biāo)記為1。
假設(shè)我們還有另外兩個(gè)值:java 和 diaosi,按上面的流程跟 3組 Hash 函數(shù)分別計(jì)算,結(jié)果如下:
java:Hash 函數(shù)計(jì)算 bitSet 下標(biāo)為:1、7、11
diaosi:Hash 函數(shù)計(jì)算 bitSet 下標(biāo)為:4、10、11
查詢(xún)流程
當(dāng)我們要查詢(xún)一個(gè)值時(shí),過(guò)程如下,同樣以“jionghui”為例::
1)首先將“jionghui”跟3組 Hash 函數(shù)分別計(jì)算,得到 bitSet 的下標(biāo)為:1、7、10。
2)查看 bitSet 的這3個(gè)下標(biāo)是否都為1,如果這3個(gè)下標(biāo)不都為1,則說(shuō)明該值必然不存在,如果這3個(gè)下標(biāo)都為1,則只能說(shuō)明可能存在,并不能說(shuō)明一定存在。
其實(shí)上圖的例子已經(jīng)說(shuō)明了這個(gè)問(wèn)題了,當(dāng)我們只有值“jionghui”和“diaosi”時(shí),bitSet 下標(biāo)為1的有:1、4、7、10、11。
當(dāng)我們又加入值“java”時(shí),bitSet 下標(biāo)為1的還是這5個(gè),所以當(dāng) bitSet 下標(biāo)為1的為:1、4、7、10、11 時(shí),我們無(wú)法判斷值“java”存不存在。
其根本原因是,不同的值在跟 Hash 函數(shù)計(jì)算后,可能會(huì)得到相同的下標(biāo),所以某個(gè)值的標(biāo)記位,可能會(huì)被其他值給標(biāo)上了。
這也是為啥布隆過(guò)濾器只能判斷某個(gè)值可能存在,無(wú)法判斷必然存在的原因。但是反過(guò)來(lái),如果該值根據(jù) Hash 函數(shù)計(jì)算的標(biāo)記位沒(méi)有全部都為1,那么則說(shuō)明必然不存在,這個(gè)是肯定的。
降低這種誤判率的思路也比較簡(jiǎn)單:
一個(gè)是加大 bitSet 的長(zhǎng)度,這樣不同的值出現(xiàn)“沖突”的概率就降低了,從而誤判率也降低。提升 Hash 函數(shù)的個(gè)數(shù),Hash 函數(shù)越多,每個(gè)值對(duì)應(yīng)的 bit 越多,從而誤判率也降低。
布隆過(guò)濾器的誤判率還有專(zhuān)門(mén)的推導(dǎo)公式,有興趣的可以去搜相關(guān)的文章和論文查看。
43、緩存擊穿
描述:某一個(gè)熱點(diǎn) key,在緩存過(guò)期的一瞬間,同時(shí)有大量的請(qǐng)求打進(jìn)來(lái),由于此時(shí)緩存過(guò)期了,所以請(qǐng)求最終都會(huì)走到數(shù)據(jù)庫(kù),造成瞬時(shí)數(shù)據(jù)庫(kù)請(qǐng)求量大、壓力驟增,甚至可能打垮數(shù)據(jù)庫(kù)。
解決方案:
1)加互斥鎖。在并發(fā)的多個(gè)請(qǐng)求中,只有第一個(gè)請(qǐng)求線程能拿到鎖并執(zhí)行數(shù)據(jù)庫(kù)查詢(xún)操作,其他的線程拿不到鎖就阻塞等著,等到第一個(gè)線程將數(shù)據(jù)寫(xiě)入緩存后,直接走緩存。
關(guān)于互斥鎖的選擇,網(wǎng)上看到的大部分文章都是選擇 Redis 分布式鎖(可以參考我之前的文章:面試必問(wèn)的分布式鎖,你懂了嗎?),因?yàn)檫@個(gè)可以保證只有一個(gè)請(qǐng)求會(huì)走到數(shù)據(jù)庫(kù),這是一種思路。
但是其實(shí)仔細(xì)想想的話,這邊其實(shí)沒(méi)有必要保證只有一個(gè)請(qǐng)求走到數(shù)據(jù)庫(kù),只要保證走到數(shù)據(jù)庫(kù)的請(qǐng)求能大大降低即可,所以還有另一個(gè)思路是 JVM 鎖。
JVM 鎖保證了在單臺(tái)服務(wù)器上只有一個(gè)請(qǐng)求走到數(shù)據(jù)庫(kù),通常來(lái)說(shuō)已經(jīng)足夠保證數(shù)據(jù)庫(kù)的壓力大大降低,同時(shí)在性能上比分布式鎖更好。
需要注意的是,無(wú)論是使用“分布式鎖”,還是“JVM 鎖”,加鎖時(shí)要按 key 維度去加鎖。
我看網(wǎng)上很多文章都是使用一個(gè)“固定的 key”加鎖,這樣會(huì)導(dǎo)致不同的 key 之間也會(huì)互相阻塞,造成性能?chē)?yán)重?fù)p耗。
使用 redis 分布式鎖的偽代碼,僅供參考:
public Object getData(String key) throws InterruptedException { Object value = redis.get(key); // 緩存值過(guò)期 if (value == null) { // lockRedis:專(zhuān)門(mén)用于加鎖的redis; // "empty":加鎖的值隨便設(shè)置都可以 if (lockRedis.set(key, "empty", "PX", lockExpire, "NX")) { try { // 查詢(xún)數(shù)據(jù)庫(kù),并寫(xiě)到緩存,讓其他線程可以直接走緩存 value = getDataFromDb(key); redis.set(key, value, "PX", expire); } catch (Exception e) { // 異常處理 } finally { // 釋放鎖 lockRedis.delete(key); } } else { // sleep50ms后,進(jìn)行重試 Thread.sleep(50); return getData(key); } } return value; }
2)熱點(diǎn)數(shù)據(jù)不過(guò)期。直接將緩存設(shè)置為不過(guò)期,然后由定時(shí)任務(wù)去異步加載數(shù)據(jù),更新緩存。
這種方式適用于比較極端的場(chǎng)景,例如流量特別特別大的場(chǎng)景,使用時(shí)需要考慮業(yè)務(wù)能接受數(shù)據(jù)不一致的時(shí)間,還有就是異常情況的處理,不要到時(shí)候緩存刷新不上,一直是臟數(shù)據(jù),那就涼了。
44、緩存雪崩
描述:大量的熱點(diǎn) key 設(shè)置了相同的過(guò)期時(shí)間,導(dǎo)在緩存在同一時(shí)刻全部失效,造成瞬時(shí)數(shù)據(jù)庫(kù)請(qǐng)求量大、壓力驟增,引起雪崩,甚至導(dǎo)致數(shù)據(jù)庫(kù)被打掛。
緩存雪崩其實(shí)有點(diǎn)像“升級(jí)版的緩存擊穿”,緩存擊穿是一個(gè)熱點(diǎn) key,緩存雪崩是一組熱點(diǎn) key。
解決方案:
1)過(guò)期時(shí)間打散。既然是大量緩存集中失效,那最容易想到就是讓他們不集中生效??梢越o緩存的過(guò)期時(shí)間時(shí)加上一個(gè)隨機(jī)值時(shí)間,使得每個(gè) key 的過(guò)期時(shí)間分布開(kāi)來(lái),不會(huì)集中在同一時(shí)刻失效。
2)熱點(diǎn)數(shù)據(jù)不過(guò)期。該方式和緩存擊穿一樣,也是要著重考慮刷新的時(shí)間間隔和數(shù)據(jù)異常如何處理的情況。
3)加互斥鎖。該方式和緩存擊穿一樣,按 key 維度加鎖,對(duì)于同一個(gè) key,只允許一個(gè)線程去計(jì)算,其他線程原地阻塞等待第一個(gè)線程的計(jì)算結(jié)果,然后直接走緩存即可。
最后
恭喜你老哥,能看到這邊你已經(jīng)超越了不少人了,文中有些題目還是有點(diǎn)深度的,但是如能掌握相信定能助你在對(duì)線大廠面試官時(shí)不落下風(fēng),建議收藏反復(fù)閱讀。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java實(shí)現(xiàn)獲取內(nèi)網(wǎng)的所有IP地址
這篇文章主要介紹了如何利用Java語(yǔ)言實(shí)現(xiàn)獲取內(nèi)網(wǎng)的所有IP地址,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)有一定的參考價(jià)值,快跟隨小編一起學(xué)習(xí)一下吧2022-06-06詳解Java8如何使用Lambda表達(dá)式進(jìn)行比較
Lambda表達(dá)式,也可稱(chēng)為閉包,是java8的新特性,作用是取代大部分內(nèi)部類(lèi),優(yōu)化java代碼結(jié)構(gòu),讓代碼變得更加簡(jiǎn)潔緊湊。本文將利用Lambda表達(dá)式進(jìn)行排序比較,需要的可以參考一下2022-01-01利用SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的兩種方式總結(jié)
關(guān)于動(dòng)態(tài)數(shù)據(jù)源的切換的方案有很多,核心只有兩種,一種是構(gòu)建多套環(huán)境,另一種是基于spring原生的AbstractRoutingDataSource切換,這篇文章主要給大家介紹了關(guān)于利用SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的兩種方式,需要的朋友可以參考下2021-10-10springboot2.0配置連接池(hikari、druid)的方法
springboot 2.0 默認(rèn)連接池就是Hikari了,直接在配置文件中輸入配置就可以了,本文通過(guò)實(shí)例代碼給大家介紹了springboot2.0配置連接池(hikari、druid)的方法,感興趣的朋友一起看看吧2021-12-12SpringBoot整合mybatisplus和druid的示例詳解
這篇文章主要介紹了SpringBoot整合mybatisplus和druid的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08IntelliJ IDEA配置Tomcat(完整版圖文教程)
這篇文章主要介紹了IntelliJ IDEA配置Tomcat(完整版圖文教程),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05tk-mybatis整合springBoot使用兩個(gè)數(shù)據(jù)源的方法
單純的使用mybaits進(jìn)行多數(shù)據(jù)配置網(wǎng)上資料很多,但是關(guān)于tk-mybaits多數(shù)據(jù)源配置沒(méi)有相關(guān)材料,本文就詳細(xì)的介紹一下如何使用,感興趣的可以了解一下2021-12-12