大家好,我是小林。
針對校招同學,我是推薦在秋招之前多積累實習經歷,或者多做一些有含金量的項目,除了能增加簡歷競爭之外,還有一個好處,就是能減少面試八股文題量。
因為面試的時間基本都是固定的,一般互聯網公司是 40-60 分鐘,如果這個時間里,問了你很多實習經歷和項目經歷方面的問題,那么所剩的時間就不多了,這樣面試官索性就隨便問幾個八股文,來結束這次面試了,這種面試相當于在你的主場進行面試,準備起來的壓力也不會太大。
相反,如果無實習經歷, 項目也是比較爛大街了,面試官對簡歷上的內容沒什么興趣的,那整場面試只能圍繞八股文來問了,這時候主場就是在面試官那,他問什么你回答什么,面試官容易問到你沒學過的內容。
比如,這位同學字節面經,全程基本上就是八股文的拷打,范圍就是操作系統、網絡協議、mysql、redis、編程語言、算法、場景題。
操作系統
線程和進程的區別?
老八股文,這個都不會,說不過去了。從定義、地址空間、通信、安全性這幾個方面對比回答。
調度和資源:進程是操作系統進行資源分配和調度的基本單位,而線程是進程的執行單元,共享進程的資源。
地址空間:每個進程都有獨立的地址空間,而線程共享所屬進程的地址空間,包括代碼段、數據段、堆。
通信:進程間通信需要額外的機制,如管道、消息隊列、共享內存等,而線程間通信可以直接共享全局變量等數據結構來實現。
安全性:多線程編程需要更小心地處理共享資源,避免出現競態條件和死鎖等問題,而多進程編程由于各進程有獨立的地址空間,相對更容易實現并發安全性,一個進程崩潰不會影響其他進程而一個線程崩潰可能影響整個進程。
進程狀態之間的相互轉化?
一個完整的進程狀態的變遷如下圖:
進程五種狀態的變遷
再來詳細說明一下進程的狀態變遷:
NULL -> 創建狀態:一個新進程被創建時的第一個狀態;
創建狀態 -> 就緒狀態:當進程被創建完成并初始化后,一切就緒準備運行時,變為就緒狀態,這個過程是很快的;
就緒態 -> 運行狀態:處于就緒狀態的進程被操作系統的進程調度器選中后,就分配給 CPU 正式運行該進程;
運行狀態 -> 結束狀態:當進程已經運行完成或出錯時,會被操作系統作結束狀態處理;
運行狀態 -> 就緒狀態:處于運行狀態的進程在運行過程中,由于分配給它的運行時間片用完,操作系統會把該進程變為就緒態,接著從就緒態選中另外一個進程運行;
運行狀態 -> 阻塞狀態:當進程請求某個事件且必須等待時,例如請求 I/O 事件;
阻塞狀態 -> 就緒狀態:當進程要等待的事件完成時,它從阻塞狀態變到就緒狀態;
你了解過哪些io模型?
阻塞I/O模型:應用程序發起I/O操作后會被阻塞,直到操作完成才返回結果。適用于對實時性要求不高的場景。
非阻塞I/O模型:應用程序發起I/O操作后立即返回,不會被阻塞,但需要不斷輪詢或者使用select/poll/epoll等系統調用來檢查I/O操作是否完成。適合于需要進行多路復用的場景,例如需要同時處理多個socket連接的服務器程序。
I/O復用模型:通過select、poll、epoll等系統調用,應用程序可以同時等待多個I/O操作,當其中任何一個I/O操作準備就緒時,應用程序會被通知。適合于需要同時處理多個I/O操作的場景,比如高并發的服務端程序。
信號驅動I/O模型:應用程序發起I/O操作后,可以繼續做其他事情,當I/O操作完成時,操作系統會向應用程序發送信號來通知其完成。適合于需要異步I/O通知的場景,可以提高系統的并發能力。
異步I/O模型:應用程序發起I/O操作后可以立即做其他事情,當I/O操作完成時,應用程序會得到通知。異步I/O模型由操作系統內核完成I/O操作,應用程序只需等待通知即可。適合于需要大量并發連接和高性能的場景,能夠減少系統調用次數,提高系統效率。
有了解過io多路復用嗎?
select 實現多路復用的方式是,將已連接的 Socket 都放到一個文件描述符集合,然后調用 select 函數將文件描述符集合拷貝到內核里,讓內核來檢查是否有網絡事件產生,檢查的方式很粗暴,就是通過遍歷文件描述符集合的方式,當檢查到有事件產生后,將此 Socket 標記為可讀或可寫, 接著再把整個文件描述符集合拷貝回用戶態里,然后用戶態還需要再通過遍歷的方法找到可讀或可寫的 Socket,然后再對其處理。
所以,對于 select 這種方式,需要進行 2 次「遍歷」文件描述符集合,一次是在內核態里,一個次是在用戶態里 ,而且還會發生 2 次「拷貝」文件描述符集合,先從用戶空間傳入內核空間,由內核修改后,再傳出到用戶空間中。
select 使用固定長度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的個數是有限制的,在 Linux 系統中,由內核中的 FD_SETSIZE 限制, 默認最大值為 1024,只能監聽 0~1023 的文件描述符。
poll 不再用 BitsMap 來存儲所關注的文件描述符,取而代之用動態數組,以鏈表形式來組織,突破了 select 的文件描述符個數限制,當然還會受到系統文件描述符限制。
但是 poll 和 select 并沒有太大的本質區別,都是使用「線性結構」存儲進程關注的 Socket 集合,因此都需要遍歷文件描述符集合來找到可讀或可寫的 Socket,時間復雜度為 O(n),而且也需要在用戶態與內核態之間拷貝文件描述符集合,這種方式隨著并發數上來,性能的損耗會呈指數級增長。
poll 通過兩個方面,很好解決了 select/poll 的問題。
第一點,epoll 在內核里使用紅黑樹來跟蹤進程所有待檢測的文件描述字,把需要監控的 socket 通過 epoll_ctl() 函數加入內核中的紅黑樹里,紅黑樹是個高效的數據結構,增刪改一般時間復雜度是 O(logn)。而 select/poll 內核里沒有類似 epoll 紅黑樹這種保存所有待檢測的 socket 的數據結構,所以 select/poll 每次操作時都傳入整個 socket 集合給內核,而 epoll 因為在內核維護了紅黑樹,可以保存所有待檢測的 socket ,所以只需要傳入一個待檢測的 socket,減少了內核和用戶空間大量的數據拷貝和內存分配。
第二點, epoll 使用事件驅動的機制,內核里維護了一個鏈表來記錄就緒事件,當某個 socket 有事件發生時,通過回調函數內核會將其加入到這個就緒事件列表中,當用戶調用 epoll_wait() 函數時,只會返回有事件發生的文件描述符的個數,不需要像 select/poll 那樣輪詢掃描整個 socket 集合,大大提高了檢測的效率。
從下圖你可以看到 epoll 相關的接口作用:
epoll 的方式即使監聽的 Socket 數量越多的時候,效率不會大幅度降低,能夠同時監聽的 Socket 的數目也非常的多了,上限就為系統定義的進程打開的最大文件描述符個數。因而,epoll 被稱為解決 C10K 問題的利器。
管道有幾種方式?
管道在Linux中有兩種方式:匿名管道和命名管道。
匿名管道:是一種在父子進程或者兄弟進程之間進行通信的機制,只能用于具有親緣關系的進程間通信,通常通過pipe系統調用創建。
命名管道:是一種允許無關的進程間進行通信的機制,基于文件系統,可以在不相關的進程之間進行通信。
網絡
網絡模型每一層作用是什么?
img
OSI 網絡模型,該模型主要有 7 層,分別是應用層、表示層、會話層、傳輸層、網絡層、數據鏈路層以及物理層。
應用層,負責給應用程序提供統一的接口;
表示層,負責把數據轉換成兼容另一個系統能識別的格式;
會話層,負責建立、管理和終止表示層實體之間的通信會話;
傳輸層,負責端到端的數據傳輸;
網絡層,負責數據的路由、轉發、分片;
數據鏈路層,負責數據的封幀和差錯檢測,以及 MAC 尋址;
物理層,負責在物理網絡中傳輸數據幀;
TCP/IP 網絡模型共有 4 層,分別是應用層、傳輸層、網絡層和網絡接口層,每一層負責的職能如下:
應用層,負責向用戶提供一組應用程序,比如 HTTP、DNS、FTP 等;
傳輸層,負責端到端的通信,比如 TCP、UDP 等;
網絡層,負責網絡包的封裝、分片、路由、轉發,比如 IP、ICMP 等;
網絡接口層,負責網絡包在物理網絡中的傳輸,比如網絡包的封幀、 MAC 尋址、差錯檢測,以及通過網卡傳輸網絡幀等;
tcp 和 udp區別?
連接:TCP 是面向連接的傳輸層協議,傳輸數據前先要建立連接;UDP 是不需要連接,即刻傳輸數據。
服務對象:TCP 是一對一的兩點服務,即一條連接只有兩個端點。UDP 支持一對一、一對多、多對多的交互通信
可靠性:TCP 是可靠交付數據的,數據可以無差錯、不丟失、不重復、按序到達。UDP 是盡最大努力交付,不保證可靠交付數據。但是我們可以基于 UDP 傳輸協議實現一個可靠的傳輸協議,比如 QUIC 協議
擁塞控制、流量控制:TCP 有擁塞控制和流量控制機制,保證數據傳輸的安全性。UDP 則沒有,即使網絡非常擁堵了,也不會影響 UDP 的發送速率。
首部開銷:TCP 首部長度較長,會有一定的開銷,首部在沒有使用「選項」字段時是 20 個字節,如果使用了「選項」字段則會變長的。UDP 首部只有 8 個字節,并且是固定不變的,開銷較小。
傳輸方式:TCP 是流式傳輸,沒有邊界,但保證順序和可靠。UDP 是一個包一個包的發送,是有邊界的,但可能會丟包和亂序。
應用場景:TCP 是面向連接,能保證數據的可靠性交付,因此經常用于:FTP、HTTP/HTTPS協議。UDP 面向無連接,它可以隨時發送數據,再加上 UDP 本身的處理既簡單又高效,經常用于視頻、音頻等多媒體通信等。
怎么用udp實現http?
HTTP/1 ~ HTTP/3
UDP 是不可靠傳輸的,但基于 UDP 的 QUIC 協議 可以實現類似 TCP 的可靠性傳輸,在http3 就用了 quic 協議。
連接遷移:QUIC支持在網絡變化時快速遷移連接,例如從WiFi切換到移動數據網絡,以保持連接的可靠性。
重傳機制:QUIC使用重傳機制來確保丟失的數據包能夠被重新發送,從而提高數據傳輸的可靠性。
前向糾錯:QUIC可以使用前向糾錯技術,在接收端修復部分丟失的數據,降低重傳的需求,提高可靠性和傳輸效率。
擁塞控制:QUIC內置了擁塞控制機制,可以根據網絡狀況動態調整數據傳輸速率,以避免網絡擁塞和丟包,提高可靠性。
http長連接是什么?
使用同一個 TCP 連接來發送和接收多個 HTTP 請求/應答,避免了連接建立和釋放的開銷,這個方法稱為 HTTP 長連接。
HTTP 長連接
HTTP 長連接的特點是,只要任意一端沒有明確提出斷開連接,則保持 TCP 連接狀態。
MySQL數據庫
mysql為什么選擇b+樹?
MySQL 是會將數據持久化在硬盤,而存儲功能是由 MySQL 存儲引擎實現的,所以討論 MySQL 使用哪種數據結構作為索引,實際上是在討論存儲引使用哪種數據結構作為索引,InnoDB 是 MySQL 默認的存儲引擎,它就是采用了 B+ 樹作為索引的數據結構。
要設計一個 MySQL 的索引數據結構,不僅僅考慮數據結構增刪改的時間復雜度,更重要的是要考慮磁盤 I/0 的操作次數。因為索引和記錄都是存放在硬盤,硬盤是一個非常慢的存儲設備,我們在查詢數據的時候,最好能在盡可能少的磁盤 I/0 的操作次數內完成。
二分查找樹雖然是一個天然的二分結構,能很好的利用二分查找快速定位數據,但是它存在一種極端的情況,每當插入的元素都是樹內最大的元素,就會導致二分查找樹退化成一個鏈表,此時查詢復雜度就會從 O(logn)降低為 O(n)。
為了解決二分查找樹退化成鏈表的問題,就出現了自平衡二叉樹,保證了查詢操作的時間復雜度就會一直維持在 O(logn) 。但是它本質上還是一個二叉樹,每個節點只能有 2 個子節點,隨著元素的增多,樹的高度會越來越高。
而樹的高度決定于磁盤 I/O 操作的次數,因為樹是存儲在磁盤中的,訪問每個節點,都對應一次磁盤 I/O 操作,也就是說樹的高度就等于每次查詢數據時磁盤 IO 操作的次數,所以樹的高度越高,就會影響查詢性能。
B 樹和 B+ 都是通過多叉樹的方式,會將樹的高度變矮,所以這兩個數據結構非常適合檢索存于磁盤中的數據。
但是 MySQL 默認的存儲引擎 InnoDB 采用的是 B+ 作為索引的數據結構。原因有:
B+ 樹的非葉子節點不存放實際的記錄數據,僅存放索引,因此數據量相同的情況下,相比存儲即存索引又存記錄的 B 樹,B+樹的非葉子節點可以存放更多的索引,因此 B+ 樹可以比 B 樹更「矮胖」,查詢底層節點的磁盤 I/O次數會更少。
B+ 樹有大量的冗余節點(所有非葉子節點都是冗余索引),這些冗余索引讓 B+ 樹在插入、刪除的效率都更高,比如刪除根節點的時候,不會像 B 樹那樣會發生復雜的樹的變化;
B+ 樹葉子節點之間用鏈表連接了起來,有利于范圍查詢,而 B 樹要實現范圍查詢,因此只能通過樹的遍歷來完成范圍查詢,這會涉及多個節點的磁盤 I/O 操作,范圍查詢效率不如 B+ 樹。
mysql acid 特性
原子性(Atomicity):一個事務中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節,而且事務在執行過程中發生錯誤,會被回滾到事務開始前的狀態,就像這個事務從來沒有執行過一樣,就好比買一件商品,購買成功時,則給商家付了錢,商品到手;購買失敗時,則商品在商家手中,消費者的錢也沒花出去。
一致性(Consistency):是指事務操作前和操作后,數據滿足完整性約束,數據庫保持一致性狀態。比如,用戶 A 和用戶 B 在銀行分別有 800 元和 600 元,總共 1400 元,用戶 A 給用戶 B 轉賬 200 元,分為兩個步驟,從 A 的賬戶扣除 200 元和對 B 的賬戶增加 200 元。一致性就是要求上述步驟操作后,最后的結果是用戶 A 還有 600 元,用戶 B 有 800 元,總共 1400 元,而不會出現用戶 A 扣除了 200 元,但用戶 B 未增加的情況(該情況,用戶 A 和 B 均為 600 元,總共 1200 元)。
隔離性(Isolation):數據庫允許多個并發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務并發執行時由于交叉執行而導致數據的不一致,因為多個事務同時使用相同的數據時,不會相互干擾,每個事務都有一個完整的數據空間,對其他并發事務是隔離的。也就是說,消費者購買商品這個事務,是不影響其他消費者購買的。
持久性(Durability):事務處理結束后,對數據的修改就是永久的,即便系統故障也不會丟失。
InnoDB 引擎通過什么技術來保證事務的這四個特性的呢?
持久性是通過 redo log (重做日志)來保證的;
原子性是通過 undo log(回滾日志) 來保證的;
隔離性是通過 MVCC(多版本并發控制) 或鎖機制來保證的;
一致性則是通過持久性+原子性+隔離性來保證;
mysql事務的隔離級別有哪些
四個隔離級別如下:
讀未提交),指一個事務還沒提交時,它做的變更就能被其他事務看到;
讀提交,指一個事務提交之后,它做的變更才能被其他事務看到;
可重復讀,指一個事務執行過程中看到的數據,一直跟這個事務啟動時看到的數據是一致的,MySQL InnoDB 引擎的默認隔離級別;
串行化;會對記錄加上讀寫鎖,在多個事務對這條記錄進行讀寫操作時,如果發生了讀寫沖突的時候,后訪問的事務必須等前一個事務執行完成,才能繼續執行;
按隔離水平高低排序如下:
圖片
針對不同的隔離級別,并發事務時可能發生的現象也會不同。
圖片
也就是說:
在「讀未提交」隔離級別下,可能發生臟讀、不可重復讀和幻讀現象;
在「讀提交」隔離級別下,可能發生不可重復讀和幻讀現象,但是不可能發生臟讀現象;
在「可重復讀」隔離級別下,可能發生幻讀現象,但是不可能臟讀和不可重復讀現象;
在「串行化」隔離級別下,臟讀、不可重復讀和幻讀現象都不可能會發生。
讀已提交和可重復讀的區別是什么?
可重復讀隔離級別是啟動事務時生成一個 Read View,然后整個事務期間都在用這個 Read View。這樣就保證了在事務期間讀到的數據都是事務啟動前的記錄。
讀提交隔離級別是在每次讀取數據時,都會生成一個新的 Read View。也意味著,事務期間的多次讀取同一條數據,前后兩次讀的數據可能會出現不一致,因為可能這期間另外一個事務修改了該記錄,并提交了事務。
mvcc怎么實現的?
MVCC 是多版本并發控制,MVCC保證了事務之間的隔離性,事務只能看到已經提交的數據版本,從而保證了數據的一致性,并且避免了事務讀寫并發的問題,因為 select 快照讀是不會加鎖的。
可重復讀隔離級別是開啟事務,執行第一個 select 查詢的時候,會創建 Read View,然后整個事務期間都在用這個 Read View。讀提交隔離級別是在每次select 查詢時,都會生成一個新的 Read View。
在創建 Read View 后,我們可以將記錄中的 trx_id 劃分這三種情況:
一個事務去訪問記錄的時候,除了自己的更新記錄總是可見之外,還有這幾種情況:
如果記錄的 trx_id 值小于 Read View 中的 min_trx_id 值,表示這個版本的記錄是在創建 Read View 前已經提交的事務生成的,所以該版本的記錄對當前事務可見。
如果記錄的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示這個版本的記錄是在創建 Read View 后才啟動的事務生成的,所以該版本的記錄對當前事務不可見。
如果記錄的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id之間,需要判斷 trx_id 是否在 m_ids 列表中:
如果記錄的 trx_id 在 m_ids 列表中,表示生成該版本記錄的活躍事務依然活躍著(還沒提交事務),所以該版本的記錄對當前事務不可見。
如果記錄的 trx_id 不在 m_ids列表中,表示生成該版本記錄的活躍事務已經被提交,所以該版本的記錄對當前事務可見。
binlog,redolog,undolog有什么區別?
binlog(二進制日志):記錄了對數據庫的所有更新操作,包括insert、update、delete等,用于數據恢復、主從復制和數據庫的增量備份。
redo log(重做日志):記錄了數據庫引擎對數據頁的物理修改操作,用于在數據庫發生異常情況時進行恢復,保證數據庫的一致性。
undo log(撤銷日志):記錄了事務執行前的數據版本,用于事務的回滾和MVCC(多版本并發控制),在事務提交前,可以使用undo log還原數據狀態。
mysql 主從復制的過程
MySQL 的主從復制依賴于 binlog ,也就是記錄 MySQL 上的所有變化并以二進制形式保存在磁盤上。復制的過程就是將 binlog 中的數據從主庫傳輸到從庫上。
這個過程一般是異步的,也就是主庫上執行事務操作的線程不會等待復制 binlog 的線程同步完成。
MySQL 主從復制過程
MySQL 集群的主從復制過程梳理成 3 個階段:
寫入 Binlog:主庫寫 binlog 日志,提交事務,并更新本地存儲數據。
同步 Binlog:把 binlog 復制到所有從庫上,每個從庫把 binlog 寫到暫存日志中。
回放 Binlog:回放 binlog,并更新存儲引擎中的數據。
具體詳細過程如下:
MySQL 主庫在收到客戶端提交事務的請求之后,會先寫入 binlog,再提交事務,更新存儲引擎中的數據,事務提交完成后,返回給客戶端“操作成功”的響應。
從庫會創建一個專門的 I/O 線程,連接主庫的 log dump 線程,來接收主庫的 binlog 日志,再把 binlog 信息寫入 relay log 的中繼日志里,再返回給主庫“復制成功”的響應。
從庫會創建一個用于回放 binlog 的線程,去讀 relay log 中繼日志,然后回放 binlog 更新存儲引擎中的數據,最終實現主從的數據一致性。
在完成主從復制之后,你就可以在寫數據時只寫主庫,在讀數據時只讀從庫,這樣即使寫請求會鎖表或者鎖記錄,也不會影響讀請求的執行。
MySQL 主從架構
binlog 兩階段提交過程是怎么樣的?
事務提交后,redo log 和 binlog 都要持久化到磁盤,但是這兩個是獨立的邏輯,可能出現半成功的狀態,這樣就造成兩份日志之間的邏輯不一致。
在 MySQL 的 InnoDB 存儲引擎中,開啟 binlog 的情況下,MySQL 會同時維護 binlog 日志與 InnoDB 的 redo log,為了保證這兩個日志的一致性,MySQL 使用了內部 XA 事務(是的,也有外部 XA 事務,跟本文不太相關,我就不介紹了),內部 XA 事務由 binlog 作為協調者,存儲引擎是參與者。
當客戶端執行 commit 語句或者在自動提交的情況下,MySQL 內部開啟一個 XA 事務,分兩階段來完成 XA 事務的提交,如下圖:
兩階段提交
從圖中可看出,事務的提交過程有兩個階段,就是將 redo log 的寫入拆成了兩個步驟:prepare 和 commit,中間再穿插寫入binlog,具體如下:
prepare 階段:將 XID(內部 XA 事務的 ID) 寫入到 redo log,同時將 redo log 對應的事務狀態設置為 prepare,然后將 redo log 持久化到磁盤(innodb_flush_log_at_trx_commit = 1 的作用);
commit 階段:把 XID 寫入到 binlog,然后將 binlog 持久化到磁盤(sync_binlog = 1 的作用),接著調用引擎的提交事務接口,將 redo log 狀態設置為 commit,此時該狀態并不需要持久化到磁盤,只需要 write 到文件系統的 page cache 中就夠了,因為只要 binlog 寫磁盤成功,就算 redo log 的狀態還是 prepare 也沒有關系,一樣會被認為事務已經執行成功;
我們來看看在兩階段提交的不同時刻,MySQL 異常重啟會出現什么現象?下圖中有時刻 A 和時刻 B 都有可能發生崩潰:
時刻 A 與時刻 B
不管是時刻 A(redo log 已經寫入磁盤, binlog 還沒寫入磁盤),還是時刻 B (redo log 和 binlog 都已經寫入磁盤,還沒寫入 commit 標識)崩潰,此時的 redo log 都處于 prepare 狀態。
在 MySQL 重啟后會按順序掃描 redo log 文件,碰到處于 prepare 狀態的 redo log,就拿著 redo log 中的 XID 去 binlog 查看是否存在此 XID:
如果 binlog 中沒有當前內部 XA 事務的 XID,說明 redolog 完成刷盤,但是 binlog 還沒有刷盤,則回滾事務。對應時刻 A 崩潰恢復的情況。
如果 binlog 中有當前內部 XA 事務的 XID,說明 redolog 和 binlog 都已經完成了刷盤,則提交事務。對應時刻 B 崩潰恢復的情況。
可以看到,對于處于 prepare 階段的 redo log,即可以提交事務,也可以回滾事務,這取決于是否能在 binlog 中查找到與 redo log 相同的 XID,如果有就提交事務,如果沒有就回滾事務。這樣就可以保證 redo log 和 binlog 這兩份日志的一致性了。
所以說,兩階段提交是以 binlog 寫成功為事務提交成功的標識,因為 binlog 寫成功了,就意味著能在 binlog 中查找到與 redo log 相同的 XID。
Redis
redis分布式鎖實現
分布式鎖是用于分布式環境下并發控制的一種機制,用于控制某個資源在同一時刻只能被一個應用所使用。如下圖所示:
img
Redis 本身可以被多個客戶端共享訪問,正好就是一個共享存儲系統,可以用來保存分布式鎖,而且 Redis 的讀寫性能高,可以應對高并發的鎖操作場景。
Redis 的 SET 命令有個 NX 參數可以實現「key不存在才插入」,所以可以用它來實現分布式鎖:
如果 key 不存在,則顯示插入成功,可以用來表示加鎖成功;
如果 key 存在,則會顯示插入失敗,可以用來表示加鎖失敗。
基于 Redis 節點實現分布式鎖時,對于加鎖操作,我們需要滿足三個條件。
加鎖包括了讀取鎖變量、檢查鎖變量值和設置鎖變量值三個操作,但需要以原子操作的方式完成,所以,我們使用 SET 命令帶上 NX 選項來實現加鎖;
鎖變量需要設置過期時間,以免客戶端拿到鎖后發生異常,導致鎖一直無法釋放,所以,我們在 SET 命令執行時加上 EX/PX 選項,設置其過期時間;
鎖變量的值需要能區分來自不同客戶端的加鎖操作,以免在釋放鎖時,出現誤釋放操作,所以,我們使用 SET 命令設置鎖變量值時,每個客戶端設置的值是一個唯一值,用于標識客戶端;
滿足這三個條件的分布式命令如下:
SETlock_keyunique_valueNXPX10000
lock_key 就是 key 鍵;
unique_value 是客戶端生成的唯一的標識,區分來自不同客戶端的鎖操作;
NX 代表只在 lock_key 不存在時,才對 lock_key 進行設置操作;
PX 10000 表示設置 lock_key 的過期時間為 10s,這是為了避免客戶端發生異常而無法釋放鎖。
而解鎖的過程就是將 lock_key 鍵刪除(del lock_key),但不能亂刪,要保證執行操作的客戶端就是加鎖的客戶端。所以,解鎖的時候,我們要先判斷鎖的 unique_value 是否為加鎖客戶端,是的話,才將 lock_key 鍵刪除。
可以看到,解鎖是有兩個操作,這時就需要 Lua 腳本來保證解鎖的原子性,因為 Redis 在執行 Lua 腳本時,可以以原子性的方式執行,保證了鎖釋放操作的原子性。
//釋放鎖時,先比較unique_value是否相等,避免鎖的誤釋放 ifredis.call("get",KEYS[1])==ARGV[1]then returnredis.call("del",KEYS[1]) else return0 end
這樣一來,就通過使用 SET 命令和 Lua 腳本在 Redis 單節點上完成了分布式鎖的加鎖和解鎖。
redis緩存問題 穿透 雪崩 擊穿?
緩存雪崩:當大量緩存數據在同一時間過期(失效)或者 Redis 故障宕機時,如果此時有大量的用戶請求,都無法在 Redis 中處理,于是全部請求都直接訪問數據庫,從而導致數據庫的壓力驟增,嚴重的會造成數據庫宕機,從而形成一系列連鎖反應,造成整個系統崩潰,這就是緩存雪崩的問題。
image.png
緩存擊穿:如果緩存中的某個熱點數據過期了,此時大量的請求訪問了該熱點數據,就無法從緩存中讀取,直接訪問數據庫,數據庫很容易就被高并發的請求沖垮,這就是緩存擊穿的問題。
image.png
緩存穿透:當用戶訪問的數據,既不在緩存中,也不在數據庫中,導致請求在訪問緩存時,發現緩存缺失,再去訪問數據庫時,發現數據庫中也沒有要訪問的數據,沒辦法構建緩存數據,來服務后續的請求。那么當有大量這樣的請求到來時,數據庫的壓力驟增,這就是緩存穿透的問題。
緩存雪崩解決方案:
均勻設置過期時間:如果要給緩存數據設置過期時間,應該避免將大量的數據設置成同一個過期時間。我們可以在對緩存數據設置過期時間時,給這些數據的過期時間加上一個隨機數,這樣就保證數據不會在同一時間過期。
互斥鎖:當業務線程在處理用戶請求時,如果發現訪問的數據不在 Redis 里,就加個互斥鎖,保證同一時間內只有一個請求來構建緩存(從數據庫讀取數據,再將數據更新到 Redis 里),當緩存構建完成后,再釋放鎖。未能獲取互斥鎖的請求,要么等待鎖釋放后重新讀取緩存,要么就返回空值或者默認值。實現互斥鎖的時候,最好設置超時時間,不然第一個請求拿到了鎖,然后這個請求發生了某種意外而一直阻塞,一直不釋放鎖,這時其他請求也一直拿不到鎖,整個系統就會出現無響應的現象。
后臺更新緩存:業務線程不再負責更新緩存,緩存也不設置有效期,而是讓緩存“永久有效”,并將更新緩存的工作交由后臺線程定時更新。
緩存擊穿解決方案:
互斥鎖方案,保證同一時間只有一個業務線程更新緩存,未能獲取互斥鎖的請求,要么等待鎖釋放后重新讀取緩存,要么就返回空值或者默認值。
不給熱點數據設置過期時間,由后臺異步更新緩存,或者在熱點數據準備要過期前,提前通知后臺線程更新緩存以及重新設置過期時間;
緩存穿透解決方案:
非法請求的限制:當有大量惡意請求訪問不存在的數據的時候,也會發生緩存穿透,因此在 API 入口處我們要判斷求請求參數是否合理,請求參數是否含有非法值、請求字段是否存在,如果判斷出是惡意請求就直接返回錯誤,避免進一步訪問緩存和數據庫。
緩存空值或者默認值:當我們線上業務發現緩存穿透的現象時,可以針對查詢的數據,在緩存中設置一個空值或者默認值,這樣后續請求就可以從緩存中讀取到空值或者默認值,返回給應用,而不會繼續查詢數據庫。
布隆過濾器:我們可以在寫入數據庫數據時,使用布隆過濾器做個標記,然后在用戶請求到來時,業務線程確認緩存失效后,可以通過查詢布隆過濾器快速判斷數據是否存在,如果不存在,就不用通過查詢數據庫來判斷數據是否存在。即使發生了緩存穿透,大量請求只會查詢 Redis 和布隆過濾器,而不會查詢數據庫,保證了數據庫能正常運行,Redis 自身也是支持布隆過濾器的。
redis寫回策略你了解哪些?
常見的緩存更新策略共有3種:
Cache Aside(旁路緩存)策略;
Read/Write Through(讀穿 / 寫穿)策略;
Write Back(寫回)策略;
實際開發中,Redis 和 MySQL 的更新策略用的是 Cache Aside,另外兩種策略應用不了。
Cache Aside(旁路緩存)策略
Cache Aside(旁路緩存)策略是最常用的,應用程序直接與「數據庫、緩存」交互,并負責對緩存的維護,該策略又可以細分為「讀策略」和「寫策略」。
寫策略的步驟:
先更新數據庫中的數據,再刪除緩存中的數據。
讀策略的步驟:
如果讀取的數據命中了緩存,則直接返回數據;
如果讀取的數據沒有命中緩存,則從數據庫中讀取數據,然后將數據寫入到緩存,并且返回給用戶。
注意,寫策略的步驟的順序不能倒過來,即不能先刪除緩存再更新數據庫,原因是在「讀+寫」并發的時候,會出現緩存和數據庫的數據不一致性的問題。
舉個例子,假設某個用戶的年齡是 20,請求 A 要更新用戶年齡為 21,所以它會刪除緩存中的內容。這時,另一個請求 B 要讀取這個用戶的年齡,它查詢緩存發現未命中后,會從數據庫中讀取到年齡為 20,并且寫入到緩存中,然后請求 A 繼續更改數據庫,將用戶的年齡更新為 21。
最終,該用戶年齡在緩存中是 20(舊值),在數據庫中是 21(新值),緩存和數據庫的數據不一致。
為什么「先更新數據庫再刪除緩存」不會有數據不一致的問題?
繼續用「讀 + 寫」請求的并發的場景來分析。
假如某個用戶數據在緩存中不存在,請求 A 讀取數據時從數據庫中查詢到年齡為 20,在未寫入緩存中時另一個請求 B 更新數據。它更新數據庫中的年齡為 21,并且清空緩存。這時請求 A 把從數據庫中讀到的年齡為 20 的數據寫入到緩存中。
最終,該用戶年齡在緩存中是 20(舊值),在數據庫中是 21(新值),緩存和數據庫數據不一致。從上面的理論上分析,先更新數據庫,再刪除緩存也是會出現數據不一致性的問題,但是在實際中,這個問題出現的概率并不高。
因為緩存的寫入通常要遠遠快于數據庫的寫入,所以在實際中很難出現請求 B 已經更新了數據庫并且刪除了緩存,請求 A 才更新完緩存的情況。而一旦請求 A 早于請求 B 刪除緩存之前更新了緩存,那么接下來的請求就會因為緩存不命中而從數據庫中重新讀取數據,所以不會出現這種不一致的情況。
Cache Aside 策略適合讀多寫少的場景,不適合寫多的場景,因為當寫入比較頻繁時,緩存中的數據會被頻繁地清理,這樣會對緩存的命中率有一些影響。如果業務對緩存命中率有嚴格的要求,那么可以考慮兩種解決方案:
一種做法是在更新數據時也更新緩存,只是在更新緩存前先加一個分布式鎖,因為這樣在同一時間只允許一個線程更新緩存,就不會產生并發問題了。當然這么做對于寫入的性能會有一些影響;
另一種做法同樣也是在更新數據時更新緩存,只是給緩存加一個較短的過期時間,這樣即使出現緩存不一致的情況,緩存的數據也會很快過期,對業務的影響也是可以接受。
Read/Write Through(讀穿 / 寫穿)策略
Read/Write Through(讀穿 / 寫穿)策略原則是應用程序只和緩存交互,不再和數據庫交互,而是由緩存和數據庫交互,相當于更新數據庫的操作由緩存自己代理了。
1、Read Through 策略
先查詢緩存中數據是否存在,如果存在則直接返回,如果不存在,則由緩存組件負責從數據庫查詢數據,并將結果寫入到緩存組件,最后緩存組件將數據返回給應用。
2、Write Through 策略
當有數據更新的時候,先查詢要寫入的數據在緩存中是否已經存在:
如果緩存中數據已經存在,則更新緩存中的數據,并且由緩存組件同步更新到數據庫中,然后緩存組件告知應用程序更新完成。
如果緩存中數據不存在,直接更新數據庫,然后返回;
下面是 Read Through/Write Through 策略的示意圖:
img
Read Through/Write Through 策略的特點是由緩存節點而非應用程序來和數據庫打交道,在我們開發過程中相比 Cache Aside 策略要少見一些,原因是我們經常使用的分布式緩存組件,無論是 Memcached 還是 Redis 都不提供寫入數據庫和自動加載數據庫中的數據的功能。而我們在使用本地緩存的時候可以考慮使用這種策略。
Write Back(寫回)策略
Write Back(寫回)策略在更新數據的時候,只更新緩存,同時將緩存數據設置為臟的,然后立馬返回,并不會更新數據庫。對于數據庫的更新,會通過批量異步更新的方式進行。
實際上,Write Back(寫回)策略也不能應用到我們常用的數據庫和緩存的場景中,因為 Redis 并沒有異步更新數據庫的功能。
Write Back 是計算機體系結構中的設計,比如 CPU 的緩存、操作系統中文件系統的緩存都采用了 Write Back(寫回)策略。
Write Back 策略特別適合寫多的場景,因為發生寫操作的時候, 只需要更新緩存,就立馬返回了。比如,寫文件的時候,實際上是寫入到文件系統的緩存就返回了,并不會寫磁盤。
但是帶來的問題是,數據不是強一致性的,而且會有數據丟失的風險,因為緩存一般使用內存,而內存是非持久化的,所以一旦緩存機器掉電,就會造成原本緩存中的臟數據丟失。所以你會發現系統在掉電之后,之前寫入的文件會有部分丟失,就是因為 Page Cache 還沒有來得及刷盤造成的。
這里貼一張 CPU 緩存與內存使用 Write Back 策略的流程圖:
img
其他
Go:
context是否線程安全?
slice底層實現?
GMP模型介紹一下
場景題:
設計短鏈系統
算法:
最長公共前綴
三數之和
審核編輯:湯梓紅
-
操作系統
+關注
關注
37文章
6892瀏覽量
123743 -
網絡協議
+關注
關注
3文章
269瀏覽量
21635 -
編程語言
+關注
關注
10文章
1950瀏覽量
34984 -
MySQL
+關注
關注
1文章
829瀏覽量
26743 -
線程
+關注
關注
0文章
505瀏覽量
19758
原文標題:不愧是字節,把我吊打了。。。
文章出處:【微信號:小林coding,微信公眾號:小林coding】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論