2023年郑州春招3年开发面试总结
MySQL必備知識
MySQL索引結構
介紹B樹結構
首先,常規的數據庫存儲引擎,一般都是采用 B 樹或者 B+樹來實現索引的存儲。 因為 B 樹是一種多路平衡樹,用這種存儲結構來存儲大量數據,它的整個高度 會相比二叉樹來說,會矮很多。 而對于數據庫來說,所有的數據必然都是存儲在磁盤上的,而磁盤 IO 的效率實際上是很低的,特別是在隨機磁盤 IO 的情況下效率更低。 所以樹的高度能夠決定磁盤 IO 的次數,磁盤 IO 次數越少,對于性能的提升就越大,這也是為什么采用 B 樹作為索引存儲結構的原因,但是在 Mysql 的 InnoDB 存儲引擎里面,它用了一種增強的 B 樹結構,也就是 B+樹來作為索引和數據的存儲結構。
B+樹相比較與B樹優化
B+樹的所有數據都存儲在葉子節點,非葉子節點只存儲索引
1 B+樹非葉子節點不存儲數據,所以每一層能夠存儲的索引數量會增加,意味著 B+樹在層高相同的情況下存儲的數據量要比 B 樹要多,使得磁盤 IO 次數更少。
2 在 Mysql 里面,范圍查詢是一個比較常用的操作,而 B+樹的所有存儲在葉子節點的數據使用了雙向鏈表來關聯,所以在查詢的時候只需查兩個節點進行遍歷就行,而 B 樹需要獲取所有節點,所以 B+樹在范圍查詢上效率更高。
3 基于 B+樹這樣一種結構,如果采用自增的整型數據作為主鍵,還能更好 的避免增加數據的時候,帶來葉子節點分裂導致的大量運算的問題。
為什么不用二叉樹
二叉樹的話有一個問題,隨著數據量越來越大,它會發生傾斜,就是斜樹,不管是左傾斜還是右傾斜,編程一個線性表,樹會退化成一個相對平坦的線性表結構。
為什么不用紅黑樹
紅黑樹是一顆平衡二叉樹,數據量大的時候,樹的深度也很深,如果樹的深度有20層,而查找的數據在葉子節點,就要進行20次IO操作,性能低。
b樹和b+樹區別
節點存放數據的區別,B+樹葉子結點,飛葉子節點,一個存放行記錄具體數據,一個存放索引;
B樹每個節點都會存放真實數據的,每頁的數據量比較少,而且會造成一種現象,查詢數據的時候,跟磁盤交互更多更頻繁。
事務特性和隔離級別
首先,A 表示 Atomic 原子性,也就是需要保證多個 DML 操作是原子的,要么都成功,要么都失敗。
那么,失敗就意味著要對原本執行成功的數據進行回滾,所以 InnoDB 設計了一 個 UNDO_LOG 表,在事務執行的過程中,把修改之前的數據快照保存到
UNDO_LOG 里面,一旦出現錯誤,就直接從 UNDO_LOG 里面讀取數據執行反 向操作就行了。
其次,C 表示一致性,表示數據的完整性約束沒有被破壞,這個更多是依賴于業 務層面的保證,數據庫本身也提供了一些,比如主鍵的唯一約束,字段長度和類型的保證等等。
接著,I 表示事務的隔離性,也就是多個并行事務對同一個數據進行操作的時候,如何避免多個事務的干擾導致數據混亂的問題。
而 InnoDB 提供了四種隔離級別的實現
RU(未提交讀)
RC(已提交讀)
RR(可重復讀)
Serializable(串行化)
InnoDB 默認的隔離級別是 RR(可重復讀),然后使用了 MVCC 機制解決了臟讀和不可重復讀的問題,然后使用了行鎖/表鎖的方式解決了幻讀的問題。
最后一個是 D,表示持久性,也就是只要事務提交成功,那對于這個數據的結果的影響一定是永久性的。
理論上來說,事務提交之后直接把數據持久化到磁盤就行了,但是因為隨機磁盤 IO 的效率確實很低,所以 InnoDB 設計了 Buffer Pool 緩沖區來優化,也就是數據發生變更的時候先更新內存緩沖區,然后在合適的時機再持久化到磁盤。 那在持久化這個過程中,如果數據庫宕機,就會導致數據丟失,也就無法滿足持久性了,所以 InnoDB 引入了 Redo_LOG 文件,這個文件存儲了數據被修改之后的值, 當我們通過事務對數據進行變更操作的時候,除了修改內存緩沖區里面的數據以外,還會把本次修改的值追加到 REDO_LOG 里面。
當提交事務的時候,直接把 REDO_LOG 日志刷到磁盤上持久化,一旦數據庫出 現宕機,在 Mysql 重啟在以后可以直接用 REDO_LOG 里面保存的重寫日志讀 取出來,再執行一遍從而保證持久性。
MyISAM與InnoDB區別
理解:不同數據文件在磁盤的不同組織形式
事務處理:InnoDB支持事務,MyISAM不支持;我們業務一般都是需要可靠性要求的,基本上用的都是InnoDB,而MyISAM存儲引擎適用于讀多寫少場景,類似于博客系統,
InnoDB是聚集索引,使用B+Tree作為索引結構,數據文件是和(主鍵)索引綁在一起的(表數據文件本身就是按B+Tree組織的一個索引結構) MyISAM是非聚集索引,也是使用B+Tree作為索引結構,索引和數據文件是分離的,索引保存的是數據文件的指針。
外鍵的支持:innodb有外鍵,myisam沒有;
延伸
為什么只讀場景myisam比innodb快
查詢的時候,由于innodb支持事務,所以會有mvcc的一個比較。這個過程會損耗性能。
查詢的時候,如果走了索引,而索引又不是主鍵索引,此時,由于innodb是聚簇索引,會有一個回表的過程,即:先去非聚簇索引樹(非主鍵索引樹)中查詢數據,找到數據對應的key之后,再通過key回表到聚簇索引樹,最后找到需要的數據。而myisam是非聚集索引,而且葉子節點存儲的是磁盤地址,所以,查詢的時候查到的最后結果不是聚簇索引樹的key,而是會直接去查詢磁盤。
其次,鎖的一個損耗,innodb鎖支持行鎖,在檢查鎖的時候不僅檢查表鎖,還要看行鎖。
常見的事務有哪些問題
臟讀,不可重復讀,幻讀
臟讀:臟讀就是指當一個事務正在訪問數據,并且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務也訪問這個數據,然后使用了這個數據。
不可重復讀:是指在一個事務內,多次讀同一數據。在這個事務還沒有結束時,另外一個事務也訪問該同一數據。那么,在第一個事務中的兩次讀數據之間,由于第二個事務的修改,那么第一個事務兩次讀到的的數據可能是不一樣的。這樣就發生了在一個事務內兩次讀到的數據是不一樣的,因此稱為是不可重復讀。
幻讀:是指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的數據進行了修改,這種修改涉及到表中的全部數據行。同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。那么,以后就會發生操作第一個事務的用戶發現表中還有沒有修改的數據行,就好象發生了幻覺一樣。
buffer pool如何理解
buffer pool是個緩沖池,在進行數據更新時候,MySQL 不會直接去修改磁盤的數據,因為這樣做太慢了,MySQL 會先改內存,然后記錄 redo log,等有空了再刷磁盤,如果內存里沒有數據,就去磁盤 load;而這些數據存放的地方,就是 Buffer Pool。
我們平時開發時,會用 redis 來做緩存,緩解數據庫壓力,其實 MySQL 自己也做了一層類似緩存的東西。
MySQL 是以「頁」(page)為單位從磁盤讀取數據的,Buffer Pool 里的數據也是如此,實際上,Buffer Pool 是a linked list of pages,一個以頁為元素的鏈表。
談談對MySQL日志的理解
binlog用于記錄數據庫執行的寫入性操作,以二進制的形式保存在磁盤中。binlog 是 mysql的邏輯日志(可以理解為是sql語句的二進制存儲),并且由 Server 層進行記錄,使用任何存儲引擎的 mysql 數據庫都會記錄 binlog 日志。在實際應用中, binlog 的主要使用場景有兩個,分別是 主從復制 和 數據恢復 。
? redo log 包括兩部分:一個是內存中的日志緩沖( redo log buffer ),另一個是磁盤上的日志文件( redo log file )。mysql 每執行一條 DML 語句,先將記錄寫入 redo log buffer,后續某個時間點再一次性將多個操作記錄寫到 redo log file 。這種 先寫日志,再寫磁盤 的技術就是 MySQL里經常說到的 WAL(Write-Ahead Loggin``g) 技術。
? 數據庫事務四大特性中有一個是 原子性 ,具體來說就是 原子性是指對數據庫的一系列操作,要么全部成功,要么全部失敗,不可能出現部分成功的情況。實際上, 原子性 底層就是通過 undo log 實現的。undo log 主要記錄了數據的邏輯變化,比如一條 INSERT 語句,對應一條 DELETE 的 undo log ,對于每個 UPDATE 語句,對應一條相反的 UPDATE 的 undo log ,這樣在發生錯誤時,就能回滾到事務之前的數據狀態。
說說行鎖與表鎖
行鎖:操作時只鎖某一(些)行,不對其它行有影響。開銷大,加鎖慢;會出現死鎖;鎖定粒度小,發生鎖沖突的概率低,并發度高。
表鎖:即使操作一條記錄也會鎖住整個表。開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突概率高,并發度最低。
頁鎖:操作時鎖住一頁數據(16kb)。開銷和加鎖速度介于表鎖和行鎖之間;會出現死鎖;鎖定粒度介于表鎖和行鎖之間,并發度一般。
InnoDB 有行鎖和表鎖,MyIsam 只有表鎖。
說說讀鎖和寫鎖
從數據操作的類型劃分,可以分為讀鎖和寫鎖,讀鎖也叫共享鎖(S),寫鎖也叫排他鎖(X)
讀鎖:針對同一份數據,多個事務的讀操作可以同時進行互不影響,相互不阻塞
寫鎖:也叫排他鎖,英文為(X),針對同一份數據,只能有一個事務(事務A)進行操作,其他事務(事務B)阻塞,不能讀,也不能寫,只有等事務A執行完畢,事務B等其他事務才能進行操作。
在Innodb中,讀鎖和寫鎖可以加載表上,也可以加在行上(行鎖和表鎖)
數據庫鎖,到底鎖的是什么
Record Lock表示記錄鎖,鎖的是索引記錄。
Record Lock,翻譯成記錄鎖,是加在索引記錄上的鎖。例如,SELECT c1 FROM t WHERE c1 = 10 For UPDATE;會對c1=10這條記錄加鎖,為了防止任何其他事務插入、更新或刪除c1值為10的行。
需要特別注意的是,記錄鎖鎖定的是索引記錄。即使表沒有定義索引,InnoDB也會創建一個隱藏的聚集索引,并使用這個索引來鎖定記錄。
Gap Lock是間隙鎖,說的是索引記錄之間的間隙。
Gap指的是InnoDB的索引數據結構中可以插入新值的位置。當你用語句SELECT…FOR UPDATE鎖定一組行時。InnoDB可以創建鎖,應用于索引中的實際值以及他們之間的間隙。例如,如果選擇所有大于10的值進行更新,間隙鎖將阻止另一個事務插入大于10的新值。
Next-Key Lock是Record Lock和Gap Lock的組合,同時鎖索引記錄和間隙。他的范圍是左開右閉的。
這三種鎖都是自動添加的。
InnoDB的RR級別中,加鎖的基本單位是 next-key lock,只要掃描到的數據都會加鎖。唯一索引上的范圍查詢會訪問到不滿足條件的第一個值為止。
MVCC機制如何理解
簡單來說,MVCC就是存儲了同一條數據的不同歷史版本鏈,不同事務可以訪問不同的數據版本。
相關的概念
1、事務版本號
事務每次開啟時,都會從數據庫獲得一個自增長的事務ID,可以從事務ID判斷事務的執行先后順序。這就是事務版本號。
也就是每當begin的時候,首選要做的就是從數據庫獲得一個自增長的事務ID,它也就是當前事務的事務ID。
2、隱藏字段
對于InnoDB存儲引擎,每一行記錄都有兩個隱藏列trx_id、roll_pointer,如果數據表中存在主鍵或者非NULL的UNIQUE鍵時不會創建row_id,否則InnoDB會自動生成單調遞增的隱藏主鍵row_id。
| row_id | 否 | 單調遞增的行ID,不是必需的,占用6個字節。 這個跟MVCC關系不大 |
| trx_id | 是 | 記錄操作該行數據事務的事務ID |
| roll_pointer | 是 | 回滾指針,指向當前記錄行的undo log信息 |
這里的記錄操作,指的是insert|update|delete。對于delete操作而已,InnoDB認為是一個update操作,不過會更新一個另外的刪除位,將行表示為deleted,并非真正刪除。
3、undo log
undo log可以理解成回滾日志,它存儲的是老版本數據。在表記錄修改之前,會先把原始數據拷貝到undo log里,如果事務回滾,即可以通過undo log來還原數據?;蛘呷绻斍坝涗浶胁豢梢?#xff0c;可以順著undo log鏈找到滿足其可見性條件的記錄行版本。
在insert/update/delete(本質也是做更新,只是更新一個特殊的刪除位字段)操作時,都會產生undo log。
在InnoDB里,undo log分為如下兩類:
1)insert undo log : 事務對insert新記錄時產生的undo log, 只在事務回滾時需要, 并且在事務提交后就可以立即丟棄。
2)update undo log : 事務對記錄進行delete和update操作時產生的undo log,不僅在事務回滾時需要,快照讀也需要,只有當數據庫所使用的快照中不涉及該日志記錄,對應的回滾日志才會被刪除。
undo log有什么用途呢?
1、事務回滾時,保證原子性和一致性。
2、如果當前記錄行不可見,可以順著undo log鏈找到滿足其可見性條件的記錄行版本(用于MVCC快照讀)。
4、版本鏈
多個事務并行操作某一行數據時,不同事務對該行數據的修改會產生多個版本,然后通過回滾指針(roll_pointer),連成一個鏈表,這個鏈表就稱為版本鏈。如下:
5、快照讀和當前讀
快照讀: 讀取的是記錄數據的可見版本(有舊的版本)。不加鎖,普通的select語句都是快照讀,如:
select * from user where id = 1;當前讀:讀取的是記錄數據的最新版本,顯式加鎖的都是當前讀
select * from user where id = 1 for update; select * from user where id = 1 lock in share mode;6、ReadView
ReadView是事務在進行快照讀的時候生成的記錄快照, 可以幫助我們解決可見性問題的
如果一個事務要查詢行記錄,需要讀取哪個版本的行記錄呢? ReadView 就是來解決這個問題的。 ReadView 保存了當前事務開啟時所有活躍的事務列表。換個角度,可以理解為: ReadView 保存了不應該讓這個事務看到的其他事務 ID 列表。
ReadView是如何保證可見性判斷的呢?我們先看看 ReadView 的幾個重要屬性
- trx_ids: 當前系統中那些活躍(未提交)的讀寫事務ID, 它數據結構為一個List。(重點注意:這里的trx_ids中的活躍事務,不包括當前事務自己和已提交的事務,這點非常重要)
- low_limit_id: 目前出現過的最大的事務ID+1,即下一個將被分配的事務ID。
- up_limit_id: 活躍事務列表trx_ids中最小的事務ID,如果trx_ids為空,則up_limit_id 為 low_limit_id。
- creator_trx_id: 表示生成該 ReadView 的事務的事務id
訪問某條記錄的時候如何判斷該記錄是否可見,具體規則如下:
- 如果被訪問版本的 事務ID = creator_trx_id,那么表示當前事務訪問的是自己修改過的記錄,那么該版本對當前事務可見;
- 如果被訪問版本的 事務ID < up_limit_id,那么表示生成該版本的事務在當前事務生成 ReadView 前已經提交,所以該版本可以被當前事務訪問。
- 如果被訪問版本的 事務ID > low_limit_id 值,那么表示生成該版本的事務在當前事務生成 ReadView 后才開啟,所以該版本不可以被當前事務訪問。
- 如果被訪問版本的 事務ID在 up_limit_id和m_low_limit_id 之間,那就需要判斷一下版本的事務ID是不是在 trx_ids 列表中,如果在,說明創建 ReadView 時生成該版本的事務還是活躍的,該版本不可以被訪問;
如果不在,說明創建 ReadView 時生成該版本的事務已經被提交,該版本可以被訪問。
畫張圖來理解下
這里需要思考的一個問題就是 何時創建ReadView?
上面說過,ReadView是來解決一個事務需要讀取哪個版本的行記錄的問題的。那么說明什么?只有在select的時候才會創建ReadView。但在不同的隔離級別是有區別的:
在RC隔離級別下,是每個select都會創建最新的ReadView;而在RR隔離級別下,則是當事務中的第一個select請求才創建ReadView(下面會詳細舉例說明)。
那insert/update/delete操作呢?
這樣操作不會創建ReadView。但是這些操作在事務開啟(begin)且其未提交的時候,那么它的事務ID,會存在在其它存在查詢事務的ReadView記錄中,也就是trx_ids中。
查詢流程
InnoDB 實現MVCC,是通過ReadView+ Undo Log 實現的,Undo Log 保存了歷史快照,ReadView可見性規則幫助判斷當前版本的數據是否可見。
總結
簡單來說,MVCC就是存儲了同一條數據的不同歷史版本鏈,不同事務可以訪問不同的數據版本。
1 事務版本號:也就是每當begin的時候,首選要做的就是從數據庫獲得一個自增長的事務ID,它也就是當前事務的事務ID。回滾指針roll_pointer:指向當前記錄行的undo log信息
2 版本鏈:多個事務并行操作某一行數據時,不同事務對該行數據的修改會產生多個版本,然后通過回滾指針(roll_pointer),連成一個鏈表,這個鏈表就稱為版本鏈
3 快照讀:讀取的是記錄數據的可見版本(有舊的版本)。不加鎖,普通的select語句都是快照讀;當前讀:讀取的是記錄數據的最新版本,顯式加鎖的都是當前讀
4 ReadView:是事務在進行快照讀的時候生成的記錄快照, 可以幫助我們解決可見性問題的
5 undo log:保存了歷史快照
流程
什么是回表查詢
InnoDB 中,對于主鍵索引,只需要走一遍主鍵索引的查詢就能在葉子節點拿到數據。
而對于普通索引,葉子節點不存儲行記錄,無法直接定位行記錄,需要掃描兩次索引樹,先定位主鍵值,通過主鍵索引找到行記錄,再定位行記錄。
什么是索引下推
概念:索引下推是把本應該在 server 層進行篩選的條件,下推到存儲引擎層來進行篩選判斷,這樣能有效減少回表。
舉例說明:
首先使用聯合索引(name,age),現在有這樣一個查詢語句:
select * from t_user where name like 'L%' and age = 17;這條語句從最左匹配原則上來說是不符合的,原因在于只有name用的索引,但是age并沒有用到。
不用索引下推的執行過程:
第一步:利用索引找出name帶'L'的數據行:LiLei、Lili、Lisa、Lucy 這四條索引數據 第二步:再根據這四條索引數據中的 id 值,逐一進行回表掃描,從聚簇索引中找到相應的行數據,將找到的行數據返回給 server 層。 第三步:在server層判斷age = 17,進行篩選,最終只留下 Lucy 用戶的數據信息。使用索引下推的執行過程:
第一步:利用索引找出name帶'L'的數據行:LiLei、Lili、Lisa、Lucy 這四條索引數據 第二步:根據 age = 17 這個條件,對四條索引數據進行判斷篩選,最終只留下 Lucy 用戶的數據信息。 (注意:這一步不是直接進行回表操作,而是根據 age = 17 這個條件,對四條索引數據進行判斷篩選) 第三步:將符合條件的索引對應的 id 進行回表掃描,最終將找到的行數據返回給 server 層。比較二者的第二步我們發現,索引下推的方式極大的減少了回表次數。
索引下推需要注意的情況:
下推的前提是索引中有 age 列信息,如果是其它條件,如 gender = 0,這個即使下推下來也沒用
開啟索引下推:
索引下推是 MySQL 5.6 及以上版本上推出的,用于對查詢進行優化。默認情況下,索引下推處于啟用狀態。我們可以使用如下命令來開啟或關閉。
set optimizer_switch='index_condition_pushdown=off'; -- 關閉索引下推 set optimizer_switch='index_condition_pushdown=on'; -- 開啟索引下推為什么需要二階段提交
場景描述
如果沒有兩階段提交,那么 binlog 和 redolog 的提交,無非就是兩種形式:
先寫 binlog 再寫 redolog。
先寫 redolog 再寫 binlog。
這兩種情況我們分別來看。
假設我們要向表中插入一條記錄 R,如果是先寫 binlog 再寫 redolog,那么假設 binlog 寫完后崩潰了,此時 redolog 還沒寫。那么重啟恢復的時候就會出問題:binlog 中已經有 R 的記錄了,當從機從主機同步數據的時候或者我們使用 binlog 恢復數據的時候,就會同步到 R 這條記錄;但是 redolog 中沒有關于 R 的記錄,所以崩潰恢復之后,插入 R 記錄的這個事務是無效的,即數據庫中沒有該行記錄,這就造成了數據不一致。
相反,假設我們要向表中插入一條記錄 R,如果是先寫 redolog 再寫 binlog,那么假設 redolog 寫完后崩潰了,此時 binlog 還沒寫。那么重啟恢復的時候也會出問題:redolog 中已經有 R 的記錄了,所以崩潰恢復之后,插入 R 記錄的這個事務是有效的,通過該記錄將數據恢復到數據庫中;但是 binlog 中還沒有關于 R 的記錄,所以當從機從主機同步數據的時候或者我們使用 binlog 恢復數據的時候,就不會同步到 R 這條記錄,這就造成了數據不一致。
那么按照前面說的兩階段提交就能解決問題嗎?
我們來看如下三種情況:
情況一: redo log時候崩潰了,此時:
由于 binlog 還沒寫,redo log 處于 prepare 狀態還沒提交,所以崩潰恢復的時候,這個事務會回滾,此時 binlog 還沒寫,所以也不會傳到備庫。
**情況二:**假設寫完 binlog 之后崩潰了,此時:
redolog 中的日志是不完整的,處于 prepare 狀態,還沒有提交,那么恢復的時候,首先檢查 binlog 中的事務是否存在并且完整,如果存在且完整,則直接提交事務,如果不存在或者不完整,則回滾事務。
**情況三:**假設 redolog 處于 commit 狀態的時候崩潰了,那么重啟后的處理方案同情況二。
由此可見,兩階段提交能夠確保數據的一致性。
為什么innodb表必須創建主鍵,并且使用整形的自增主鍵
InnoDB中采用的是聚簇索引,表數據文件本身就是按照B+Tree組織的一個索引結構文件,主鍵索引默認就是B+Tree,由此主鍵索引可以維護整張表。如果在實際建表過程中不建立主鍵,MySQL會自動在表中找一列數據(該列數據沒有重復值)來建立唯一索引,在B+tree中維護整張表的數據。
整型比大小更快,整型對于UUID來說占用存儲空間小。
用自增方便每次插入到葉子節點鏈的后面,對于B+樹的分裂來說更加方便。如果不用自增的話,有可能插入到葉子節點的中間位置,對于B+樹的分裂來說不太方便。主要影響數據寫入表的性能。
如何處理線上慢SQL
1、通過相關指令開啟慢查詢日志
-- 查看是否開啟了慢查詢日志 show variables like 'slow_query_log'; -- 默認是OFF,不開啟,可以手動開啟 -- 方式一 set global slow_query_log=1; -- 修改配置文件my.cnf,加入下面一行命令 slow_query_log = ON2、慢查詢日志找到對應的SQL,分析SQL
-- 查詢慢查詢日志文件路徑 show variables like '%slow_query_log_file%'; -- MySQL提供了分析慢查詢日志的工具mysqldumpslow mysqldumpslow -s t -t 10 /usr/local/mysql/data/localhost_slow.log-- 例如 ,休眠20s SELECT sleep(20); 常用參數有 -s: 表示按何種方式排序: c: 訪問次數 l: 鎖定時間 r: 返回記錄 t: 查詢時間 al: 平均鎖定時間 ar: 平均返回記錄數 at: 平均查詢時間-t: 返回前面多少條的數據3、where條件單表查,鎖定最小返回記錄表。這句話的意思是把查詢語句的where都應用到表中返回的記錄數最小的表開始查起,單表每個字段分別查詢,看哪個字段的區分度最高
看一下accurate_result = 1的記錄數:
select count(*),accurate_result from stage_poi group by accurate_result; +----------+-----------------+ | count(*) | accurate_result | +----------+-----------------+ | 1023 | -1 | | 2114655 | 0 | | 972815 | 1 | +----------+-----------------+我們看到accurate_result這個字段的區分度非常低,整個表只有-1,0,1三個值,加上索引也無法鎖定特別少量的數據。
4、explain查看執行計劃,是否與1預期一致(從鎖定記錄較少的表開始查詢 )
索引是否應用,聯合索引是否完全應用,掃描行數rows
explain select * from a;
explain select * from b;
5、了解業務應用場景,實時數據,歷史數據,定期刪除等因素,輔助我們更好的分析和優化查詢語句
6、加索引時參照建索引的幾大原則
最左匹配原則
區分度高的列作為索引,count(distinct name)/count(*),表示字段不重復的比例,比例越大掃描記錄越少
索引失效情況
盡量擴展索引,不要新建索引
7、mysqldumpslow
使用幫助
-s ORDER what to sort by (al, at, ar, c, l, r, t), 'at' is default # 默認是at 平均查詢時間al: average lock timear: average rows sentat: average query timec: countl: lock timer: rows sentt: query time # 查詢時間排序 -r reverse the sort order (largest last instead of first) # 反轉排序順序 -t n just show the top n queries # 僅僅顯示前n行實踐
mysqldumpslow -s t -t 10 localhost-slow.log總結
1 根據命令mysqldumpslow找到慢查詢時間耗時比較長的SQL
2 explain查看執行計劃,需要重點關注 type索引是否應用,聯合索引是否完全應用,掃描行數rows
3 加索引時參照建索引的幾大原則
3.1 最左匹配原則
3.2 區分度高的列作為索引,count(distinct name)/count(*),表示字段不重復的比例,比例越大掃描記錄越少
3.3 索引失效情況
3.4 盡量擴展索引,不要新建索引
4 了解業務應用場景,實時數據,歷史數據,定期刪除等因素,輔助我們更好的分析和優化查詢語句
MySQL調優
1 加索引
增加索引是一種簡單搞笑的手段,但是需要選擇合適的列,同時避免導致索引失效的操作,比如like,函數等。
2 避免返回不必要的數據列,減少返回的數據列可以增加查詢效率
3 根據執行計劃適當優化SQL結構,比如是否全表掃描,避免子查詢等問題
4 分庫分表
在單表數據量比較大時候或者并發連接數過多,通過這種方式提高查詢效率
5 讀寫分離
講一下MySQL死鎖,如何解決排查
死鎖是指兩個或多個事務在同一資源上相互占用,并請求鎖定對方的資源,從而導致惡性循環的現象。
常見的解決死鎖的方法
1、如果不同程序會并發存取多個表,盡量約定以相同的順序訪問表,可以大大降低死鎖機會。
2、在同一個事務中,盡可能做到一次鎖定所需要的所有資源,減少死鎖產生概率;
3、對于非常容易產生死鎖的業務部分,可以嘗試使用升級鎖定顆粒度,通過表級鎖定來減少死鎖產生的概率;
如果業務處理不好可以用分布式事務鎖或者使用樂觀鎖
如何查看死鎖
使用命令show engine innodb status查看最近一次出現的死鎖。
還可以使用Innodb Lock Monitor 打開監控,每15s輸出一次日志。 使用完后建議關閉,會影響性能。
如何處理死鎖
innodb如何解決幻讀
事務隔離級別
Mysql 有四種事務隔離級別,這四種隔離級別代表當存在多個事務并發沖突時,
可能出現的臟讀、不可重復讀、幻讀的問題。
其中 InnoDB 在 RR 的隔離級別下,解決了幻讀的問題
幻讀
那么,什么是幻讀呢?
幻讀是指在同一個事務中,前后兩次查詢相同的范圍時,得到的結果不一致
第一個事務里面我們執行了一個范圍查詢,這個時候滿足條件的數據只有一條
第二個事務里面,它插入了一行數據,并且提交了
接著第一個事務再去查詢的時候,得到的結果比第一查詢的結果多出來了一條數
據。
如何解決
InnoDB 引入了間隙鎖和 next-key Lock 機制來解決幻讀問題,為了更清晰的說明這兩種鎖,
我舉一個例子: 假設現在存在這樣這樣一個 B+Tree 的索引結構,這個結構中有四個索引元素分 別是:1、4、7、10。
當我們通過主鍵索引查詢一條記錄,并且對這條記錄通過 for update 加鎖
這個時候,會產生一個記錄鎖,也就是行鎖,鎖定 id=1 這個索引
被鎖定的記錄在鎖釋放之前,其他事務無法對這條記錄做任何操作。
前面我說過對幻讀的定義:幻讀是指在同一個事務中,前后兩次查詢相同的范圍時,得到的結果不一致! 注意,這里強調的是范圍查詢, 也就是說,InnoDB 引擎要解決幻讀問題,必須要保證一個點,就是如果一個事務通過這樣一條語句進行鎖定時,另外一個事務再執行這樣一條 insert 語句,需要被阻塞,直到前面獲得鎖的事務釋放;所以,在 InnoDB 中設計了一種間隙鎖,它的主要功能是鎖定一段范圍內的索引。
記錄當對查詢范圍 id>4 and id<7 加鎖的時候,會針對 B+樹中(4,7)這個開區間范
圍的索引加間隙鎖。 意味著在這種情況下,其他事務對這個區間的數據進行插入、更新、刪除都會被
鎖住。
但是,還有另外一種情況,比如像這樣這條查詢語句是針對 id>4 這個條件加鎖,那么它需要鎖定多個索引區間,所以
在這種情況下 InnoDB 引入了 next-key Lock 機制。
next-key Lock 相當于間隙鎖和記錄鎖的合集,記錄鎖鎖定存在的記錄行,間隙
鎖鎖住記錄行之間的間隙,而 next-key Lock 鎖住的是兩者之和每個數據行上的非唯一索引列上都會存在一把 next-key lock,當某個事務持有該數
據行的 next-key lock 時,會鎖住一段左開右閉區間的數據。
因此,當通過 id>4 這樣一種范圍查詢加鎖時,會加 next-key Lock,鎖定的區間
范圍是:(4,7],(7,10],(10,+∞],間隙鎖和 next-key Lock 的區別在于加鎖的范圍,間隙鎖只鎖定兩個索引之間的引用間隙,而 next-key Lock 會鎖定多個索引區間,它包含記錄鎖和間隙鎖。
總結
雖然 InnoDB 中通過間隙鎖的方式解決了幻讀問題,但是加鎖之后一定會影響到 并發性能,因此,如果對性能要求較高的業務場景中,可以把隔離級別設置成 RC(讀已提交),這個級別中不存在間隙鎖
樂觀鎖和悲觀鎖了解
樂觀鎖
顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量。
如何實現樂觀鎖
第一種方案
通過 數據版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂數據版本?即為數據增加一個版本標識,一般是通過為數據庫表增加一個數字類型的 “version” 字段來實現。當讀取數據時,將 version 字段的值一同讀出,數據每更新一次,對此 version 值加一。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的 version 值進行比對,如果數據庫表當前版本號與第一次取出來的 version 值相等,則予以更新,否則認為是過期數據,如果更新失敗則說明是重復請求,直接異常中斷或者查詢出上次執行的結果數據返回即可。
第二種方案
在需要樂觀鎖控制的 table 中增加一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的 version 類似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和自己更新前取到的時間戳進行對比,如果一致則 OK,否則就是版本沖突。
應用場景
比如A、B操作員同時讀取一余額為1000元的賬戶,A操作員為該賬戶增加100元,B操作員同時為該賬戶扣除50元,A先提交,B后提交。最后實際賬戶余額為1000-50=950元,但本該為1000+100-50=1050。這就是典型的并發問題。
悲觀鎖
對于并發間操作產生的線程安全問題持悲觀狀態,悲觀鎖認為競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨占的鎖,然后再操作資源。
悲觀鎖采用的是「先獲取鎖再訪問」的策略,來保障數據的安全。但是加鎖策略,依賴數據庫實現,會增加數據庫的負擔,且會增加死鎖的發生幾率。此外,對于不會發生變化的只讀數據,加鎖只會增加額外不必要的負擔。在實際的實踐中,對于并發很高的場景并不會使用悲觀鎖,因為當一個事務鎖住了數據,那么其他事務都會發生阻塞,會導致大量的事務發生積壓拖垮整個系統。
場景
在商品購買場景中,當有多個用戶對某個庫存有限的商品同時進行下單操作。若采用先查詢庫存,后減庫存的方式進行庫存數量的變更,將會導致超賣的產生。
若使用悲觀鎖,當B用戶獲取到某個商品的庫存數據時,用戶A則會阻塞,直到B用戶完成減庫存的整個事務時,A用戶才可以獲取到商品的庫存數據。則可以避免商品被超賣。
初始化表結構和數據
CREATE TABLE `tbl_user` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`status` int(11) DEFAULT NULL,`name` varchar(255) COLLATE utf8_bin DEFAULT NULL,PRIMARY KEY (`id`) );INSERT INTO `tbl_user` (`id`, `status`, `name`) VALUES(1,1,X'7469616E'),(2,1,X'63697479');事務操作
窗口1
// 關閉mysql數據庫的自動提交屬性 set autocommit=0;// 開啟事務 BEGIN;SELECT * FROM tbl_user where id=1 for update;窗口2
SELECT * FROM tbl_user where id=1 for update;如何使用
如何使用悲觀鎖
用法:SELECT … FOR UPDATE;
如下操作
select * from tbl_user where id=1 for update;獲取鎖的前提:結果集中的數據沒有使用排他鎖或共享鎖時,才能獲取鎖,否則將會阻塞。
需要注意的是, FOR UPDATE 生效需要同時滿足兩個條件時才生效:
- 數據庫的引擎為 innoDB
- 操作位于事務塊中(BEGIN/COMMIT)
InnoDB 四大特性或者如何設計的
插入緩沖(insert buffer)
索引是存儲在磁盤上的,所以對于索引的操作需要涉及磁盤操作。如果我們使用自增主鍵,那么在插入主鍵索引(聚簇索引)時,只需不斷追加即可,不需要磁盤的隨機 I/O。但是如果我們使用的是普通索引,大概率是無序的,此時就涉及到磁盤的隨機 I/O,而隨機I/O的性能是比較差的(Kafka 官方數據:磁盤順序I/O的性能是磁盤隨機I/O的4000~5000倍)。
因此,InnoDB 存儲引擎設計了 Insert Buffer ,對于非聚集索引的插入或更新操作,不是每一次直接插入到索引頁中,而是先判斷插入的非聚集索引頁是否在緩沖池(Buffer pool)中,若在,則直接插入;若不在,則先放入到一個 Insert Buffer 對象中,然后再以一定的頻率和情況進行 Insert Buffer 和輔助索引頁子節點的 merge(合并)操作,這時通常能將多個插入合并到一個操作中(因為在一個索引頁中),這就大大提高了對于非聚集索引插入的性能。
插入緩沖的使用需要滿足以下兩個條件:1)索引是輔助索引;2)索引不是唯一的。
二次寫(double write):
臟頁刷盤風險:InnoDB 的 page size一般是16KB,操作系統寫文件是以4KB作為單位,那么每寫一個 InnoDB 的 page 到磁盤上,操作系統需要寫4個塊。于是可能出現16K的數據,寫入4K 時,發生了系統斷電或系統崩潰,只有一部分寫是成功的,這就是 partial page write(部分頁寫入)問題。這時會出現數據不完整的問題。
這時是無法通過 redo log 恢復的,因為 redo log 記錄的是對頁的物理修改,如果頁本身已經損壞,重做日志也無能為力。
doublewrite 就是用來解決該問題的。doublewrite 由兩部分組成,一部分為內存中的 doublewrite buffer,其大小為2MB,另一部分是磁盤上共享表空間中連續的128個頁,即2個區(extent),大小也是2M。
為了解決 partial page write 問題,當 MySQL 將臟數據刷新到磁盤的時候,會進行以下操作:
1)先將臟數據復制到內存中的 doublewrite buffer
2)之后通過 doublewrite buffer 再分2次,每次1MB寫入到共享表空間的磁盤上(順序寫,性能很高)
3)完成第二步之后,馬上調用 fsync 函數,將doublewrite buffer中的臟頁數據寫入實際的各個表空間文件(離散寫)。
如果操作系統在將頁寫入磁盤的過程中發生崩潰,InnoDB 再次啟動后,發現了一個 page 數據已經損壞,InnoDB 存儲引擎可以從共享表空間的 doublewrite 中找到該頁的一個最近的副本,用于進行數據恢復了。
自適應哈希索引(adaptive hash index)
哈希(hash)是一種非常快的查找方法,一般情況下查找的時間復雜度為 O(1)。但是由于不支持范圍查詢等條件的限制,InnoDB 并沒有采用 hash 索引,但是如果能在一些特殊場景下使用 hash 索引,則可能是一個不錯的補充,而 InnoDB 正是這么做的。
具體的,InnoDB 會監控對表上索引的查找,如果觀察到某些索引被頻繁訪問,索引成為熱數據,建立哈希索引可以帶來速度的提升,則建立哈希索引,所以稱之為自適應(adaptive)的。自適應哈希索引通過緩沖池的 B+ 樹構造而來,因此建立的速度很快。而且不需要將整個表都建哈希索引,InnoDB 會自動根據訪問的頻率和模式來為某些頁建立哈希索引。
預讀(read ahead):
InnoDB 在 I/O 的優化上有個比較重要的特性為預讀,當 InnoDB 預計某些 page 可能很快就會需要用到時,它會異步地將這些 page 提前讀取到緩沖池(buffer pool)中,這其實有點像空間局部性的概念。
空間局部性(spatial locality):如果一個數據項被訪問,那么與他地址相鄰的數據項也可能很快被訪問。
InnoDB使用兩種預讀算法來提高I/O性能:線性預讀(linear read-ahead)和隨機預讀(randomread-ahead)。
其中,線性預讀以 extent(塊,1個 extent 等于64個 page)為單位,而隨機預讀放到以 extent 中的 page 為單位。線性預讀著眼于將下一個extent 提前讀取到 buffer pool 中,而隨機預讀著眼于將當前 extent 中的剩余的 page 提前讀取到 buffer pool 中。
線性預讀(Linear read-ahead):線性預讀方式有一個很重要的變量 innodb_read_ahead_threshold,可以控制 Innodb 執行預讀操作的觸發閾值。如果一個 extent 中的被順序讀取的 page 超過或者等于該參數變量時,Innodb將會異步的將下一個 extent 讀取到 buffer pool中,innodb_read_ahead_threshold 可以設置為0-64(一個 extend 上限就是64頁)的任何值,默認值為56,值越高,訪問模式檢查越嚴格。
隨機預讀(Random read-ahead): 隨機預讀方式則是表示當同一個 extent 中的一些 page 在 buffer pool 中發現時,Innodb 會將該 extent 中的剩余 page 一并讀到 buffer pool中,由于隨機預讀方式給 Innodb code 帶來了一些不必要的復雜性,同時在性能也存在不穩定性。
數據庫數據量有多少
2000萬
聚集索引、輔助索引、覆蓋索引、聯合索引的使用
聚集索引就是按照每張表的主鍵構造一棵B+樹,同時葉子節點中存放的即為整張表的行記錄數據。
輔助索引,也叫非聚集索引。和聚集索引相比,葉子節點中并不包含行記錄的全部數據。葉子節點除了包含鍵值以外,每個葉子節點的索引行還包含了一個書簽(bookmark),該書簽用來告訴InnoDB哪里可以找到與索引相對應的行數據。
InnoDB存儲引擎支持覆蓋索引,即從輔助索引中就可以得到查詢的記錄,而不需要查詢聚集索引中的記錄。
如果要查詢輔助索引中不含有的字段,得先遍歷輔助索引,再遍歷聚集索引,而如果要查詢的字段值在輔助索引上就有,就不用再查聚集索引了,這顯然會減少IO操作。
聯合索引是指對表上的多個列進行索引。
聚集索引的葉子節點稱為數據頁,每個數據頁通過一個雙向鏈表來進行鏈接,而且數據頁按照主鍵的順序進行排列。
每個數據頁上存放的是完整的行記錄,而在非數據頁的索引頁中,存放的僅僅是鍵值及指向數據頁的偏移量,而不是一個完整的行記錄。
分庫分表
主要是對兩個方法進行攔截,一個query方法,一個是update方法
那么對這兩個方法進行攔截,并且在這個接口上面實現這個或者
自定義的這個注解Annotation,通過反射拿到這個接口的注解
根據注解的參數來分表操作,再去修改原來這個SQL這個執行結果
按照年份來劃分的,這個是按照業務場景來劃分的,也可以根據id取模。
Redis必備知識
SDS了解嗎
背景
Redis 是用 C 語言實現的,但是它沒有直接使用 C 語言的 char* 字符數組來實現字符串,而是自己封裝了一個名為簡單動態字符串(simple dynamic string,SDS) 的數據結構來表示字符串,也就是 Redis 的 String 數據類型的底層數據結構是 SDS。
c語言字符串缺陷
C 語言的字符串其實就是一個字符數組,即數組中每個元素是字符串中的一個字符。
1 在 C 語言里,對字符串操作時,char * 指針只是指向字符數組的起始位置,而字符數組的結尾位置就用“\0”表示,意思是指字符串的結束。
舉個例子,C 語言獲取字符串長度的函數 strlen,就是通過字符數組中的每一個字符,并進行計數,等遇到字符為“\0”后,就會停止遍歷,然后返回已經統計到的字符個數,即為字符串長度
C 語言的字符串用 “\0” 字符作為結尾標記有個缺陷。假設有個字符串中有個 “\0” 字符,這時在操作這個字符串時就會提早結束,比如 “xiao\0lin” 字符串,計算字符串長度的時候則會是 4
2 用 char* 字符串中的字符必須符合某種編碼(比如ASCII)。這些限制使得 C 語言的字符串只能保存文本數據,不能保存像圖片、音頻、視頻文化這樣的二進制數據
3 C 語言標準庫中字符串的操作函數是很不安全的,對程序員很不友好,稍微一不注意,就會導致緩沖區溢出。
舉個例子,strcat 函數是可以將兩個字符串拼接在一起。
c //將 src 字符串拼接到 dest 字符串后面 char *strcat(char *dest, const char* src);C 語言的字符串是不會記錄自身的緩沖區大小的,所以 strcat 函數假定程序員在執行這個函數時,已經為 dest 分配了足夠多的內存,可以容納 src 字符串中的所有內容,而一旦這個假定不成立,就會發生緩沖區溢出將可能會造成程序運行終止,(*這是一個可以改進的地方*)。
而且,strcat 函數和 strlen 函數類似,時間復雜度也很高,也都需要先通過遍歷字符串才能得到目標字符串的末尾。然后對于 strcat 函數來說,還要再遍歷源字符串才能完成追加,對字符串的操作效率不高。
sds結構設計
結構中的每個成員變量分別介紹下:
- len,SDS 所保存的字符串長度。這樣獲取字符串長度的時候,只需要返回這個變量值就行,時間復雜度只需要 O(1)。
- alloc,分配給字符數組的空間長度。這樣在修改字符串的時候,可以通過 alloc - len 計算 出剩余的空間大小,然后用來判斷空間是否滿足修改需求,如果不滿足的話,就會自動將 SDS 的空間擴展至執行修改所需的大小,然后才執行實際的修改操作,所以使用 SDS 既不需要手動修改 SDS 的空間大小,也不會出現前面所說的緩沖區益處的問題。
- flags,SDS 類型,用來表示不同類型的 SDS。一共設計了 5 種類型,分別是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在說明區別之處。
- buf[],字節數組,用來保存實際數據。不需要用 “\0” 字符來標識字符串結尾了,而是直接將其作為二進制數據處理,可以用來保存圖片等二進制數據。它即可以保存文本數據,也可以保存二進制數據,所以叫字節數組會更好點。
總的來說,Redis 的 SDS 結構在原本字符數組之上,增加了三個元數據:len、alloc、flags,用來解決 C 語言字符串的缺陷。
優勢
O(1)復雜度獲取字符串長度
Redis 的 SDS 結構因為加入了 len 成員變量,那么獲取字符串長度的時候,直接返回這個變量的值就行,所以復雜度只有 O(1)
二進制安全
因為 SDS 不需要用 “\0” 字符來標識字符串結尾了,而且 SDS 的 API 都是以處理二進制的方式來處理 SDS 存放在 buf[] 里的數據,程序不會對其中的數據做任何限制,數據寫入的時候時什么樣的,它被讀取時就是什么樣的
不會發生緩沖區溢出
C 語言的字符串標準庫提供的字符串操作函數,大多數(比如 strcat 追加字符串函數)都是不安全的,因為這些函數把緩沖區大小是否滿足操作的工作交由開發者來保證,程序內部并不會判斷緩沖區大小是否足夠用,當發生了緩沖區溢出就有可能造成程序異常結束。
所以,Redis 的 SDS 結構里引入了 alloc 和 leb 成員變量,這樣 SDS API 通過 alloc - len 計算,可以算出剩余可用的空間大小,這樣在對字符串做修改操作的時候,就可以由程序內部判斷緩沖區大小是否足夠用。
而且,當判斷出緩沖區大小不夠用時,Redis 會自動將擴大 SDS 的空間大小,以滿足修改所需的大小。
在擴展 SDS 空間之前,SDS API 會優先檢查未使用空間是否足夠,如果不夠的話,API 不僅會為 SDS 分配修改所必須要的空間,還會給 SDS 分配額外的「未使用空間」。
這樣的好處是,下次在操作 SDS 時,如果 SDS 空間夠的話,API 就會直接使用「未使用空間」,而無須執行內存分配,有效的減少內存分配次數。
節省內存空間
SDS 結構中有個 flags 成員變量,表示的是 SDS 類型。
Redis 一共設計了 5 種類型,分別是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。
這 5 種類型的主要區別就在于,它們數據結構中的 len 和 alloc 成員變量的數據類型不同,
之所以 SDS 設計不同類型的結構體,是為了能靈活保存不同大小的字符串,從而有效節省內存空間。比如,在保存小字符串時,結構頭占用空間也比較少。
使用了專門的編譯優化來節省內存空間
如何使用,常用哪些數據結構
用途:緩存熱點數據,白名單,延時隊列,分布式鎖
數據結構:sds,hash,壓縮列表,雙向鏈表,整數集合,跳表
Redis五種數據類型及應用場景
| LIST | 列表 | 從兩端壓入或者彈出元素 對單個或者多個元素進行修剪, 只保留一個范圍內的元素 | 存儲一些列表型的數據結構,類似粉絲列表、文章的評論列表之類的數據,簡單的消息隊列的功能 |
| SET | 無序集合 | 添加、獲取、移除單個元素 檢查一個元素是否存在于集合中 計算交集、并集、差集 從集合里面隨機獲取元素 | 交集、并集、差集的操作,比如交集,可以把兩個人的粉絲列表整一個交集,做全局去重的功能,點贊,轉發,收藏; |
| HASH | 包含鍵值對的無序散列表 | 添加、獲取、移除單個鍵值對 獲取所有鍵值對 檢查某個鍵是否存在 | 結構化的數據,比如一個對象,單點登錄 |
| ZSET | 有序集合 | 添加、獲取、刪除元素 根據分值范圍或者成員來獲取元素 計算一個鍵的排名 | 去重但可以排序,如獲取排名前幾名的用戶,做排行榜應用,取TOPN操作;延時任務;做范圍查找。周榜,月榜,年榜 |
持久化技術
1、持久化就是把內存的數據寫到磁盤中去,防止服務宕機了內存數據丟失。
2、rdb:rdb:快照方式,按照一定的時間將內存的數據以快照的形式保存到硬盤中,對應產生的數據文件為dump.rdb。通過配置文件中的save參數來定義快照的周期。
aop:aof:將數據每隔一秒追加到文件中。
3、兩者區別聯系
- AOF文件比RDB更新頻率高,優先使用AOF還原數據。
- AOF比RDB更安全也更大
- RDB性能比AOF好
- 如果兩個都配了優先加載AOF
bgsave做鏡像全量持久化,aof做增量持久化。因為bgsave會耗費較長時間,不夠實時,在停機的時候會導致大量丟失數據,所以需要aof來配合使用。在redis實例重啟時,會使用bgsave持久化文件重新構建內存,再使用aof重放近期的操作指令來實現完整恢復重啟之前的狀態。
對方追問那如果突然機器掉電會怎樣?取決于aof日志sync屬性的配置,如果不要求性能,在每條寫指令時都sync一下磁盤,就不會丟失數據。但是在高性能的要求下每次都sync是不現實的,一般都使用定時sync,比如1s1次,這個時候最多就會丟失1s的數據。
對方追問bgsave的原理是什么?你給出兩個詞匯就可以了,fork和cow。fork是指redis通過創建子進程來進行bgsave操作,cow指的是copy on write,子進程創建后,父子進程共享數據段,父進程繼續提供讀寫服務,寫臟的頁面數據會逐漸和子進程分離開來。
緩存雪崩,緩存穿透,緩存擊穿
緩存雪崩是指緩存同一時間大面積的失效,所以,后面的請求都會落到數據庫上,造成數據庫短時間內承受大量請求而崩掉。
解決:redis高可用
緩存穿透是指緩存和數據庫中都沒有的數據,導致所有的請求都落到數據庫上,造成數據庫短時間內承受大量請求而崩掉。
解決:采用布隆過濾器,將所有可能存在的數據哈希到一個足夠大的 bitmap 中,一個一定不存在的數據會被這個 bitmap 攔截掉,從而避免了對底層存儲系統的查詢壓力;
接口層增加校驗,如用戶鑒權校驗,id做基礎校驗,id<=0的直接攔截;
緩存擊穿是指緩存中沒有但數據庫中有的數據(一般是緩存時間到期),這時由于并發用戶特別多,同時讀緩存沒讀到數據,又同時去數據庫去取數據,引起數據庫壓力瞬間增大,造成過大壓力。和緩存雪崩不同的是,緩存擊穿指并發查同一條數據,緩存雪崩是不同數據都過期了,很多數據都查不到從而查數據庫。
解決:
(1)一個“冷門”key,突然被大量用戶請求訪問。
(2)一個“熱門”key,在緩存中時間恰好過期,這時有大量用戶來進行訪問。
分布式鎖實現方式,區別
zookeeper
Zookeeper數據存儲結構是一顆樹,樹由節點組成,節點叫ZNode
Znode分四種類型:
1、持久節點(persistent)
默認節點類型,創建節點的客戶端和Zookeeper斷開鏈接后,節點依舊存在
2、持久節點順序節點(persistent_sequential)
創建節點時,根據創建時間給節點編號。
3、臨時節點
斷開鏈接后,節點被刪除
4、臨時順序節點
Zookeeper分布式鎖的原理:
獲取鎖:
釋放鎖:
redis
1、加鎖
加鎖實際上就是在redis中,給Key鍵設置一個值,為避免死鎖,并給定一個過期時間。 SET lock_key random_value NX PX 5000 值得注意的是:
random_value 是客戶端生成的唯一的字符串。 NX 代表只在鍵不存在時,才對鍵進行設置操作。 PX 5000 設置鍵的過期時間為5000毫秒。 這樣,如果上面的命令執行成功,則證明客戶端獲取到了鎖。2、解鎖
解鎖的過程就是將Key鍵刪除。但也不能亂刪,不能說客戶端1的請求將客戶端2的鎖給刪除掉。這時候random_value的作用就體現出來。 為了保證解鎖操作的原子性,我們用LUA腳本完成這一操作。先判斷當前鎖的字符串是否與傳入的值相等,是的話就刪除Key,解鎖成功。
if redis.call('get',KEYS[1]) == ARGV[1] thenreturn redis.call('del',KEYS[1])elsereturn 0end3、使用
首先,我們在pom文件中,引入Redis包。在這里,筆者用的是最新版本,注意由于版本的不同,API可能有所差異。
Redis有哪些場景導致數據丟失
主從復制:因為master -> slave的復制是異步的,所以可能有部分數據還沒復制到slave,master就宕機了,此時這些部分數據就丟失了。
腦裂導致的數據丟失:腦裂,也就是說,某個master所在機器突然脫離了正常的網絡,跟其他slave機器不能連接,但是實際上master還運行著
此時哨兵可能就會認為master宕機了,然后開啟選舉,將其他slave切換成了master
這個時候,集群里就會有兩個master,也就是所謂的腦裂
此時雖然某個slave被切換成了master,但是可能client還沒來得及切換到新的master,還繼續寫向舊master的數據可能也丟失了,因此舊master再次恢復的時候,會被作為一個slave掛到新的master上去,自己的數據會清空,重新從新的master復制數據
解決腦裂問題:通過在redis.conf配置控制同步時間減少數據丟失.
# 要求至少有1個slave,數據復制和同步延遲不能超過10秒 min-slaves-to-write 1# 如果說一旦所有的slave,數據復制和同步的延遲都超過了10秒鐘,那么master就會拒絕接收任何請求 min-slaves-max-lag 10看門狗機制了解嗎
基于時間輪算法實現,底層數據結構就是數組+鏈表,數組相當于一個時鐘的一個位置,當我們初始化一個時間輪時候,只是初始化一個數組,然后它還沒有啟動一個新的線程,當我們真正添加一個任務到時間輪當中,才會新起一個線程,避免了空轉的發生,放任務的時候也不是直接加入時間輪當中,會先加入一個mpsc隊列當中,對于這塊設計并發的一個考慮,就是多生產者,單消費者一個模式下,然后當時間輪走過哪個ticket,一個數組的位置的時候,會有一個判斷,這次是否達到了時間輪,開始工作的一個時間,如果還沒到的話,就會睡眠;直到下一次時間輪開始時間,它就會繼續工作,然后從隊列中把任務取出來,然后放在不同數組下標里面,如果任務在同一個數組的位置,以鏈表形式添加這個桶。
數據庫鎖
1、根據名字獲取鎖信息
2、更新鎖信息(比如版本,狀態等)占有鎖
如何保證Redis與數據庫的數據一致性
| 問題 | 解決思路 |
| 先更新數據庫,再刪除緩存。如果刪除緩存失敗了,那么會導致數據庫中是新數據,緩存中是舊數據,數據就出現了不一致 | 先刪除緩存,再更新數據庫。如果數據庫更新失敗了,那么數據庫中是舊數據,緩存中是空的,那么數據不會不一致。因為讀的時候緩存沒有,所以去讀了數據庫中的舊數據,然后更新到緩存中 |
| 數據發生了變更,先刪除了緩存,然后要去修改數據庫,此時還沒修改。一個請求過來,去讀緩存,發現緩存空了,去查詢數據庫,查到了修改前的舊數據,放到了緩存中。隨后數據變更的程序完成了數據庫的修改。完了,數據庫和緩存中的數據不一樣了 | 方案一:寫請求先刪除緩存,再去更新數據庫,(異步等待段時間)再刪除緩存(成功表示有臟數據出現);這種方案讀取快速,但會出現短時間的臟數據。,異步刪除對線上業務無影響,串行化處理保障并發情況下正確刪除,如果雙刪失敗怎么辦,整個重試機制,可以借助消息隊列的重試機制,也可以自己整個表,記錄重試次數 方案二:寫請求先修改緩存為指定值,再去更新數據庫,再更新緩存。讀請求過來后,先讀緩存,判斷是指定值后進入循環狀態,等待寫請求更新緩存。如果循環超時就去數據庫讀取數據,更新緩存。這種方案保證了讀寫的一致性,但是讀請求會等待寫操作的完成,降低了吞吐量。 |
如何解決redis并發競爭的key問題
問題描述:多個客戶端set同一個key《=》三個請求有序的修改某個key,正常情況數據版本應該是123,但是由于網絡原因變成了132,出現問題了。
第一種方案:分布式鎖+時間戳
這種情況,主要是準備一個分布式鎖,大家去搶鎖,搶到鎖就做set操作。
加鎖的目的實際上就是把并行讀寫改成串行讀寫的方式,從而來避免資源競爭。
寫入時順便寫一個時間戳;
寫入之前比較一下自己的時間戳是否早于現有記錄的時間戳,如果早于的話,說明自己的時間戳過期了,就
不需要寫入了;
分布式鎖
問題
1、如何避免死鎖
2、鎖被別人釋放怎么辦
3、鎖過期時間不好評估
時間戳
要求key的操作需要順序執行,所以需要保存一個時間戳判斷set順序
第二種方案:利用消息隊列
在并發量過大的情況下,可以通過消息中間件進行處理,把并行讀寫進行串行化。
把Redis.set操作放在隊列中使其串行化,必須的一個一個執行。
這種方式在一些高并發的場景中算是一種通用的解決方案。
Redis常見性能問題和解決方案
Master寫內存快照,save命令調度rdbSave函數,會阻塞主線程的工作,當快照比較大時對性能影響是非常大的,會間斷性暫停服務,所以Master最好不要寫內存快照。
Master AOF持久化,如果不重寫AOF文件,這個持久化方式對性能的影響是最小的,但是AOF文件會不斷增大,AOF文件過大會影響Master重啟的恢復速度。Master最好不要做任何持久化工作,包括內存快照和AOF日志文件,特別是不要啟用內存快照做持久化,如果數據比較關鍵,某個Slave開啟AOF備份數據,策略為每秒同步一次。
Master調用BGREWRITEAOF重寫AOF文件,AOF在重寫的時候會占大量的CPU和內存資源,導致服務load過高,出現短暫服務暫停現象。
Redis主從復制的性能問題,為了主從復制的速度和連接的穩定性,Slave和Master最好在同一個局域網內。
Redis淘汰key算法
Redis官方給的警告,當內存不足時,Redis會根據配置的緩存策略淘汰部分keys,以保證寫入成功。當無淘汰策略時或沒有找到適合淘汰的key時,Redis直接返回out of memory錯誤。
1、volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰
2、volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
3、volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰
4、allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰
5、allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
6、no-enviction(驅逐):禁止驅逐數據
應用場景: 1).在Redis中,數據有一部分訪問頻率較高,其余部分訪問頻率較低,或者無法預測數據的使用頻率時,設置allkeys-lru是比較合適的。 2). 如果所有數據訪問概率大致相等時,可以選擇allkeys-random。 3). 如果研發者需要通過設置不同的ttl來判斷數據過期的先后順序,此時可以選擇volatile-ttl策略。 4). 如果希望一些數據能長期被保存,而一些數據可以被淘汰掉時,選擇volatile-lru或volatile-random都是比較不錯的。 5). 由于設置expire會消耗額外的內存,如果計劃避免Redis內存在此項上的浪費,可以選用allkeys-lru 策略,這樣就可以不再設置過期時間,高效利用內存了。延時隊列如何實現,哪幾種方案
項目中的流程監控,有幾種節點,需要監控每一個節點是否超時。按傳統的做法,肯定是通過定時任務,去掃描然后判斷,但是定時任務有缺點:1,數據量大會慢;2,時間不好控制,太短,怕一次處理不完,太長狀態就會有延遲。所以就想到用延遲隊列的方式去實現。
方案一:redis的zset實現延遲隊列
生產者:可以看到生產者很簡單,其實就是利用zset的特性,給一個zset添加元素而已,而時間就是它的score。
消費者:消費者的代碼也不難,就是把已經過期的zset中的元素給刪除掉,然后處理數據。
方案二:rabbitmq通過TTL+死信隊列實現延遲隊列
我們需要建立2個隊列,一個用于發送消息,一個用于消息過期后的轉發目標隊列,生產者輸出消息到Queue1,并且這個消息是設置有有效時間的,比如60s。消息會在Queue1中等待60s,如果沒有消費者收掉的話,它就是被轉發到Queue2,Queue2有消費者,收到,處理延遲任務。
方案三:rocketmq 臨時存儲+定時任務。
RocketMQ是支持延時消息的,只需要在生產消息的時候設置消息的延時級別,Broker收到延時消息了,會先發送到主題(SCHEDULE_TOPIC_XXXX)的相應時間段的Message Queue中,然后通過一個定時任務輪詢這些隊列,到期后,把消息投遞到目標Topic的隊列中,然后消費者就可以正常消費這些消息。
紅鎖
主從模式問題:有一個嚴重的單點失敗問題:如果Redis掛了怎么辦?你可能會說,可以通過增加一個slave節點解決這個問題。但這通常是行不通的。這樣做,我們不能實現資源的獨享,因為Redis的主從同步通常是異步的
客戶端A從master獲取到鎖
在master將鎖同步到slave之前,master宕掉了。
slave節點被晉級為master節點
客戶端B從新的master獲取到鎖
這個鎖對應的資源之前已經被客戶端A已經獲取到了。安全失效!
解決方案:紅鎖采用主節點過半機制,即獲取鎖或者釋放鎖成功的標志為:在過半的節點上操作成功。
獲取當前的時間(單位是毫秒)。
使用相同的key和隨機值在N個節點上請求鎖。這里獲取鎖的嘗試時間要遠遠小于鎖的超時時間,防止某個masterDown了,我們還在不斷的獲取鎖,而被阻塞過長的時間。
只有在大多數節點上獲取到了鎖,而且總的獲取時間小于鎖的超時時間的情況下,認為鎖獲取成功了。
如果鎖獲取成功了,鎖的超時時間就是最初的鎖超時時間進去獲取鎖的總耗時時間。
如果鎖獲取失敗了,不管是因為獲取成功的節點的數目沒有過半,還是因為獲取鎖的耗時超過了鎖的釋放時間,都會將已經設置了key的master上的key刪除。
網絡IO模型或者redis為什么是單線程
Redis單線程:所謂的單線程是指從網絡連接(accept) -> 讀取請求內容(read) -> 執行命令 -> 響應內容(write),這整個過程是由一個線程完成的。
我們在傳統的I/O模型中,如果服務端需要支持多個客戶端,我們可能要為每個客戶端分配一個進程/線程。
不管是基于重一點的進程模型,還是輕一點的線程模型,假如連接多了,操作系統是扛不住的。
所以就引入了I/O多路復用 技術。
簡單說,就是一個進程/線程維護多個Socket,這個多路復用就是多個連接復用一個進程/線程。
我們來看看I/O多路復用三種實現機制:
- select
select 實現多路復?的?式是:
將已連接的 Socket 都放到?個?件描述符集合fd_set,然后調? select 函數將fd_set集合拷?到內核?,讓內核來檢查是否有?絡事件產?,檢查的?式很粗暴,就是通過遍歷fd_set的?式,當檢查到有事件產?后,將此 Socket 標記為可讀或可寫, 接著再把整個fd_set拷?回?戶態?,然后?戶態還需要再通過遍歷的?法找到可讀或可寫的 Socket,再對其處理。
select 使?固定?度的 BitsMap,表示?件描述符集合,?且所?持的?件描述符的個數是有限制的,在Linux 系統中,由內核中的 FD_SETSIZE 限制, 默認最?值為 1024 ,只能監聽 0~1023 的?件描述符。
select機制的缺點:
(1)每次調用select,都需要把fd_set集合從用戶態拷貝到內核態,如果fd_set集合很大時,那這個開銷也很大,比如百萬連接卻只有少數活躍連接時這樣做就太沒有效率。
(2)每次調用select都需要在內核遍歷傳遞進來的所有fd_set,如果fd_set集合很大時,那這個開銷也很大。
(3)為了減少數據拷貝帶來的性能損壞,內核對被監控的fd_set集合大小做了限制,一般為1024,如果想要修改會比較麻煩,可能還需要編譯內核。
(4)每次調用select之前都需要遍歷設置監聽集合,重復工作。
- poll
poll 不再? BitsMap 來存儲所關注的?件描述符,取?代之?動態數組,以鏈表形式來組織,突破了select 的?件描述符個數限制,當然還會受到系統?件描述符限制。
但是 poll 和 select 并沒有太?的本質區別,都是使?線性結構存儲進程關注的Socket集合,因此都需要遍歷?件描述符集合來找到可讀或可寫的Socke,時間復雜度為O(n),?且也需要在?戶態與內核態之間拷??件描述符集合,這種?式隨著并發數上來,性能的損耗會呈指數級增?。
- epoll
epoll 通過兩個??,很好解決了 select/poll 的問題。
第?點,epoll 在內核?使?紅?樹來跟蹤進程所有待檢測的?件描述字,把需要監控的 socket 通過epoll_ctl() 函數加?內核中的紅?樹?,紅?樹是個?效的數據結構,增刪查?般時間復雜度是O(logn) ,通過對這棵?紅樹進?操作,這樣就不需要像 select/poll 每次操作時都傳?整個 socket 集合,只需要傳??個待檢測的 socket,減少了內核和?戶空間?量的數據拷?和內存分配。
第?點, epoll 使?事件驅動的機制,內核?維護了?個鏈表來記錄就緒事件,當某個 socket 有事件發?時,通過回調函數,內核會將其加?到這個就緒事件列表中,當?戶調? epoll_wait() 函數時,只會返回有事件發?的?件描述符的個數,不需要像 select/poll 那樣輪詢掃描整個 socket 集合,??提?了檢測的效率。
epoll 的?式即使監聽的 Socket 數量越多的時候,效率不會?幅度降低,能夠同時監聽的 Socket 的數?也?常的多了,上限就為系統定義的進程打開的最??件描述符個數。因?,epoll 被稱為解決 C10K 問題的利器。
Redis 是單線程,主要是指 Redis 的網絡 IO 和鍵值對讀寫是由一個線程來完成的,這也是 Redis 對外提供鍵值存儲服務的主要流程。但 Redis 的其他功能,比如持久化、異步刪除、集群數據同步等,其實是由額外的線程執行的。
布隆過濾器
布隆過濾器的原理是,當一個元素被加入集合時,通過K個散列函數將這個元素映射成一個位數組中的K個點,把它們置為1。檢索時,我們只要看看這些點是不是都是1就(大約)知道集合中有沒有它了:如果這些點有任何一個0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。這就是布隆過濾器的基本思想。
某樣東西一定不存在或者可能存在
假如,數據庫中有100條訂單數據,id分別從1到100,現在用戶通過id查詢訂單數據,為了防止緩存穿透,我們需要提前做一個bloom filter(一個自定義的bit數組長度為1000,并自定義一個hash算法),將數據庫中的所有id提前通過hash算法計算后,放到bit數組中;當用戶的請求到達controller層中,先到這個數組中查詢這個id是否存在,如果存在,在讓請求往下走,訪問redis或mysql;如果不存在則直接返回。
存在問題:假設id=110的數據經過hash函數計算的值也是3,那么當用戶查詢這條數據時,數據庫不存在這條數據,但是bloom filter查詢后顯示數據存在,所有也會查詢redis和mysql。這就是hash碰撞導致的假陽性
解決方案:可以通過增加hash函數的個數和增加bit數組的長度來解決
Redis為什么那么快
1、完全基于內存,絕大部分請求是純粹的內存操作,非??焖?。數據存在內存中,類似于HashMap,HashMap的優勢就是查找和操作的時間復雜度都是O(1);
2、數據結構簡單,對數據操作也簡單,Redis中的數據結構是專門進行設計的;
3、采用單線程,避免了不必要的上下文切換和競爭條件,也不存在多進程或者多線程導致的切換而消耗 CPU,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因為可能出現死鎖而導致的性能消耗;
4、使用多路I/O復用模型,非阻塞IO;
統計問題HyperLoglog
Redis 在 2.8.9 版本添加了 HyperLogLog 數據結構,用來做基數統計,其優點是在輸入元素的數量非常大時,計算基數所需的空間比較小并且一般比較恒定。
在 Redis 里面,每個 HyperLogLog 鍵只需要花費 12 KB 內存就可以計算接近 2^64 個不同元素的基數。這和計算基數時,元素越多耗費內存越多的集合形成鮮明對比。但是,因為 HyperLogLog 只會根據輸入元素來計算基數,并不會儲存輸入元素本身,所以 HyperLogLog 不能像集合那樣能返回輸入的各個元素。
比如數據集 {1, 3, 5, 7, 5, 7, 8}, 那么這個數據集的基數集為 {1, 3, 5 ,7, 8}, 基數(不重復元素)為5?;鶖倒烙嬀褪窃谡`差可接受的范圍內,快速計算基數。
一致性hash,hash槽
redis cluster采用數據分片的哈希槽來進行數據存儲和數據的讀取。redis cluster一共有2^14(16384)個槽,所有的master節點都會有一個槽區比如0~1000,槽數是可以遷移的。master節點的slave節點不分配槽,只擁有讀權限。但是注意在代碼中redis cluster執行讀寫操作的都是master節點,并不是讀是從節點,寫是主節點。
為什么是16384個槽?
在握手成功后,兩個節點之間會定期發送ping/pong消息,交換數據信息,在redis節點發送心跳包時需要把所有的槽信息放到這個心跳包里,以便讓節點知道當前集群信息,在發送心跳包時使用char進行bitmap壓縮后是2k(16384÷8÷1024=2kb),也就是說使用2k的空間創建了16k的槽數。
雖然使用CRC16算法最多可以分配65535(2^16-1)個槽位,65535=65k,壓縮后就是8k(8 * 8 (8 bit) * 1024(1k) = 8K),也就是說需要需要8k的心跳包,作者認為這樣做不太值得;并且一般情況下一個redis集群不會有超過1000個master節點,所以16k的槽位是個比較合適的選擇。
如果槽位為65536,發送心跳信息的消息頭達8k,發送的心跳包過于龐大。
redis的集群主節點數量基本不可能超過1000個。集群節點越多,心跳包的消息體內攜帶的數據越多。如果節點過1000個,也會導致網絡擁堵。因此redis作者,不建議redis cluster節點數量超過1000個。
槽位越小,節點少的情況下,壓縮率高
Redis主節點的配置信息中,它所負責的哈希槽是通過一張bitmap的形式來保存的,在傳輸過程中,會對bitmap進行壓縮,但是如果bitmap的填充率slots / N很高的話(N表示節點數),bitmap的壓縮率就很低。
如果節點數很少,而哈希槽數量很多的話,bitmap的壓縮率就很低。
和一致性哈希相比
并不是閉合的,key的定位規則是根據CRC-16(key)%16384的值來判斷屬于哪個槽區,從而判斷該key屬于哪個節點,而一致性哈希是根據hash(key)的值來順時針找第一個hash(ip)的節點,從而確定key存儲在哪個節點。
一致性哈希是創建虛擬節點來實現節點宕機后的數據轉移并保證數據的安全性和集群的可用性的。redis cluster是采用master節點有多個slave節點機制來保證數據的完整性的,master節點寫入數據,slave節點同步數據。當master節點掛機后,slave節點會通過選舉機制選舉出一個節點變成master節點,實現高可用。但是這里有一點需要考慮,如果master節點存在熱點緩存,某一個時刻某個key的訪問急劇增高,這時該mater節點可能操勞過度而死,隨后從節點選舉為主節點后,同樣宕機,進入fail狀態。
擴容和縮容
一致性哈希算法在新增和刪除節點后,數據會按照順時針來重新分布節點。而redis cluster的新增和刪除節點都需要手動來分配槽區。
redis客戶端是什么
RedisDesktopManager
redis相比較數據庫優勢
多線程
說說你對原子性、可見性、有序性的理解?
原子性、有序性、可見性是并發編程中非常重要的基礎概念,JMM的很多技術都是圍繞著這三大特性展開。
- 原子性:原子性指的是一個操作是不可分割、不可中斷的,要么全部執行并且執行的過程不會被任何因素打斷,要么就全不執行。
- 可見性:可見性指的是一個線程修改了某一個共享變量的值時,其它線程能夠立即知道這個修改。
- 有序性:有序性指的是對于一個線程的執行代碼,從前往后依次執行,單線程下可以認為程序是有序的,但是并發時有可能會發生指令重排。
分析下面幾行代碼的原子性?
int i = 2; int j = i; i++; i = i + 1;- 第1句是基本類型賦值,是原子性操作。
- 第2句先讀i的值,再賦值到j,兩步操作,不能保證原子性。
- 第3和第4句其實是等效的,先讀取i的值,再+1,最后賦值到i,三步操作了,不能保證原子性。
原子性、可見性、有序性都應該怎么保證呢?
- 原子性:JMM只能保證基本的原子性,如果要保證一個代碼塊的原子性,需要使用synchronized 。
- 可見性:Java是利用volatile關鍵字來保證可見性的,除此之外,final和synchronized也能保證可見性。
- 有序性:synchronized或者volatile都可以保證多線程之間操作的有序性。
synchronize
為什么會需要synchronized?什么場景下使用synchronized?
這個就要說到多線程訪問共享資源了,當一個資源有可能被多個線程同時訪問并修改的話,需要用到鎖。
應用場景:
1 兩個線程同時訪問同一個對象的同步方法
package com.geekmice.sbcmgenerator;import javax.sound.midi.Soundbank;/*** @version: V1.0* @author: pmb* @className: MethodLock* @packageName: com.geekmice.sbcmgenerator* @description:* @date: 2023-03-01 23:20**/ public class MethodLock implements Runnable {private static MethodLock instance = new MethodLock();/*** 同步方法 先執行線程1,四秒鐘之后,執行線程2,再執行四秒。*/private synchronized void method() {System.out.println("線程:" + Thread.currentThread().getName() + ",運行開始");try {Thread.sleep(4000);System.out.println("線程:" + Thread.currentThread().getName() + ",運行結束");} catch (Exception e) {e.printStackTrace();}}@Overridepublic void run() {method();}public static void main(String[] args) {Thread threadOne = new Thread(instance);Thread threadTwo = new Thread(instance);threadOne.start();threadTwo.start();// 方法isAlive() 的功能是判斷當前的線程是否處于活動狀態;活動狀態就是線程已經啟動尚未終止,// 那么這時候線程就是存活的,則返回true,否則則返回falsewhile (threadOne.isAlive() || threadTwo.isAlive()) {}System.out.println("測試結束");} }2 兩個線程同時訪問兩個對象的同步方法
package com.geekmice.sbcmgenerator;/*** @version: V1.0* @author: pmb* @className: ConditionTwo* @packageName: com.geekmice.sbcmgenerator* @description:* @date: 2023-03-01 23:32**/ public class ConditionTwo implements Runnable {static ConditionTwo oneInstance = new ConditionTwo();static ConditionTwo twoInstance = new ConditionTwo();private static synchronized void method() {System.out.println("線程:" + Thread.currentThread().getName() + ",開始");try {Thread.sleep(4000);} catch (Exception e) {e.printStackTrace();}System.out.println("線程:" + Thread.currentThread().getName() + ",結束");}@Overridepublic void run() {method();}public static void main(String[] args) {Thread threadOne = new Thread(oneInstance);Thread threadTwo = new Thread(twoInstance);threadOne.start();threadTwo.start();while (threadOne.isAlive() || threadTwo.isAlive()) {//System.out.println("線程還存活");}System.out.println("測試結束");} }問題在此:
兩個線程(thread1、thread2),訪問兩個對象(instance1、instance2)的同步方法(method()),兩個線程都有各自的鎖,不能形成兩個線程競爭一把鎖的局勢,所以這時,synchronized修飾的方法method()和不用synchronized修飾的效果一樣(不信去把synchronized關鍵字去掉,運行結果一樣),所以此時的method()只是個普通方法。
如何解決這個問題:
若要使鎖生效,只需將method()方法用static修飾,這樣就形成了類鎖,多個實例(instance1、instance2)共同競爭一把類鎖,就可以使兩個線程串行執行了。
synchronized 還有別的作用范圍嗎?
那你了解 synchronized 這三種作用范圍的加鎖方式的區別嗎
這三種作用范圍的區別實際是被加鎖的對象的區別,請看下表:
| 非靜態方法 | 當前對象 => this |
| 靜態方法 | 類對象 => SynchronizedSample.class (一切皆對象,這個是類對象) |
| 代碼塊 | 指定對象 => lock (以上面的代碼為例) |
那你清楚 JVM 是怎么通過synchronized 在對象上實現加鎖,保證多線程訪問競態資源安全的嗎?
先說在JDK6 以前,synchronized 那時還屬于重量級鎖,每次加鎖都依賴操作系統Mutex Lock實現,涉及到操作系統讓線程從用戶態切換到內核態,切換成本很高;
到了JDK6,研究人員引入了偏向鎖和輕量級鎖,因為Sun 程序員發現大部分程序大多數時間都不會發生多個線程同時訪問競態資源的情況,每次線程都加鎖解鎖,每次這么搞都要操作系統在用戶態和內核態之間來回切,太耗性能了。
synchronize理解
synchronized經常用的,用來保證代碼的原子性。
場景:兩個線程分別同時訪問(一個或兩個)對象的同步方法和非同步方法
修飾實例方法: 作用于當前對象實例加鎖,進入同步代碼前要獲得 當前對象實例的鎖
修飾靜態方法:也就是給當前類加鎖,會作?于類的所有對象實例 ,進?同步代碼前要獲得當前 class 的鎖。因為靜態成員不屬于任何?個實例對象,是類成員( static 表明這是該類的?個靜態資源,不管 new 了多少個對象,只有?份)。
如果?個線程 A 調??個實例對象的?靜態 synchronized ?法,?線程 B 需要調?這個實例對象所屬類的靜態 synchronized ?法,是允許的,不會發?互斥現象,因為訪問靜態 synchronized ?法占?的鎖是當前類的鎖,?訪問?靜態 synchronized ?法占?的鎖是當前實例對象鎖。
修飾代碼塊 :指定加鎖對象,對給定對象/類加鎖。 synchronized(this|object) 表示進?同步代碼庫前要獲得給定對象的鎖。 synchronized(類.class) 表示進?同步代碼前要獲得 當前 class 的鎖。
synchronized底層實現
我們使用synchronized的時候,發現不用自己去lock和unlock,是因為JVM幫我們把這個事情做了。
synchronized修飾代碼塊時,JVM采用monitorenter、monitorexit兩個指令來實現同步,monitorenter 指令指向同步代碼塊的開始位置, monitorexit 指令則指向同步代碼塊的結束位置。
反編譯一段synchronized修飾代碼塊代碼,javap -c -s -v -l SynchronizedDemo.class,可以看到相應的字節碼指令。
synchronized修飾同步方法時,JVM采用ACC_SYNCHRONIZED標記符來實現同步,這個標識指明了該方法是一個同步方法。
monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor實現的。
實例對象結構里有對象頭,對象頭里面有一塊結構叫Mark Word,Mark Word指針指向了monitor。
所謂的Monitor其實是一種同步工具,也可以說是一種同步機制。在Java虛擬機(HotSpot)中,Monitor是由ObjectMonitor實現的,可以叫做內部鎖,或者Monitor鎖。
鎖升級的過程
synchronized默認采用的是偏向鎖,然后程序運行過程中始終是只有一個線程去獲取,這個synchronized的這個鎖,那么java對象中記錄一個線程id,我們下次再獲取這個synchronize的鎖時候,只需要比較這個線程id就行了,在運行過程中如果出現第二個線程請求synchronized的鎖時候,分兩種情況,在沒有發生并發競爭鎖情況下,這個synchronized就會自動升級為輕量級鎖,這個時候,第二個線程就會嘗試自旋鎖方式獲取鎖,很快便可以拿到鎖,所以第二個線程也不會阻塞,但是如果出現兩個線程競爭鎖情況,這個synchronize就會升級為重量級鎖,這個時候就是只有一個線程獲取鎖,那么另外一個線程就是阻塞狀態,需要等待第一個線程釋放鎖之后,才能拿到鎖。
synchronized如何使用
- 修飾實例方法
分析:在上面的代碼當中的add方法只有一個簡單的count++操作,因為這個方法是使用synchronized修飾的因此每一個時刻只能有一個線程執行add方法,因此上面打印的結果是200。如果add方法沒有使用synchronized修飾的話,那么線程t1和線程t2就可以同時執行add方法,這可能會導致最終count的結果小于200,因為count++操作不具備原子性。
上面的分析還是比較明確的,但是我們還需要知道的是synchronized修飾的add方法一個時刻只能有一個線程執行的意思是對于一個SyncDemo類的對象來說一個時刻只能有一個線程進入。比如現在有兩個SyncDemo的對象s1和s2,一個時刻只能有一個線程進行s1的add方法,一個時刻只能有一個線程進入s2的add方法,但是同一個時刻可以有兩個不同的線程執行s1和s2的add方法,也就說s1的add方法和s2的add是沒有關系的,一個線程進入s1的add方法并不會阻止另外的線程進入s2的add方法,也就是說synchronized在修飾一個非靜態方法的時候“鎖”住的只是一個實例對象,并不會“鎖”住其它的對象。其實這也很容易理解,一個實例對象是一個獨立的個體別的對象不會影響他,他也不會影響別的對象。
- 修飾靜態方法
上面的代碼最終輸出的結果也是200,但是與前一個程序不同的是。這里的add方法用static修飾的,在這種情況下真正的只能有一個線程進入到add代碼塊,因為用static修飾的話是所有對象公共的,因此和前面的那種情況不同,不存在兩個不同的線程同一時刻執行add方法。
你仔細想想如果能夠讓兩個不同的線程執行add代碼塊,那么count++的執行就不是原子的了。那為什么沒有用static修飾的代碼為什么可以呢?因為當沒有用static修飾時,每一個對象的count都是不同的,內存地址不一樣,因此在這種情況下count++這個操作仍然是原子的!
說說synchronized和ReentrantLock的區別?
- 鎖的實現: synchronized是Java語言的關鍵字,基于JVM實現。而ReentrantLock是基于JDK的API層面實現的(一般是lock()和unlock()方法配合try/finally 語句塊來完成。)
- ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
- synchronized與wait()和notify()/notifyAll()方法結合實現等待/通知機制,ReentrantLock類借助Condition接口與newCondition()方法實現。
- ReentrantLock需要手工聲明來加鎖和釋放鎖,一般跟finally配合釋放鎖。而synchronized不用手動釋放鎖。
synchronized如何保證原子性、可見性、有序性
原子性: 即一個操作或者多個操作 要么全部執行并且執行的過程不會被任何因素打斷,要么就都不執行。
Java內存模型提供了字節碼指令monitorenter和monitorexit來隱式的使用這兩個操作,在synchronized塊之間的操作是具備原子性的。
線程1在執行monitorenter指令的時候,會對Monitor進行加鎖,加鎖后其他線程無法獲得鎖,除非線程1主動解鎖。即使在執行過程中,由于某種原因,比如CPU時間片用完,線程1放棄了CPU,但是它并沒有進行解鎖。而由于synchronized的鎖是可重入的,下一個時間片還是只能被他自己獲取到,還是會繼續執行代碼。直到所有代碼執行完。這就保證了原子性。
有序性: 程序執行的順序按照代碼的先后順序執行。
在并發時,程序的執行可能會出現亂序。給人的直觀感覺就是:寫在前面的代碼,會在后面執行。但是synchronized提供了有序性保證,這其實和as-if-serial語義有關。
as-if-serial語義是指不管怎么重排序(編譯器和處理器為了提高并行度),單線程程序的執行結果都不能被改變。編譯器和處理器無論如何優化,都必須遵守as-if-serial語義。只要編譯器和處理器都遵守了這個語義,那么就可以認為單線程程序是按照順序執行的,由于synchronized修飾的代碼,同一時間只能被同一線程訪問。那么可以認為是單線程執行的。所以可以保證其有序性。
但是需要注意的是synchronized雖然能夠保證有序性,但是無法禁止指令重排和處理器優化的。
可見性: 當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
被synchronized修飾的代碼,在開始執行時會加鎖,執行完成后會進行解鎖,但在一個變量解鎖之前,必須先把此變量同步回主存中,這樣解鎖后,后續其它線程就可以訪問到被修改后的值,從而保證可見性。
ThreadLocal
你在工作中用到過ThreadLocal嗎?
場景:有用到過的,用來做用戶信息上下文的存儲。
我們的系統應用是一個典型的MVC架構,登錄后的用戶每次訪問接口,都會在請求頭中攜帶一個token,在控制層可以根據這個token,解析出用戶的基本信息。那么問題來了,假如在服務層和持久層都要用到用戶信息,比如rpc調用、當前用戶獲取等等,那應該怎么辦呢?
一種辦法是顯式定義用戶相關的參數,比如賬號、用戶名……這樣一來,我們可能需要大面積地修改代碼,多少有點瓜皮,那該怎么辦呢?
這時候我們就可以用到ThreadLocal,在控制層攔截請求把用戶信息存入ThreadLocal,這樣我們在任何一個地方,都可以取出ThreadLocal中存的用戶數據。
每個線程內需要保存類似于全局變量的信息(例如在攔截器中獲取的用戶信息),可以讓不同方法直接使用,避免參數傳遞的麻煩卻不想被多線程共享(因為不同線程獲取到的用戶信息不一樣)。
例如,用 ThreadLocal 保存一些業務內容(用戶權限信息、從用戶系統獲取到的用戶名、用戶ID 等),這些信息在同一個線程內相同,但是不同的線程使用的業務內容是不相同的。
在線程生命周期內,都通過這個靜態 ThreadLocal 實例的 get() 方法取得自己 set 過的那個對象,避免了將這個對象(如 user 對象)作為參數傳遞的麻煩。
ThreadLocal怎么實現的呢?
我們看一下ThreadLocal的set(T)方法,發現先獲取到當前線程,再獲取ThreadLocalMap,然后把元素存到這個map中。
public void set(T value) {//獲取當前線程Thread t = Thread.currentThread();//獲取ThreadLocalMapThreadLocalMap map = getMap(t);//講當前元素存入mapif (map != null)map.set(this, value);elsecreateMap(t, value);}ThreadLocal實現的秘密都在這個ThreadLocalMap了,可以Thread類中定義了一個類型為ThreadLocal.ThreadLocalMap的成員變量threadLocals。
public class Thread implements Runnable {//ThreadLocal.ThreadLocalMap是Thread的屬性ThreadLocal.ThreadLocalMap threadLocals = null; }ThreadLocalMap既然被稱為Map,那么毫無疑問它是<key,value>型的數據結構。我們都知道map的本質是一個個<key,value>形式的節點組成的數組,那ThreadLocalMap的節點是什么樣的呢?
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;//節點類Entry(ThreadLocal<?> k, Object v) {//key賦值super(k);//value賦值value = v;} }這里的節點,key可以簡單低視作ThreadLocal,value為代碼中放入的值,當然實際上key并不是ThreadLocal本身,而是它的一個弱引用,可以看到Entry的key繼承了 WeakReference(弱引用),再來看一下key怎么賦值的:
public WeakReference(T referent) {super(referent); }key的賦值,使用的是WeakReference的賦值。
所以,怎么回答ThreadLocal原理?要答出這幾個點:
- Thread類有一個類型為ThreadLocal.ThreadLocalMap的實例變量threadLocals,每個線程都有一個屬于自己的ThreadLocalMap。
- ThreadLocalMap內部維護著Entry數組,每個Entry代表一個完整的對象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。
- 每個線程在往ThreadLocal里設置值的時候,都是往自己的ThreadLocalMap里存,讀也是以某個ThreadLocal作為引用,在自己的map里找對應的key,從而實現了線程隔離。
- ThreadLocal本身不存儲值,它只是作為一個key來讓線程往ThreadLocalMap里存取值。
ThreadLocal 內存泄露是怎么回事?
我們先來分析一下使用ThreadLocal時的內存,我們都知道,在JVM中,棧內存線程私有,存儲了對象的引用,堆內存線程共享,存儲了對象實例。
所以呢,棧中存儲了ThreadLocal、Thread的引用,堆中存儲了它們的具體實例。
ThreadLocalMap中使用的 key 為 ThreadLocal 的弱引用。
“弱引用:只要垃圾回收機制一運行,不管JVM的內存空間是否充足,都會回收該對象占用的內存?!?/p>
那么現在問題就來了,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一樣的,它這時候如果不被回收,就會出現這種情況:ThreadLocalMap的key沒了,value還在,這就會造成了內存泄漏問題。
那怎么解決內存泄漏問題呢?
很簡單,使用完ThreadLocal后,及時調用remove()方法釋放內存空間。
ThreadLocal<String> localVariable = new ThreadLocal(); try {localVariable.set("鄙人三某”);…… } finally {localVariable.remove(); }那為什么key還要設計成弱引用?
key設計成弱引用同樣是為了防止內存泄漏。
假如key被設計成強引用,如果ThreadLocal Reference被銷毀,此時它指向ThreadLoca的強引用就沒有了,但是此時key還強引用指向ThreadLoca,就會導致ThreadLocal不能被回收,這時候就發生了內存泄漏的問題。
ThreadLocalMap的結構了解嗎?
ThreadLocalMap雖然被叫做Map,其實它是沒有實現Map接口的,但是結構還是和HashMap比較類似的,主要關注的是兩個要素:元素數組和散列方法。
-
元素數組
一個table數組,存儲Entry類型的元素,Entry是ThreaLocal弱引用作為key,Object作為value的結構。
-
散列方法
散列方法就是怎么把對應的key映射到table數組的相應下標,ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table數組長度減一&運算(相當于取余)。
這里的threadLocalHashCode計算有點東西,每創建一個ThreadLocal對象,它就會新增0x61c88647,這個值很特殊,它是斐波那契數 也叫 黃金分割數。hash增量為 這個數字,帶來的好處就是 hash 分布非常均勻。
private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}ThreadLocalMap怎么解決Hash沖突的?
我們可能都知道HashMap使用了鏈表來解決沖突,也就是所謂的鏈地址法。
ThreadLocalMap沒有使用鏈表,自然也不是用鏈地址法來解決沖突了,它用的是另外一種方式——開放定址法。開放定址法是什么意思呢?簡單來說,就是這個坑被人占了,那就接著去找空著的坑。
如上圖所示,如果我們插入一個value=27的數據,通過 hash計算后應該落入第 4 個槽位中,而槽位 4 已經有了 Entry數據,而且Entry數據的key和當前不相等。此時就會線性向后查找,一直找到 Entry為 null的槽位才會停止查找,把元素放到空的槽中。
在get的時候,也會根據ThreadLocal對象的hash值,定位到table中的位置,然后判斷該槽位Entry對象中的key是否和get的key一致,如果不一致,就判斷下一個位置。
ThreadLocalMap擴容機制了解嗎?
在ThreadLocalMap.set()方法的最后,如果執行完啟發式清理工作后,未清理到任何數據,且當前散列數組中Entry的數量已經達到了列表的擴容閾值(len*2/3),就開始執行rehash()邏輯:
if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();再著看rehash()具體實現:這里會先去清理過期的Entry,然后還要根據條件判斷size >= threshold - threshold / 4 也就是size >= threshold* 3/4來決定是否需要擴容。
private void rehash() {//清理過期EntryexpungeStaleEntries();//擴容if (size >= threshold - threshold / 4)resize(); }//清理過期Entry private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {Entry e = tab[j];if (e != null && e.get() == null)expungeStaleEntry(j);} }接著看看具體的resize()方法,擴容后的newTab的大小為老數組的兩倍,然后遍歷老的table數組,散列方法重新計算位置,開放地址解決沖突,然后放到新的newTab,遍歷完成之后,oldTab中所有的entry數據都已經放入到newTab中了,然后table引用指向newTab
具體代碼:
父子線程怎么共享數據?
父線程能用ThreadLocal來給子線程傳值嗎?毫無疑問,不能。那該怎么辦?
這時候可以用到另外一個類——InheritableThreadLocal 。
使用起來很簡單,在主線程的InheritableThreadLocal實例設置值,在子線程中就可以拿到了。
public class InheritableThreadLocalTest {public static void main(String[] args) {final ThreadLocal threadLocal = new InheritableThreadLocal();// 主線程threadLocal.set("不擅技術");//子線程Thread t = new Thread() {@Overridepublic void run() {super.run();System.out.println("鄙人三某 ," + threadLocal.get());}};t.start();} }那原理是什么呢?
原理很簡單,在Thread類里還有另外一個變量:
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;在Thread.init的時候,如果父線程的inheritableThreadLocals不為空,就把它賦給當前線程(子線程)的inheritableThreadLocals 。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);CAS呢?CAS了解多少?有什么問題
是什么
CAS叫做CompareAndSwap,?較并交換,主要是通過處理器的指令來保證操作的原?性的。
CAS 指令包含 3 個參數:共享變量的內存地址 A、預期的值 B 和共享變量的新值 C。
只有當內存中地址 A 處的值等于 B 時,才能將內存中地址 A 處的值更新為新值 C。作為一條 CPU 指令,CAS 指令本身是能夠保證原子性的 。
應用場景
CAS就是通過一個原子操作,用預期值去和實際值做對比,如果實際值和預期相同,則做更新操作。
如果預期值和實際不同,我們就認為,其他線程更新了這個值,此時不做更新操作。
而且這整個流程是原子性的,所以只要實際值和預期值相同,就能保證這次更新不會被其他線程影響。
保證i++的原子操作,在increase方法上使用了重量級的鎖synchronized,這會導致該方法的性能低下,所有調用該方法的操作都需要同步等待處理。保證i++的原子操作,在increase方法上使用了重量級的鎖synchronized,這會導致該方法的性能低下,所有調用該方法的操作都需要同步等待處理。
public class ThreadSafeTest {private final AtomicInteger counter = new AtomicInteger(0);public int increase(){return counter.addAndGet(1);} }其中,在static靜態代碼塊中,基于Unsafe類獲取value字段相對當前對象的“起始地址”的偏移量,用于后續Unsafe類的處理。
在處理自增的原子操作時,使用的是Unsafe類中的getAndAddInt方法,CAS的實現便是由Unsafe類的該方法提供,從而保證自增操作的原子性。
同時,在AtomicInteger類中,可以看到value值通過volatile進行修飾,保證了該屬性值的線程可見性。在多并發的情況下,一個線程的修改,可以保證到其他線程立馬看到修改后的值。
通過源碼可以看出, AtomicInteger 底層是通過volatile變量和CAS兩者相結合來保證更新數據的原子性。
public class AtomicInteger extends Number implements java.io.Serializable {private static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;static {try {// 用于獲取value字段相對當前對象的“起始地址”的偏移量valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }} ?private volatile int value; ?//返回當前值public final int get() {return value;} ?//遞增加detlapublic final int getAndAdd(int delta) {// 1、this:當前的實例 // 2、valueOffset:value實例變量的偏移量 // 3、delta:當前value要加上的數(value+delta)。return unsafe.getAndAddInt(this, valueOffset, delta);} ?//遞增加1public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;} ... }有什么問題
1 ABA 問題
并發環境下,假設初始條件是A,去修改數據時,發現是A就會執行修改。但是看到的雖然是A,中間可能發生了A變B,B又變回A的情況。此時A已經非彼A,數據即使成功修改,也可能有問題。
怎么解決ABA問題?
- 加版本號
每次修改變量,都在這個變量的版本號上加1,這樣,剛剛A->B->A,雖然A的值沒變,但是它的版本號已經變了,再判斷版本號就會發現此時的A已經被改過了。參考樂觀鎖的版本號,這種做法可以給數據帶上了一種實效性的檢驗。
Java提供了AtomicStampReference類,它的compareAndSet方法首先檢查當前的對象引用值是否等于預期引用,并且當前印戳(Stamp)標志是否等于預期標志,如果全部相等,則以原子方式將引用值和印戳標志的值更新為給定的更新值。
2 循環性能開銷
自旋CAS,如果一直循環執行,一直不成功,會給CPU帶來非常大的執行開銷。
怎么解決循環性能開銷問題?
在Java中,很多使用自旋CAS的地方,會有一個自旋次數的限制,超過一定次數,就停止自旋。
3 只能保證一個變量的原子操作
CAS 保證的是對一個變量執行操作的原子性,如果對多個變量操作時,CAS 目前無法直接保證操作的原子性的。
怎么解決只能保證一個變量的原子操作問題?
- 可以考慮改用鎖來保證操作的原子性
- 可以考慮合并多個變量,將多個變量封裝成一個對象,通過AtomicReference來保證原子性。
線程池理解
線程池: 簡單理解,它就是一個管理線程的池子。
- 它幫我們管理線程,避免增加創建線程和銷毀線程的資源損耗。因為線程其實也是一個對象,創建一個對象,需要經過類加載過程,銷毀一個對象,需要走GC垃圾回收流程,都是需要資源開銷的。
- 提高響應速度。 如果任務到達了,相對于從線程池拿線程,重新去創建一條線程執行,速度肯定慢很多。
- 重復利用。 線程用完,再放回池子,可以達到重復利用的效果,節省資源。
執行流程
- 如果正在運行的線程數量小于 corePoolSize,那么馬上創建線程運行這個任務;
- 如果正在運行的線程數量大于或等于 corePoolSize,那么將這個任務放入隊列;
- 如果這時候隊列滿了,而且正在運行的線程數量小于 maximumPoolSize,那么還是要創建非核心線程立刻運行這個任務;
- 如果隊列滿了,而且正在運行的線程數量大于或等于 maximumPoolSize,那么線程池會根據拒絕策略來對應處理。
- 當一個線程完成任務時,它會從隊列中取下一個任務來執行。
- 當一個線程無事可做,超過一定的時間(keepAliveTime)時,線程池會判斷,如果當前運行的線程數大于 corePoolSize,那么這個線程就被停掉。所以線程池的所有任務完成后,它最終會收縮到 corePoolSize 的大小。
線程池如何配置的,有哪些參數
之前我們有一個和第三方對接的需求,需要向第三方推送數據,引入了多線程來提升數據推送的效率,其中用到了線程池來管理線程。
線程池的參數如下:
-
corePoolSize:線程核心參數選擇了CPU數×2
-
maximumPoolSize:最大線程數選擇了和核心線程數相同
-
keepAliveTime:非核心閑置線程存活時間直接置為0
-
unit:非核心線程保持存活的時間選擇了 TimeUnit.SECONDS 秒
-
workQueue:線程池等待隊列,使用 LinkedBlockingQueue阻塞隊列
同時還用了synchronized 來加鎖,保證數據不會被重復推送
線程池拒絕策略
- AbortPolicy :直接拋出異常,默認使用此策略
- CallerRunsPolicy:用調用者所在的線程來執行任務
- DiscardOldestPolicy:丟棄阻塞隊列里最老的任務,也就是隊列里靠前的任務
- DiscardPolicy :當前任務直接丟棄
想實現自己的拒絕策略,實現RejectedExecutionHandler接口即可。
線程池有哪幾種工作隊列
常用的阻塞隊列主要有以下幾種:
- ArrayBlockingQueue:ArrayBlockingQueue(有界隊列)是一個用數組實現的有界阻塞隊列,按FIFO排序量。
- LinkedBlockingQueue:LinkedBlockingQueue(可設置容量隊列)是基于鏈表結構的阻塞隊列,按FIFO排序任務,容量可以選擇進行設置,不設置的話,將是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool線程池使用了這個隊列
- DelayQueue:DelayQueue(延遲隊列)是一個任務定時周期的延遲執行的隊列。根據指定的執行時間從小到大排序,否則根據插入到隊列的先后排序。newScheduledThreadPool線程池使用了這個隊列。
- PriorityBlockingQueue:PriorityBlockingQueue(優先級隊列)是具有優先級的無界阻塞隊列
- SynchronousQueue:SynchronousQueue(同步隊列)是一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處于阻塞狀態,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool線程池使用了這個隊列。
線程池怎么關閉知道嗎?
可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池。它們的原理是遍歷線程池中的工作線程,然后逐個調用線程的interrupt方法來中斷線程,所以無法響應中斷的任務可能永遠無法終止。
shutdown() 將線程池狀態置為shutdown,并不會立即停止:
shutdownNow() 將線程池狀態置為stop。一般會立即停止,事實上不一定:
shutdown 和shutdownnow簡單來說區別如下:
- shutdownNow()能立即停止線程池,正在跑的和正在等待的任務都停下了。這樣做立即生效,但是風險也比較大。
- shutdown()只是關閉了提交通道,用submit()是無效的;而內部的任務該怎么跑還是怎么跑,跑完再徹底停止線程池。
線程池異常怎么處理知道嗎
如何做線程復用
線程池的線程復用就是通過取 Worker 的 firstTask 或者通過 getTask 方法從 workQueue 中不停地取任務,并直接調用 Runnable 的 run 方法來執行任務,這樣就保證了每個線程都始終在一個循環中,反復獲取任務,然后執行任務,從而實現了線程的復用。
volatile
如何保證可見性和有序性
volatile有兩個作用,保證可見性和有序性。
volatile怎么保證可見性的呢?
相比synchronized的加鎖方式來解決共享變量的內存可見性問題,volatile就是更輕量的選擇,它沒有上下文切換的額外開銷成本。
volatile可以確保對某個變量的更新對其他線程馬上可見,一個變量被聲明為volatile 時,線程在寫入變量時不會把值緩存在寄存器或者其他地方,而是會把值刷新回主內存 當其它線程讀取該共享變量 ,會從主內存重新獲取最新值,而不是使用當前線程的本地內存中的值。
volatile怎么保證有序性的呢?
重排序可以分為編譯器重排序和處理器重排序,valatile保證有序性,就是通過分別限制這兩種類型的重排序。
為了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
為什么不能保證原子性
原子性指的是,當某個線程正在執行某件事情的過程中,是不允許被外來線程打斷的。也就是說,原子性的特點是要么不執行,一旦執行就必須全部執行完畢。而volatile是不能保證原子性的,即執行過程中是可以被其他線程打斷甚至是加塞的。
所以,volatile變量的原子性與synchronized的原子性是不同的。synchronized的原子性是指,只要聲明為synchronized的方法或代碼塊,在執行上就是原子操作的。而volatile是不修飾方法或代碼塊的,它只用來修飾變量,對于單個volatile變量的讀和寫操作都具有原子性,但類似于volatile++這種復合操作不具有原子性。所以volatile的原子性是受限制的。并且在多線程環境中,volatile并不能保證原子性。
如何保證原子性
方式一:方法上加 synchronized 關鍵字。
方式二:利用AtomicInteger類實現原子性
import java.util.concurrent.atomic.AtomicInteger;/*** 變量上加了volatile關鍵字 ,* 但 不能保證原子性 的 解決方式。*/ public class Test2 {volatile int number = 0;//解決方式一:方法上加 synchronized 關鍵字public void add(){number++;}//解決方式二:如下AtomicInteger atomicInteger = new AtomicInteger();public void addMyAtomic(){//每調用一次此方法,加個一。atomicInteger.getAndIncrement();}public static void main(String[] args) {Test2 test2 = new Test2();//創建10個線程for (int i = 0;i < 10;i++){new Thread(() -> {//每個線程執行1001次+1操作for (int j = 0;j<100;j++){test2.add();//調用不能保證原子性的方法test2.addMyAtomic();//調用可以保證原子性的方法。}},"Thread_"+(i+1)).start();}//如果正在運行的線程數>2個(除了main線程和GC線程以外,還有其他線程正在運行)while(Thread.activeCount() >2){Thread.yield();//禮讓其他線程,暫不執行后續程序}System.out.println("執行 1000次 +1操作后,number = "+test2.number);System.out.println("執行 1000次 +1操作后,atomicInteger = "+test2.atomicInteger);} }你先跟我舉幾個實際volatile 實際項目中的例子
比如我們工程中經常用一個變量標識程序是否啟動、初始化完成、是否停止等
volatile 很適合只有一個線程修改,其他線程讀取的情況。volatile 變量被修改之后,對其他線程立即可見。
比如這是一個帶前端交互的系統,有A、 B二個線程,用戶點了停止應用按鈕,A 線程調用shutdown() 方法,讓變量shutdown 從false 變成 true,但是因為沒有使用volatile 修飾, B 線程可能感知不到shutdown 的變化,而繼續執行 doWork 內的循環,這樣違背了程序的意愿:當shutdown 變量為true 時,代表應用該停下了,doWork函數應該跳出循環,不再執行。
線程通信方式
- volatile 基于共享內存
關鍵字volatile可以用來修飾字段(成員變量),就是告知程序任何對該變量的訪問均需要從共享內存中獲取,而對它的改變必須同步刷新回共享內存,它能保證所有線程對變量訪問的可見性。
多個線程同時監聽一個變量,當該變量發生變化的時候,線程能夠感知并執行相應的業務。這是最簡單的一種實現方式
@Testpublic void t1() {List<String> list = new ArrayList<>();//線程AThread threadA = new Thread(() -> {for (int i = 1; i <= 10; i++) {list.add("abc");System.out.println("線程A添加元素,此時list的size為:" + list.size());try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}if (list.size() == 5)notice = true;}});//線程BThread threadB = new Thread(() -> {while (true) {if (notice) {System.out.println("線程B收到通知,開始執行自己的業務...");break;}}});//需要先啟動線程BthreadB.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 再啟動線程AthreadA.start();}- wait,notify,notifyall
注意:wait()/notify()/notifyAll() 必須配合 synchronized 使用,wait 方法釋放鎖,notify 方法不釋放鎖。wait 是指在一個已經進入了同步鎖的線程內,讓自己暫時讓出同步鎖,以便其他正在等待此鎖的線程可以得到同步鎖并運行,只有其他線程調用了notify()notify并不釋放鎖,只是告訴調用過wait()的線程可以去參與獲得鎖的競爭了,但不會馬上得到鎖,因為鎖還在別人手里,別人還沒釋放,調用 wait() 的一個或多個線程才會解除 wait 狀態,重新參與競爭對象鎖,程序如果可以再次得到鎖,就可以繼續向下運行
public class TestSync {public static void main(String[] args) {//定義一個鎖對象Object lock = new Object();List<String> list = new ArrayList<>();// 線程AThread threadA = new Thread(() -> {synchronized (lock) {for (int i = 1; i <= 10; i++) {list.add("abc");System.out.println("線程A添加元素,此時list的size為:" + list.size());try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}if (list.size() == 5)lock.notify();//喚醒B線程}}});//線程BThread threadB = new Thread(() -> {while (true) {synchronized (lock) {if (list.size() != 5) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("線程B收到通知,開始執行自己的業務...");}}});//需要先啟動線程BthreadB.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//再啟動線程AthreadA.start();} }juc并發包
CountdownLatch 實現計數器功能,可以用來控制等待多個線程執行任務后進行匯總。
AtomicInteger類是系統底層保護的int類型,通過對int類型的數據進行封裝,提供執行方法的控制進行值的原子操作,AtomicInteger它不能當作Integer來使用,想讓線程安全,往往可能需要通過加鎖的方式去保證線程安全,但是,加鎖對性能會有很大的影響;而AtomicInteger原子類型就是讓程序在不加鎖的時候也能保障線程安全。
static int b =0;public static void main(String[] args) throws InterruptedException {AtomicInteger a = new AtomicInteger(0);Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {a.incrementAndGet();b++;}}});t1.start();Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {a.incrementAndGet();b++;}}});t2.start();Thread.sleep(1000);System.out.println("a="+a);System.out.println("b="+b);}Tomcat線程池
Tomcat 的實現就是為了,線程池即使核心線程數滿了以后,且使用無界隊列的時候,線程池依然有機會創建新的線程,直到達到線程池的最大線程數。
處理邏輯
如果當前運行的線程,少于corePoolSize,則創建一個新的線程來執行任務。
如果線程數大于 corePoolSize了,Tomcat 的線程不會直接把線程加入到無界的阻塞隊列中,而是去判斷,submittedCount(已經提交線程數)是否等于 maximumPoolSize。
如果等于,表示線程池已經滿負荷運行,不能再創建線程了,直接把線程提交到隊列,
如果不等于,則需要判斷,是否有空閑線程可以消費。
如果有空閑線程則加入到阻塞隊列中,等待空閑線程消費。
如果沒有空閑線程,嘗試創建新的線程。(這一步保證了使用無界隊列,仍然可以利用線程的 maximumPoolSize)。
如果總線程數達到 maximumPoolSize,則繼續嘗試把線程加入 BlockingQueue 中。
如果 BlockingQueue 達到上限(假如設置了上限),被默認線程池啟動拒絕策略,tomcat 線程池會 catch 住拒絕策略拋出的異常,再次把嘗試任務加入中 BlockingQueue 中。
再次加入失敗,啟動拒絕策略。
Java中的四大引用
1 強引用
我們平常使用new操作符來創建的對象就是強引用對象,只要有一個引用存在,垃圾回收器永遠不可能回收具有強引用的對象。
2 軟引用
軟引用是用來描述一些還有用但并非必須的對象。當內存充足時,垃圾回收器不會清理具有軟引用的對象,只有當內存不足時垃圾回收器才會去清理這些對象,如果清理完軟引用的對象后內存還是不足才會拋出異常。
3 弱引用
無論內存夠不夠,只要垃圾回收器啟動,弱引用關聯的對象肯定被回收。
4 虛引用
虛引用,又稱作幻象引用,如果一個對象具有虛引用,那么它和沒有任何引用一樣,被虛引用關聯的對象引用通過get方法獲取到的永遠為null,也就是說這種對象在任何時候都有可能被垃圾回收器回收,通過這種方式關聯的對象也無法調用對象中的方法。虛引用主要是用來管理堆外內存的,通過ReferenceQueue這個類實現,當一個對象被回收的時候,會向這個引用隊列里面添加相關數據,給一個通知。
RocketMQ
2.為什么要選擇RocketMQ?
市場上幾大消息隊列對比如下:
總結一下:
選擇中間件的可以從這些維度來考慮:可靠性,性能,功能,可運維行,可拓展性,社區活躍度。目前常用的幾個中間件,ActiveMQ作為“老古董”,市面上用的已經不多,其它幾種:
- RabbitMQ:
- 優點:輕量,迅捷,容易部署和使用,擁有靈活的路由配置
- 缺點:性能和吞吐量不太理想,不易進行二次開發
- RocketMQ:
- 優點:性能好,高吞吐量,穩定可靠,有活躍的中文社區
- 缺點:兼容性上不是太好
- Kafka:
- 優點:擁有強大的性能及吞吐量,兼容性很好
- 缺點:由于“攢一波再處理”導致延遲比較高
我們的系統是面向用戶的C端系統,具有一定的并發量,對性能也有比較高的要求,所以選擇了低延遲、吞吐量比較高,可用性比較好的RocketMQ。
3.RocketMQ有什么優缺點?
RocketMQ優點:
- 單機吞吐量:十萬級
- 可用性:非常高,分布式架構
- 消息可靠性:經過參數優化配置,消息可以做到0丟失
- 功能支持:MQ功能較為完善,還是分布式的,擴展性好
- 支持10億級別的消息堆積,不會因為堆積導致性能下降
- 源碼是Java,方便結合公司自己的業務二次開發
- 天生為金融互聯網領域而生,對于可靠性要求很高的場景,尤其是電商里面的訂單扣款,以及業務削峰,在大量交易涌入時,后端可能無法及時處理的情況
- RoketMQ在穩定性上可能更值得信賴,這些業務場景在阿里雙11已經經歷了多次考驗,如果你的業務有上述并發場景,建議可以選擇RocketMQ
RocketMQ缺點:
- 支持的客戶端語言不多,目前是Java及c++,其中c++不成熟
- 沒有在 MQ核心中去實現JMS等接口,有些系統要遷移需要修改大量代碼
4.消息隊列有哪些消息模型?
消息隊列有兩種模型:隊列模型和發布/訂閱模型。
- 隊列模型
這是最初的一種消息隊列模型,對應著消息隊列“發-存-收”的模型。生產者往某個隊列里面發送消息,一個隊列可以存儲多個生產者的消息,一個隊列也可以有多個消費者,但是消費者之間是競爭關系,也就是說每條消息只能被一個消費者消費。
- 發布/訂閱模型
如果需要將一份消息數據分發給多個消費者,并且每個消費者都要求收到全量的消息。很顯然,隊列模型無法滿足這個需求。解決的方式就是發布/訂閱模型。
在發布 - 訂閱模型中,消息的發送方稱為發布者(Publisher),消息的接收方稱為訂閱者(Subscriber),服務端存放消息的容器稱為主題(Topic)。發布者將消息發送到主題中,訂閱者在接收消息之前需要先“訂閱主題”?!坝嗛啞痹谶@里既是一個動作,同時還可以認為是主題在消費時的一個邏輯副本,每份訂閱中,訂閱者都可以接收到主題的所有消息。
它和 “隊列模式” 的異同:生產者就是發布者,隊列就是主題,消費者就是訂閱者,無本質區別。唯一的不同點在于:一份消息數據是否可以被多次消費。
5.那RocketMQ的消息模型呢?
RocketMQ使用的消息模型是標準的發布-訂閱模型,在RocketMQ的術語表中,生產者、消費者和主題,與發布-訂閱模型中的概念是完全一樣的。
RocketMQ本身的消息是由下面幾部分組成:
- Message
Message(消息)就是要傳輸的信息。
一條消息必須有一個主題(Topic),主題可以看做是你的信件要郵寄的地址。
一條消息也可以擁有一個可選的標簽(Tag)和額處的鍵值對,它們可以用于設置一個業務 Key 并在 Broker 上查找此消息以便在開發期間查找問題。
- Topic
Topic(主題)可以看做消息的歸類,它是消息的第一級類型。比如一個電商系統可以分為:交易消息、物流消息等,一條消息必須有一個 Topic 。
Topic 與生產者和消費者的關系非常松散,一個 Topic 可以有0個、1個、多個生產者向其發送消息,一個生產者也可以同時向不同的 Topic 發送消息。
一個 Topic 也可以被 0個、1個、多個消費者訂閱。
- Tag
Tag(標簽)可以看作子主題,它是消息的第二級類型,用于為用戶提供額外的靈活性。使用標簽,同一業務模塊不同目的的消息就可以用相同 Topic 而不同的 Tag 來標識。比如交易消息又可以分為:交易創建消息、交易完成消息等,一條消息可以沒有 Tag 。
標簽有助于保持你的代碼干凈和連貫,并且還可以為 RocketMQ 提供的查詢系統提供幫助。
- Group
RocketMQ中,訂閱者的概念是通過消費組(Consumer Group)來體現的。每個消費組都消費主題中一份完整的消息,不同消費組之間消費進度彼此不受影響,也就是說,一條消息被Consumer Group1消費過,也會再給Consumer Group2消費。
消費組中包含多個消費者,同一個組內的消費者是競爭消費的關系,每個消費者負責消費組內的一部分消息。默認情況,如果一條消息被消費者Consumer1消費了,那同組的其他消費者就不會再收到這條消息。
- Message Queue
Message Queue(消息隊列),一個 Topic 下可以設置多個消息隊列,Topic 包括多個 Message Queue ,如果一個 Consumer 需要獲取 Topic下所有的消息,就要遍歷所有的 Message Queue。
RocketMQ還有一些其它的Queue——例如ConsumerQueue。
- Offset
在Topic的消費過程中,由于消息需要被不同的組進行多次消費,所以消費完的消息并不會立即被刪除,這就需要RocketMQ為每個消費組在每個隊列上維護一個消費位置(Consumer Offset),這個位置之前的消息都被消費過,之后的消息都沒有被消費過,每成功消費一條消息,消費位置就加一。
也可以這么說,Queue 是一個長度無限的數組,Offset 就是下標。
RocketMQ的消息模型中,這些就是比較關鍵的概念了。畫張圖總結一下:
6.消息的消費模式了解嗎?
消息消費模式有兩種:Clustering(集群消費)和Broadcasting(廣播消費)。
默認情況下就是集群消費,這種模式下一個消費者組共同消費一個主題的多個隊列,一個隊列只會被一個消費者消費,如果某個消費者掛掉,分組內其它消費者會接替掛掉的消費者繼續消費。
而廣播消費消息會發給消費者組中的每一個消費者進行消費。
7.RoctetMQ基本架構了解嗎?
先看圖,RocketMQ的基本架構:
RocketMQ 一共有四個部分組成:NameServer,Broker,Producer 生產者,Consumer 消費者,它們對應了:發現、發、存、收,為了保證高可用,一般每一部分都是集群部署的。
8.那能介紹一下這四部分嗎?
類比一下我們生活的郵政系統——
郵政系統要正常運行,離不開下面這四個角色, 一是發信者,二 是收信者, 三是負責暫存傳輸的郵局, 四是負責協調各個地方郵局的管理機構。對應到 RocketMQ 中,這四個角色就是 Producer、 Consumer、 Broker 、NameServer。
NameServer
NameServer 是一個無狀態的服務器,角色類似于 Kafka使用的 Zookeeper,但比 Zookeeper 更輕量。
特點:
- 每個 NameServer 結點之間是相互獨立,彼此沒有任何信息交互。
- Nameserver 被設計成幾乎是無狀態的,通過部署多個結點來標識自己是一個偽集群,Producer 在發送消息前從 NameServer 中獲取 Topic 的路由信息也就是發往哪個 Broker,Consumer 也會定時從 NameServer 獲取 Topic 的路由信息,Broker 在啟動時會向 NameServer 注冊,并定時進行心跳連接,且定時同步維護的 Topic 到 NameServer。
功能主要有兩個:
- 1、和Broker 結點保持長連接。
- 2、維護 Topic 的路由信息。
Broker
消息存儲和中轉角色,負責存儲和轉發消息。
- Broker 內部維護著一個個 Consumer Queue,用來存儲消息的索引,真正存儲消息的地方是 CommitLog(日志文件)。
- 單個 Broker 與所有的 Nameserver 保持著長連接和心跳,并會定時將 Topic 信息同步到 NameServer,和 NameServer 的通信底層是通過 Netty 實現的。
Producer
消息生產者,業務端負責發送消息,由用戶自行實現和分布式部署。
- Producer由用戶進行分布式部署,消息由Producer通過多種負載均衡模式發送到Broker集群,發送低延時,支持快速失敗。
- RocketMQ 提供了三種方式發送消息:同步、異步和單向
- 同步發送:同步發送指消息發送方發出數據后會在收到接收方發回響應之后才發下一個數據包。一般用于重要通知消息,例如重要通知郵件、營銷短信。
- 異步發送:異步發送指發送方發出數據后,不等接收方發回響應,接著發送下個數據包,一般用于可能鏈路耗時較長而對響應時間敏感的業務場景,例如用戶視頻上傳后通知啟動轉碼服務。
- 單向發送:單向發送是指只負責發送消息而不等待服務器回應且沒有回調函數觸發,適用于某些耗時非常短但對可靠性要求并不高的場景,例如日志收集。
Consumer
消息消費者,負責消費消息,一般是后臺系統負責異步消費。
- Consumer也由用戶部署,支持PUSH和PULL兩種消費模式,支持集群消費和廣播消費,提供實時的消息訂閱機制。
- Pull:拉取型消費者(Pull Consumer)主動從消息服務器拉取信息,只要批量拉取到消息,用戶應用就會啟動消費過程,所以 Pull 稱為主動消費型。
- Push:推送型消費者(Push Consumer)封裝了消息的拉取、消費進度和其他的內部維護工作,將消息到達時執行的回調接口留給用戶應用程序來實現。所以 Push 稱為被動消費類型,但其實從實現上看還是從消息服務器中拉取消息,不同于 Pull 的是 Push 首先要注冊消費監聽器,當監聽器處觸發后才開始消費消息。
進階
9.如何保證消息的可用性/可靠性/不丟失呢?
消息可能在哪些階段丟失呢?可能會在這三個階段發生丟失:生產階段、存儲階段、消費階段。
所以要從這三個階段考慮:
生產
在生產階段,主要通過請求確認機制,來保證消息的可靠傳遞。
- 1、同步發送的時候,要注意處理響應結果和異常。如果返回響應OK,表示消息成功發送到了Broker,如果響應失敗,或者發生其它異常,都應該重試。
- 2、異步發送的時候,應該在回調方法里檢查,如果發送失敗或者異常,都應該進行重試。
- 3、如果發生超時的情況,也可以通過查詢日志的API,來檢查是否在Broker存儲成功。
存儲
存儲階段,可以通過配置可靠性優先的 Broker 參數來避免因為宕機丟消息,簡單說就是可靠性優先的場景都應該使用同步。
- 1、消息只要持久化到CommitLog(日志文件)中,即使Broker宕機,未消費的消息也能重新恢復再消費。
- 2、Broker的刷盤機制:同步刷盤和異步刷盤,不管哪種刷盤都可以保證消息一定存儲在pagecache中(內存中),但是同步刷盤更可靠,它是Producer發送消息后等數據持久化到磁盤之后再返回響應給Producer。
- 3、Broker通過主從模式來保證高可用,Broker支持Master和Slave同步復制、Master和Slave異步復制模式,生產者的消息都是發送給Master,但是消費既可以從Master消費,也可以從Slave消費。同步復制模式可以保證即使Master宕機,消息肯定在Slave中有備份,保證了消息不會丟失。
消費
從Consumer角度分析,如何保證消息被成功消費?
- Consumer保證消息成功消費的關鍵在于確認的時機,不要在收到消息后就立即發送消費確認,而是應該在執行完所有消費業務邏輯之后,再發送消費確認。因為消息隊列維護了消費的位置,邏輯執行失敗了,沒有確認,再去隊列拉取消息,就還是之前的一條。
10.如何處理消息重復的問題呢?
對分布式消息隊列來說,同時做到確保一定投遞和不重復投遞是很難的,就是所謂的“有且僅有一次” 。RocketMQ擇了確保一定投遞,保證消息不丟失,但有可能造成消息重復。
處理消息重復問題,主要有業務端自己保證,主要的方式有兩種:業務冪等和消息去重。
業務冪等:第一種是保證消費邏輯的冪等性,也就是多次調用和一次調用的效果是一樣的。這樣一來,不管消息消費多少次,對業務都沒有影響。
消息去重:第二種是業務端,對重復的消息就不再消費了。這種方法,需要保證每條消息都有一個惟一的編號,通常是業務相關的,比如訂單號,消費的記錄需要落庫,而且需要保證和消息確認這一步的原子性。
具體做法是可以建立一個消費記錄表,拿到這個消息做數據庫的insert操作。給這個消息做一個唯一主鍵(primary key)或者唯一約束,那么就算出現重復消費的情況,就會導致主鍵沖突,那么就不再處理這條消息。
11.怎么處理消息積壓?
發生了消息積壓,這時候就得想辦法趕緊把積壓的消息消費完,就得考慮提高消費能力,一般有兩種辦法:
- 消費者擴容:如果當前Topic的Message Queue的數量大于消費者數量,就可以對消費者進行擴容,增加消費者,來提高消費能力,盡快把積壓的消息消費玩。
- 消息遷移Queue擴容:如果當前Topic的Message Queue的數量小于或者等于消費者數量,這種情況,再擴容消費者就沒什么用,就得考慮擴容Message Queue。可以新建一個臨時的Topic,臨時的Topic多設置一些Message Queue,然后先用一些消費者把消費的數據丟到臨時的Topic,因為不用業務處理,只是轉發一下消息,還是很快的。接下來用擴容的消費者去消費新的Topic里的數據,消費完了之后,恢復原狀。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-x9di1wua-1678634582882)(null)]
12.順序消息如何實現?
順序消息是指消息的消費順序和產生順序相同,在有些業務邏輯下,必須保證順序,比如訂單的生成、付款、發貨,這個消息必須按順序處理才行。
順序消息分為全局順序消息和部分順序消息,全局順序消息指某個 Topic 下的所有消息都要保證順序;
部分順序消息只要保證每一組消息被順序消費即可,比如訂單消息,只要保證同一個訂單 ID 個消息能按順序消費即可。
部分順序消息
部分順序消息相對比較好實現,生產端需要做到把同 ID 的消息發送到同一個 Message Queue ;在消費過程中,要做到從同一個Message Queue讀取的消息順序處理——消費端不能并發處理順序消息,這樣才能達到部分有序。
發送端使用 MessageQueueSelector 類來控制 把消息發往哪個 Message Queue 。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5avDGIQU-1678634576522)(https://camo.githubusercontent.com/a15bef253022797650e9ff6d58995aa2310e5b7ada0dbe459ad2a9e69e663afa/68747470733a2f2f63646e2e746f62656265747465726a61766165722e636f6d2f746f62656265747465726a61766165722f696d616765732f6e6963652d61727469636c652f77656978696e2d6d69616e7a6e78726f636b65746d71657373772d34616164383533652d346464372d346364652d623238642d6236653734636139333331662e6a7067)]
消費端通過使用 MessageListenerOrderly 來解決單 Message Queue 的消息被并發處理的問題。
全局順序消息
RocketMQ 默認情況下不保證順序,比如創建一個 Topic ,默認八個寫隊列,八個讀隊列,這時候一條消息可能被寫入任意一個隊列里;在數據的讀取過程中,可能有多個 Consumer ,每個 Consumer 也可能啟動多個線程并行處理,所以消息被哪個 Consumer 消費,被消費的順序和寫人的順序是否一致是不確定的。
要保證全局順序消息, 需要先把 Topic 的讀寫隊列數設置為 一,然后Producer Consumer 的并發設置,也要是一。簡單來說,為了保證整個 Topic全局消息有序,只能消除所有的并發處理,各部分都設置成單線程處理 ,這時候就完全犧牲RocketMQ的高并發、高吞吐的特性了。
13.如何實現消息過濾?
有兩種方案:
- 一種是在 Broker 端按照 Consumer 的去重邏輯進行過濾,這樣做的好處是避免了無用的消息傳輸到 Consumer 端,缺點是加重了 Broker 的負擔,實現起來相對復雜。
- 另一種是在 Consumer 端過濾,比如按照消息設置的 tag 去重,這樣的好處是實現起來簡單,缺點是有大量無用的消息到達了 Consumer 端只能丟棄不處理。
一般采用Cosumer端過濾,如果希望提高吞吐量,可以采用Broker過濾。
對消息的過濾有三種方式:
- 根據Tag過濾:這是最常見的一種,用起來高效簡單
- SQL 表達式過濾:SQL表達式過濾更加靈活
- Filter Server 方式:最靈活,也是最復雜的一種方式,允許用戶自定義函數進行過濾
14.延時消息了解嗎?
電商的訂單超時自動取消,就是一個典型的利用延時消息的例子,用戶提交了一個訂單,就可以發送一個延時消息,1h后去檢查這個訂單的狀態,如果還是未付款就取消訂單釋放庫存。
RocketMQ是支持延時消息的,只需要在生產消息的時候設置消息的延時級別:
// 實例化一個生產者來產生延時消息 DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); // 啟動生產者 producer.start(); int totalMessagesToSend = 100; for (int i = 0; i < totalMessagesToSend; i++) {Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());// 設置延時等級3,這個消息將在10s之后發送(現在只支持固定的幾個時間,詳看delayTimeLevel)message.setDelayTimeLevel(3);// 發送消息producer.send(message); }但是目前RocketMQ支持的延時級別是有限的:
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";RocketMQ怎么實現延時消息的?
簡單,八個字:臨時存儲+定時任務。
Broker收到延時消息了,會先發送到主題(SCHEDULE_TOPIC_XXXX)的相應時間段的Message Queue中,然后通過一個定時任務輪詢這些隊列,到期后,把消息投遞到目標Topic的隊列中,然后消費者就可以正常消費這些消息。
15.怎么實現分布式消息事務的?半消息?
半消息:是指暫時還不能被 Consumer 消費的消息,Producer 成功發送到 Broker 端的消息,但是此消息被標記為 “暫不可投遞” 狀態,只有等 Producer 端執行完本地事務后經過二次確認了之后,Consumer 才能消費此條消息。
依賴半消息,可以實現分布式消息事務,其中的關鍵在于二次確認以及消息回查:
- 1、Producer 向 broker 發送半消息
- 2、Producer 端收到響應,消息發送成功,此時消息是半消息,標記為 “不可投遞” 狀態,Consumer 消費不了。
- 3、Producer 端執行本地事務。
- 4、正常情況本地事務執行完成,Producer 向 Broker 發送 Commit/Rollback,如果是 Commit,Broker 端將半消息標記為正常消息,Consumer 可以消費,如果是 Rollback,Broker 丟棄此消息。
- 5、異常情況,Broker 端遲遲等不到二次確認。在一定時間后,會查詢所有的半消息,然后到 Producer 端查詢半消息的執行情況。
- 6、Producer 端查詢本地事務的狀態
- 7、根據事務的狀態提交 commit/rollback 到 broker 端。(5,6,7 是消息回查)
- 8、消費者段消費到消息之后,執行本地事務,執行本地事務。
16.死信隊列知道嗎?
死信隊列用于處理無法被正常消費的消息,即死信消息。
當一條消息初次消費失敗,消息隊列 RocketMQ 會自動進行消息重試;達到最大重試次數后,若消費依然失敗,則表明消費者在正常情況下無法正確地消費該消息,此時,消息隊列 RocketMQ 不會立刻將消息丟棄,而是將其發送到該消費者對應的特殊隊列中,該特殊隊列稱為死信隊列。
死信消息的特點:
- 不會再被消費者正常消費。
- 有效期與正常消息相同,均為 3 天,3 天后會被自動刪除。因此,需要在死信消息產生后的 3 天內及時處理。
死信隊列的特點:
- 一個死信隊列對應一個 Group ID, 而不是對應單個消費者實例。
- 如果一個 Group ID 未產生死信消息,消息隊列 RocketMQ 不會為其創建相應的死信隊列。
- 一個死信隊列包含了對應 Group ID 產生的所有死信消息,不論該消息屬于哪個 Topic。
RocketMQ 控制臺提供對死信消息的查詢、導出和重發的功能。
17.如何保證RocketMQ的高可用?
NameServer因為是無狀態,且不相互通信的,所以只要集群部署就可以保證高可用。
RocketMQ的高可用主要是在體現在Broker的讀和寫的高可用,Broker的高可用是通過集群和主從實現的。
Broker可以配置兩種角色:Master和Slave,Master角色的Broker支持讀和寫,Slave角色的Broker只支持讀,Master會向Slave同步消息。
也就是說Producer只能向Master角色的Broker寫入消息,Cosumer可以從Master和Slave角色的Broker讀取消息。
Consumer 的配置文件中,并不需要設置是從 Master 讀還是從 Slave讀,當 Master 不可用或者繁忙的時候, Consumer 的讀請求會被自動切換到從 Slave。有了自動切換 Consumer 這種機制,當一個 Master 角色的機器出現故障后,Consumer 仍然可以從 Slave 讀取消息,不影響 Consumer 讀取消息,這就實現了讀的高可用。
如何達到發送端寫的高可用性呢?在創建 Topic 的時候,把 Topic 的多個Message Queue 創建在多個 Broker 組上(相同 Broker 名稱,不同 brokerId機器組成 Broker 組),這樣當 Broker 組的 Master 不可用后,其他組Master 仍然可用, Producer 仍然可以發送消息 RocketMQ 目前還不支持把Slave自動轉成 Master ,如果機器資源不足,需要把 Slave 轉成 Master ,則要手動停止 Slave 色的 Broker ,更改配置文件,用新的配置文件啟動 Broker。
原理
18.說一下RocketMQ的整體工作流程?
簡單來說,RocketMQ是一個分布式消息隊列,也就是消息隊列+分布式系統。
作為消息隊列,它是發-存-收的一個模型,對應的就是Producer、Broker、Cosumer;作為分布式系統,它要有服務端、客戶端、注冊中心,對應的就是Broker、Producer/Consumer、NameServer
所以我們看一下它主要的工作流程:RocketMQ由NameServer注冊中心集群、Producer生產者集群、Consumer消費者集群和若干Broker(RocketMQ進程)組成:
19.為什么RocketMQ不使用Zookeeper作為注冊中心呢?
Kafka我們都知道采用Zookeeper作為注冊中心——當然也開始逐漸去Zookeeper,RocketMQ不使用Zookeeper其實主要可能從這幾方面來考慮:
20.Broker是怎么保存數據的呢?
RocketMQ主要的存儲文件包括CommitLog文件、ConsumeQueue文件、Indexfile文件。
消息存儲的整體的設計:
- CommitLog:消息主體以及元數據的存儲主體,存儲Producer端寫入的消息主體內容,消息內容不是定長的。單個文件大小默認1G, 文件名長度為20位,左邊補零,剩余為起始偏移量,比如00000000000000000000代表了第一個文件,起始偏移量為0,文件大小為1G=1073741824;當第一個文件寫滿了,第二個文件為00000000001073741824,起始偏移量為1073741824,以此類推。消息主要是順序寫入日志文件,當文件滿了,寫入下一個文件。
CommitLog文件保存于${Rocket_Home}/store/commitlog目錄中,從圖中我們可以明顯看出來文件名的偏移量,每個文件默認1G,寫滿后自動生成一個新的文件。
- ConsumeQueue:消息消費隊列,引入的目的主要是提高消息消費的性能,由于RocketMQ是基于主題topic的訂閱模式,消息消費是針對主題進行的,如果要遍歷commitlog文件中根據topic檢索消息是非常低效的。
Consumer即可根據ConsumeQueue來查找待消費的消息。其中,ConsumeQueue(邏輯消費隊列)作為消費消息的索引,保存了指定Topic下的隊列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。
ConsumeQueue文件可以看成是基于Topic的CommitLog索引文件,故ConsumeQueue文件夾的組織方式如下:topic/queue/file三層組織結構,具體存儲路徑為:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同樣ConsumeQueue文件采取定長設計,每一個條目共20個字節,分別為8字節的CommitLog物理偏移量、4字節的消息長度、8字節tag hashcode,單個文件由30W個條目組成,可以像數組一樣隨機訪問每一個條目,每個ConsumeQueue文件大小約5.72M;
- IndexFile:IndexFile(索引文件)提供了一種可以通過key或時間區間來查詢消息的方法。Index文件的存儲位置是: {fileName},文件名fileName是以創建時的時間戳命名的,固定的單個IndexFile文件大小約為400M,一個IndexFile可以保存 2000W個索引,IndexFile的底層存儲設計為在文件系統中實現HashMap結構,故RocketMQ的索引文件其底層實現為hash索引。
總結一下:RocketMQ采用的是混合型的存儲結構,即為Broker單個實例下所有的隊列共用一個日志數據文件(即為CommitLog)來存儲。
RocketMQ的混合型存儲結構(多個Topic的消息實體內容都存儲于一個CommitLog中)針對Producer和Consumer分別采用了數據和索引部分相分離的存儲結構,Producer發送消息至Broker端,然后Broker端使用同步或者異步的方式對消息刷盤持久化,保存至CommitLog中。
只要消息被刷盤持久化至磁盤文件CommitLog中,那么Producer發送的消息就不會丟失。正因為如此,Consumer也就肯定有機會去消費這條消息。當無法拉取到消息后,可以等下一次消息拉取,同時服務端也支持長輪詢模式,如果一個消息拉取請求未拉取到消息,Broker允許等待30s的時間,只要這段時間內有新消息到達,將直接返回給消費端。
這里,RocketMQ的具體做法是,使用Broker端的后臺服務線程—ReputMessageService不停地分發請求并異步構建ConsumeQueue(邏輯消費隊列)和IndexFile(索引文件)數據。
21.說說RocketMQ怎么對文件進行讀寫的?
RocketMQ對文件的讀寫巧妙地利用了操作系統的一些高效文件讀寫方式——PageCache、順序讀寫、零拷貝。
- PageCache、順序讀取
在RocketMQ中,ConsumeQueue邏輯消費隊列存儲的數據較少,并且是順序讀取,在page cache機制的預讀取作用下,Consume Queue文件的讀性能幾乎接近讀內存,即使在有消息堆積情況下也不會影響性能。而對于CommitLog消息存儲的日志數據文件來說,讀取消息內容時候會產生較多的隨機訪問讀取,嚴重影響性能。如果選擇合適的系統IO調度算法,比如設置調度算法為“Deadline”(此時塊存儲采用SSD的話),隨機讀的性能也會有所提升。
頁緩存(PageCache)是OS對文件的緩存,用于加速對文件的讀寫。一般來說,程序對文件進行順序讀寫的速度幾乎接近于內存的讀寫速度,主要原因就是由于OS使用PageCache機制對讀寫訪問操作進行了性能優化,將一部分的內存用作PageCache。對于數據的寫入,OS會先寫入至Cache內,隨后通過異步的方式由pdflush內核線程將Cache內的數據刷盤至物理磁盤上。對于數據的讀取,如果一次讀取文件時出現未命中PageCache的情況,OS從物理磁盤上訪問讀取文件的同時,會順序對其他相鄰塊的數據文件進行預讀取。
- 零拷貝
另外,RocketMQ主要通過MappedByteBuffer對文件進行讀寫操作。其中,利用了NIO中的FileChannel模型將磁盤上的物理文件直接映射到用戶態的內存地址中(這種Mmap的方式減少了傳統IO,將磁盤文件數據在操作系統內核地址空間的緩沖區,和用戶應用程序地址空間的緩沖區之間來回進行拷貝的性能開銷),將對文件的操作轉化為直接對內存地址進行操作,從而極大地提高了文件的讀寫效率(正因為需要使用內存映射機制,故RocketMQ的文件存儲都使用定長結構來存儲,方便一次將整個文件映射至內存)。
說說什么是零拷貝?
在操作系統中,使用傳統的方式,數據需要經歷幾次拷貝,還要經歷用戶態/內核態切換。
所以,可以通過零拷貝的方式,減少用戶態與內核態的上下文切換和內存拷貝的次數,用來提升I/O的性能。零拷貝比較常見的實現方式是mmap,這種機制在Java中是通過MappedByteBuffer實現的。
22.消息刷盤怎么實現的呢?
RocketMQ提供了兩種刷盤策略:同步刷盤和異步刷盤
- 同步刷盤:在消息達到Broker的內存之后,必須刷到commitLog日志文件中才算成功,然后返回Producer數據已經發送成功。
- 異步刷盤:異步刷盤是指消息達到Broker內存后就返回Producer數據已經發送成功,會喚醒一個線程去將數據持久化到CommitLog日志文件中。
Broker 在消息的存取時直接操作的是內存(內存映射文件),這可以提供系統的吞吐量,但是無法避免機器掉電時數據丟失,所以需要持久化到磁盤中。
刷盤的最終實現都是使用NIO中的 MappedByteBuffer.force() 將映射區的數據寫入到磁盤,如果是同步刷盤的話,在Broker把消息寫到CommitLog映射區后,就會等待寫入完成。
異步而言,只是喚醒對應的線程,不保證執行的時機,流程如圖所示。
22.能說下 RocketMQ 的負載均衡是如何實現的?
RocketMQ中的負載均衡都在Client端完成,具體來說的話,主要可以分為Producer端發送消息時候的負載均衡和Consumer端訂閱消息的負載均衡。
Producer的負載均衡
Producer端在發送消息的時候,會先根據Topic找到指定的TopicPublishInfo,在獲取了TopicPublishInfo路由信息后,RocketMQ的客戶端在默認方式下selectOneMessageQueue()方法會從TopicPublishInfo中的messageQueueList中選擇一個隊列(MessageQueue)進行發送消息。具這里有一個sendLatencyFaultEnable開關變量,如果開啟,在隨機遞增取模的基礎上,再過濾掉not available的Broker代理。
所謂的"latencyFaultTolerance",是指對之前失敗的,按一定的時間做退避。例如,如果上次請求的latency超過550Lms,就退避3000Lms;超過1000L,就退避60000L;如果關閉,采用隨機遞增取模的方式選擇一個隊列(MessageQueue)來發送消息,latencyFaultTolerance機制是實現消息發送高可用的核心關鍵所在。
Consumer的負載均衡
在RocketMQ中,Consumer端的兩種消費模式(Push/Pull)都是基于拉模式來獲取消息的,而在Push模式只是對pull模式的一種封裝,其本質實現為消息拉取線程在從服務器拉取到一批消息后,然后提交到消息消費線程池后,又“馬不停蹄”的繼續向服務器再次嘗試拉取消息。如果未拉取到消息,則延遲一下又繼續拉取。在兩種基于拉模式的消費方式(Push/Pull)中,均需要Consumer端知道從Broker端的哪一個消息隊列中去獲取消息。因此,有必要在Consumer端來做負載均衡,即Broker端中多個MessageQueue分配給同一個ConsumerGroup中的哪些Consumer消費。
在Consumer啟動后,它就會通過定時任務不斷地向RocketMQ集群中的所有Broker實例發送心跳包(其中包含了,消息消費分組名稱、訂閱關系集合、消息通信模式和客戶端id的值等信息)。Broker端在收到Consumer的心跳消息后,會將它維護在ConsumerManager的本地緩存變量—consumerTable,同時并將封裝后的客戶端網絡通道信息保存在本地緩存變量—channelInfoTable中,為之后做Consumer端的負載均衡提供可以依據的元數據信息。
在Consumer實例的啟動流程中的啟動MQClientInstance實例部分,會完成負載均衡服務線程—RebalanceService的啟動(每隔20s執行一次)。
通過查看源碼可以發現,RebalanceService線程的run()方法最終調用的是RebalanceImpl類的rebalanceByTopic()方法,這個方法是實現Consumer端負載均衡的核心。
rebalanceByTopic()方法會根據消費者通信類型為“廣播模式”還是“集群模式”做不同的邏輯處理。這里主要來看下集群模式下的主要處理流程:
(1) 從rebalanceImpl實例的本地緩存變量—topicSubscribeInfoTable中,獲取該Topic主題下的消息消費隊列集合(mqSet);
(2) 根據topic和consumerGroup為參數調用mQClientFactory.findConsumerIdList()方法向Broker端發送通信請求,獲取該消費組下消費者Id列表;
(3) 先對Topic下的消息消費隊列、消費者Id排序,然后用消息隊列分配策略算法(默認為:消息隊列的平均分配算法),計算出待拉取的消息隊列。這里的平均分配算法,類似于分頁的算法,將所有MessageQueue排好序類似于記錄,將所有消費端Consumer排好序類似頁數,并求出每一頁需要包含的平均size和每個頁面記錄的范圍range,最后遍歷整個range而計算出當前Consumer端應該分配到的的MessageQueue。
(4) 然后,調用updateProcessQueueTableInRebalance()方法,具體的做法是,先將分配到的消息隊列集合(mqSet)與processQueueTable做一個過濾比對。
- 上圖中processQueueTable標注的紅色部分,表示與分配到的消息隊列集合mqSet互不包含。將這些隊列設置Dropped屬性為true,然后查看這些隊列是否可以移除出processQueueTable緩存變量,這里具體執行removeUnnecessaryMessageQueue()方法,即每隔1s 查看是否可以獲取當前消費處理隊列的鎖,拿到的話返回true。如果等待1s后,仍然拿不到當前消費處理隊列的鎖則返回false。如果返回true,則從processQueueTable緩存變量中移除對應的Entry;
- 上圖中processQueueTable的綠色部分,表示與分配到的消息隊列集合mqSet的交集。判斷該ProcessQueue是否已經過期了,在Pull模式的不用管,如果是Push模式的,設置Dropped屬性為true,并且調用removeUnnecessaryMessageQueue()方法,像上面一樣嘗試移除Entry;
- 最后,為過濾后的消息隊列集合(mqSet)中的每個MessageQueue創建一個ProcessQueue對象并存入RebalanceImpl的processQueueTable隊列中(其中調用RebalanceImpl實例的computePullFromWhere(MessageQueue mq)方法獲取該MessageQueue對象的下一個進度消費值offset,隨后填充至接下來要創建的pullRequest對象屬性中),并創建拉取請求對象—pullRequest添加到拉取列表—pullRequestList中,最后執行dispatchPullRequest()方法,將Pull消息的請求對象PullRequest依次放入PullMessageService服務線程的阻塞隊列pullRequestQueue中,待該服務線程取出后向Broker端發起Pull消息的請求。其中,可以重點對比下,RebalancePushImpl和RebalancePullImpl兩個實現類的dispatchPullRequest()方法不同,RebalancePullImpl類里面的該方法為空。
消息消費隊列在同一消費組不同消費者之間的負載均衡,其核心設計理念是在一個消息消費隊列在同一時間只允許被同一消費組內的一個消費者消費,一個消息消費者能同時消費多個消息隊列。
23.RocketMQ消息長輪詢了解嗎?
所謂的長輪詢,就是Consumer 拉取消息,如果對應的 Queue 如果沒有數據,Broker 不會立即返回,而是把 PullReuqest hold起來,等待 queue 有了消息后,或者長輪詢阻塞時間到了,再重新處理該 queue 上的所有 PullRequest。
- PullMessageProcessor#processRequest
掛起的請求,有一個服務線程會不停地檢查,看queue中是否有數據,或者超時。
- PullRequestHoldService#run()
總結
以上是生活随笔為你收集整理的2023年郑州春招3年开发面试总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: dspace相关资料
- 下一篇: 德州中级职称计算机考试题,考试资讯 -