涼透了!止步螞蟻金服三面
共 18719字,需瀏覽 38分鐘
·
2024-07-14 16:06
圖解學(xué)習(xí)網(wǎng)站:https://xiaolincoding.com
大家好,我是小林。
今天來分享一位同學(xué)的螞蟻金服的Java后端社招面試題,這位讀者在過程中經(jīng)歷了三次面試,感覺自己答得不錯,結(jié)果三面后掛了,著實讓自己有點摸不著頭腦。
三次面試過程當(dāng)中,穿插了基礎(chǔ)的八股題,當(dāng)我看到面試問題時,心里暗暗說了一句“不愧是螞蟻”,問的問題都比較偏難,問的是比較細節(jié)的,所以平常的技術(shù)積累是很重要的。
并且通過面經(jīng)可以看到,螞蟻非常注重算法能力,直接給了 3 個算法題,然后選擇 2 個來做。
接下來,就讓我們來一起看看螞蟻基礎(chǔ)題的難度吧!
考察的知識點,我給大家羅列了一下:
-
Java:volatile、弱引用、堆內(nèi)存、垃圾回收、Spring、線程池 -
MySQL:索引、聯(lián)合索引、行級鎖、SQL語句 -
kafka:副本、ISR -
Redis:大key處理 -
算法:翻轉(zhuǎn)二叉樹、從左到右打印二叉樹、給一個字符串清除特定字符前的所有字符
Java
volatile關(guān)鍵字的作用
volatite作用有 2 個:
-
保證變量對所有線程的可見性。當(dāng)一個變量被聲明為volatile時,它會保證對這個變量的寫操作會立即刷新到主存中,而對這個變量的讀操作會直接從主存中讀取,從而確保了多線程環(huán)境下對該變量訪問的可見性。這意味著一個線程修改了volatile變量的值,其他線程能夠立刻看到這個修改,不會受到各自線程工作內(nèi)存的影響。 -
禁止指令重排序優(yōu)化。volatile關(guān)鍵字在Java中主要通過內(nèi)存屏障來禁止特定類型的指令重排序。 -
1)寫-寫(Write-Write)屏障:在對volatile變量執(zhí)行寫操作之前,會插入一個寫屏障。這確保了在該變量寫操作之前的所有普通寫操作都已完成,防止了這些寫操作被移到volatile寫操作之后。 -
2)讀-寫(Read-Write)屏障:在對volatile變量執(zhí)行讀操作之后,會插入一個讀屏障。它確保了對volatile變量的讀操作之后的所有普通讀操作都不會被提前到volatile讀之前執(zhí)行,保證了讀取到的數(shù)據(jù)是最新的。 -
3)寫-讀(Write-Read)屏障:這是最重要的一個屏障,它發(fā)生在volatile寫之后和volatile讀之前。這個屏障確保了volatile寫操作之前的所有內(nèi)存操作(包括寫操作)都不會被重排序到volatile讀之后,同時也確保了volatile讀操作之后的所有內(nèi)存操作(包括讀操作)都不會被重排序到volatile寫之前。
弱引用了解嗎,舉例說明在哪里可以用
Java中的弱引用是一種引用類型,它不會阻止一個對象被垃圾回收。
在Java中,弱引用是通過java.lang.ref.WeakReference類實現(xiàn)的。弱引用的一個主要用途是創(chuàng)建非強制性的對象引用,這些引用可以在內(nèi)存壓力大時被垃圾回收器清理,從而避免內(nèi)存泄露。弱引用的使用場景:
-
緩存系統(tǒng):弱引用常用于實現(xiàn)緩存,特別是當(dāng)希望緩存項能夠在內(nèi)存壓力下自動釋放時。如果緩存的大小不受控制,可能會導(dǎo)致內(nèi)存溢出。使用弱引用來維護緩存,可以讓JVM在需要更多內(nèi)存時自動清理這些緩存對象。 -
對象池:在對象池中,弱引用可以用來管理那些暫時不使用的對象。當(dāng)對象不再被強引用時,它們可以被垃圾回收,釋放內(nèi)存。 -
避免內(nèi)存泄露:當(dāng)一個對象不應(yīng)該被長期引用時,使用弱引用可以防止該對象被意外地保留,從而避免潛在的內(nèi)存泄露。
示例代碼:假設(shè)我們有一個緩存系統(tǒng),我們使用弱引用來維護緩存中的對象:
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
public class CacheExample {
private Map<String, WeakReference<MyHeavyObject>> cache = new HashMap<>();
public MyHeavyObject get(String key) {
WeakReference<MyHeavyObject> ref = cache.get(key);
if (ref != null) {
return ref.get();
} else {
MyHeavyObject obj = new MyHeavyObject();
cache.put(key, new WeakReference<>(obj));
return obj;
}
}
// 假設(shè)MyHeavyObject是一個占用大量內(nèi)存的對象
private static class MyHeavyObject {
private byte[] largeData = new byte[1024 * 1024 * 10]; // 10MB data
}
}
在這個例子中,使用WeakReference來存儲MyHeavyObject實例,當(dāng)內(nèi)存壓力增大時,垃圾回收器可以自由地回收這些對象,而不會影響緩存的正常運行。
如果一個對象被垃圾回收,下次嘗試從緩存中獲取時,get()方法會返回null,這時我們可以重新創(chuàng)建對象并將其放入緩存中。因此,使用弱引用時要注意,一旦對象被垃圾回收,通過弱引用獲取的對象可能會變?yōu)?code style="font-size: 14px;line-height: 1.8em;letter-spacing: 0em;background: none 0% 0% / auto no-repeat scroll padding-box border-box rgba(27, 31, 35, 0.05);width: auto;height: auto;margin-left: 2px;margin-right: 2px;padding: 2px 4px;border-style: none;border-width: 3px;border-color: rgb(0, 0, 0) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.4);border-radius: 4px;font-family: Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(100, 149, 237);">null,因此在使用前通常需要檢查這一點。
堆內(nèi)存結(jié)構(gòu)
Java堆是Java虛擬機中內(nèi)存管理的一個重要區(qū)域,主要用于存放對象實例和數(shù)組。隨著JVM的發(fā)展和不同垃圾收集器的實現(xiàn),堆的具體劃分可能會有所不同,但通常可以分為以下幾個部分:
-
新生代:新生代分為Eden Space和Survivor Space。在Eden Space中, 大多數(shù)新創(chuàng)建的對象首先存放在這里。Eden區(qū)相對較小,當(dāng)Eden區(qū)滿時,會觸發(fā)一次Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分為兩個相等大小的區(qū)域,稱為S0(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下來的對象會被移動到其中一個Survivor空間,以繼續(xù)它們的生命周期。這兩個區(qū)域輪流充當(dāng)對象的中轉(zhuǎn)站,幫助區(qū)分短暫存活的對象和長期存活的對象。 -
老年代:存放過一次或多次Minor GC仍存活的對象會被移動到老年代。老年代中的對象生命周期較長,因此Major GC(也稱為Full GC,涉及老年代的垃圾回收)發(fā)生的頻率相對較低,但其執(zhí)行時間通常比Minor GC長。老年代的空間通常比新生代大,以存儲更多的長期存活對象。 -
元空間(Metaspace):從Java 8開始,永久代(Permanent Generation)被元空間取代,用于存儲類的元數(shù)據(jù)信息,如類的結(jié)構(gòu)信息(如字段、方法信息等)。元空間并不在Java堆中,而是使用本地內(nèi)存,這解決了永久代容易出現(xiàn)的內(nèi)存溢出問題。 -
大對象區(qū):在某些JVM實現(xiàn)中(如G1垃圾收集器),為大對象分配了專門的區(qū)域,稱為大對象區(qū)或Humongous Objects區(qū)域。大對象是指需要大量連續(xù)內(nèi)存空間的對象,如大數(shù)組。這類對象直接分配在老年代,以避免因頻繁的年輕代晉升而導(dǎo)致的內(nèi)存碎片化問題。
minorGC、majorGC、fullGC的區(qū)別,什么場景觸發(fā)full GC
在Java中,垃圾回收機制是自動管理內(nèi)存的重要組成部分。根據(jù)其作用范圍和觸發(fā)條件的不同,可以將GC分為三種類型:Minor GC(也稱為Young GC)、Major GC(有時也稱為Old GC)、以及Full GC。以下是這三種GC的區(qū)別和觸發(fā)場景:
Minor GC (Young GC)
-
作用范圍:只針對年輕代進行回收,包括Eden區(qū)和兩個Survivor區(qū)(S0和S1)。 -
觸發(fā)條件:當(dāng)Eden區(qū)空間不足時,JVM會觸發(fā)一次Minor GC,將Eden區(qū)和一個Survivor區(qū)中的存活對象移動到另一個Survivor區(qū)或老年代(Old Generation)。 -
特點:通常發(fā)生得非常頻繁,因為年輕代中對象的生命周期較短,回收效率高,暫停時間相對較短。
Major GC
-
作用范圍:主要針對老年代進行回收,但不一定只回收老年代。 -
觸發(fā)條件:當(dāng)老年代空間不足時,或者系統(tǒng)檢測到年輕代對象晉升到老年代的速度過快,可能會觸發(fā)Major GC。 -
特點:相比Minor GC,Major GC發(fā)生的頻率較低,但每次回收可能需要更長的時間,因為老年代中的對象存活率較高。
Full GC
-
作用范圍:對整個堆內(nèi)存(包括年輕代、老年代以及永久代/元空間)進行回收。 -
觸發(fā)條件: -
直接調(diào)用 System.gc()或Runtime.getRuntime().gc()方法時,雖然不能保證立即執(zhí)行,但JVM會嘗試執(zhí)行Full GC。 -
Minor GC(新生代垃圾回收)時,如果存活的對象無法全部放入老年代,或者老年代空間不足以容納存活的對象,則會觸發(fā)Full GC,對整個堆內(nèi)存進行回收。 -
當(dāng)永久代(Java 8之前的版本)或元空間(Java 8及以后的版本)空間不足時。 -
特點:Full GC是最昂貴的操作,因為它需要停止所有的工作線程(Stop The World),遍歷整個堆內(nèi)存來查找和回收不再使用的對象,因此應(yīng)盡量減少Full GC的觸發(fā)。
Spring bean的作用域
Spring框架中的Bean作用域(Scope)定義了Bean的生命周期和可見性。不同的作用域影響著Spring容器如何管理這些Bean的實例,包括它們?nèi)绾伪粍?chuàng)建、如何被銷毀以及它們是否可以被多個用戶共享。
Spring支持幾種不同的作用域,以滿足不同的應(yīng)用場景需求。以下是一些主要的Bean作用域:
-
Singleton(單例):在整個應(yīng)用程序中只存在一個 Bean 實例。默認作用域,Spring 容器中只會創(chuàng)建一個 Bean 實例,并在容器的整個生命周期中共享該實例。 -
Prototype(原型):每次請求時都會創(chuàng)建一個新的 Bean 實例。次從容器中獲取該 Bean 時都會創(chuàng)建一個新實例,適用于狀態(tài)非常瞬時的 Bean。 -
Request(請求):每個 HTTP 請求都會創(chuàng)建一個新的 Bean 實例。僅在 Spring Web 應(yīng)用程序中有效,每個 HTTP 請求都會創(chuàng)建一個新的 Bean 實例,適用于 Web 應(yīng)用中需求局部性的 Bean。 -
Session(會話):Session 范圍內(nèi)只會創(chuàng)建一個 Bean 實例。該 Bean 實例在用戶會話范圍內(nèi)共享,僅在 Spring Web 應(yīng)用程序中有效,適用于與用戶會話相關(guān)的 Bean。 -
Application:當(dāng)前 ServletContext 中只存在一個 Bean 實例。僅在 Spring Web 應(yīng)用程序中有效,該 Bean 實例在整個 ServletContext 范圍內(nèi)共享,適用于應(yīng)用程序范圍內(nèi)共享的 Bean。 -
WebSocket(Web套接字):在 WebSocket 范圍內(nèi)只存在一個 Bean 實例。僅在支持 WebSocket 的應(yīng)用程序中有效,該 Bean 實例在 WebSocket 會話范圍內(nèi)共享,適用于 WebSocket 會話范圍內(nèi)共享的 Bean。 -
Custom scopes(自定義作用域):Spring 允許開發(fā)者定義自定義的作用域,通過實現(xiàn) Scope 接口來創(chuàng)建新的 Bean 作用域。
在Spring配置文件中,可以通過
<bean id="myBean" class="com.example.MyBeanClass" scope="singleton"/>
在Spring Boot或基于Java的配置中,可以通過@Scope注解來指定Bean的作用域。例如:
@Bean
@Scope("prototype")
public MyBeanClass myBean() {
return new MyBeanClass();
}
在Spring中,在bean加載/銷毀前后,如果想實現(xiàn)某些邏輯,可以怎么做
在Spring框架中,如果你希望在Bean加載(即實例化、屬性賦值、初始化等過程完成后)或銷毀前后執(zhí)行某些邏輯,你可以使用Spring的生命周期回調(diào)接口或注解。這些接口和注解允許你定義在Bean生命周期的關(guān)鍵點執(zhí)行的代碼。
使用init-method和destroy-method
在XML配置中,你可以通過init-method和destroy-method屬性來指定Bean初始化后和銷毀前需要調(diào)用的方法。
<bean id="myBean" class="com.example.MyBeanClass"
init-method="init" destroy-method="destroy"/>
然后,在你的Bean類中實現(xiàn)這些方法:
public class MyBeanClass {
public void init() {
// 初始化邏輯
}
public void destroy() {
// 銷毀邏輯
}
}
實現(xiàn)InitializingBean和DisposableBean接口
你的Bean類可以實現(xiàn)org.springframework.beans.factory.InitializingBean和org.springframework.beans.factory.DisposableBean接口,并分別實現(xiàn)afterPropertiesSet和destroy方法。
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class MyBeanClass implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// 初始化邏輯
}
@Override
public void destroy() throws Exception {
// 銷毀邏輯
}
}
使用@PostConstruct和@PreDestroy注解
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class MyBeanClass {
@PostConstruct
public void init() {
// 初始化邏輯
}
@PreDestroy
public void destroy() {
// 銷毀邏輯
}
}
使用@Bean注解的initMethod和destroyMethod屬性
在基于Java的配置中,你還可以在@Bean注解中指定initMethod和destroyMethod屬性。
@Configuration
public class AppConfig {
@Bean(initMethod = "init", destroyMethod = "destroy")
public MyBeanClass myBean() {
return new MyBeanClass();
}
}
Java線程池,5核心、10最大、20隊列,第6個任務(wù)來了是什么狀態(tài)?第26個任務(wù)來了是什么狀態(tài)?隊列滿了以后執(zhí)行隊列的任務(wù)是從隊列頭 or 隊尾取?核心線程和非核心線程執(zhí)行結(jié)束后,誰先執(zhí)行隊列里的任務(wù)?
第6個任務(wù)來了是什么狀態(tài)
-
當(dāng)?shù)谝粋€任務(wù)到達時,線程池會創(chuàng)建一個核心線程來執(zhí)行這個任務(wù)。 -
當(dāng)?shù)?至第5個任務(wù)到達時,線程池將繼續(xù)創(chuàng)建核心線程直到達到核心線程數(shù)上限(即5個核心線程都在運行)。 -
當(dāng)?shù)?個任務(wù)到達時,由于所有核心線程都已經(jīng)在運行,這個任務(wù)將被放入阻塞隊列中等待執(zhí)行。
第26個任務(wù)到達時的狀態(tài)
-
當(dāng)?shù)?至第25個任務(wù)到達時,它們都將依次被加入到阻塞隊列中,直到隊列滿(即20個任務(wù)在隊列中)。 -
當(dāng)?shù)?6個任務(wù)到達時,由于隊列已滿,而當(dāng)前線程數(shù)小于最大線程數(shù)(10),線程池會創(chuàng)建新的非核心線程(即超出核心線程數(shù)的線程)來執(zhí)行這個任務(wù),直到達到最大線程數(shù)上限(即總共10個線程在運行)。
如果第26個任務(wù)到達時線程池已經(jīng)有10個線程在運行(包括核心線程和非核心線程),那么根據(jù)線程池的拒絕策略,這個任務(wù)將被拒絕。默認的拒絕策略是AbortPolicy,它會拋出一個RejectedExecutionException異常。
阻塞隊列的FIFO原則
阻塞隊列通常遵循先進先出(FIFO)的原則,這意味著任務(wù)將按照它們被添加到隊列中的順序執(zhí)行。當(dāng)線程完成當(dāng)前任務(wù)并準(zhǔn)備獲取下一個任務(wù)時,它會從隊列的頭部取出下一個等待的任務(wù)。
核心線程與非核心線程執(zhí)行隊列任務(wù)
無論是核心線程還是非核心線程,一旦有空閑,都會從隊列頭部獲取任務(wù)來執(zhí)行。線程池并不區(qū)分是核心線程還是非核心線程去執(zhí)行隊列中的任務(wù),只要線程有空閑,就會嘗試從隊列中獲取任務(wù)執(zhí)行。
不過,在保持存活時間(keep-alive time)過后,非核心線程可能會被終止,而核心線程則會一直保留,除非線程池被顯式關(guān)閉。
Redis
Redis中的大key的場景怎么處理
在Redis中,大key指的是那些存儲了大量數(shù)據(jù)的鍵,這些鍵可能因為其值的大小或者其包含的元素數(shù)量巨大,導(dǎo)致在執(zhí)行相關(guān)操作時對Redis服務(wù)器造成顯著的性能影響。以下是處理大key的一些建議:
-
識別大key:使用 redis-cli工具的SCAN命令結(jié)合KEYS或HGETALL、LRANGE等命令來定位哪些key占用過多的內(nèi)存或哪些操作可能引起性能問題。 -
優(yōu)化數(shù)據(jù)結(jié)構(gòu):使用更高效的數(shù)據(jù)結(jié)構(gòu),例如用 Set、Sorted Set或Hash替換String類型,當(dāng)數(shù)據(jù)適合這些結(jié)構(gòu)時。對于大型列表,考慮使用List的LPUSH和RPUSH來限制列表的長度,或者使用ZADD和ZREM在有序集合中維護固定大小的滑動窗口。使用Bitmaps來存儲大量二進制數(shù)據(jù),尤其是當(dāng)數(shù)據(jù)是稀疏的或需要進行位操作時。 -
數(shù)據(jù)拆分:最好在設(shè)計階段,就把大 key 拆分成一個一個小 key。或者,定時檢查 Redis 是否存在大 key ,如果該大 key 是可以刪除的,不要使用 DEL 命令刪除,因為該命令刪除過程會阻塞主線程,而是用 unlink 命令(Redis 4.0+)刪除大 key,因為該命令的刪除過程是異步的,不會阻塞主線程。 -
避免全量讀取:對于大key,盡量避免一次性讀取全部數(shù)據(jù),而是使用范圍查詢?nèi)?code style="font-size: 14px;line-height: 1.8em;letter-spacing: 0em;background: none 0% 0% / auto no-repeat scroll padding-box border-box rgba(27, 31, 35, 0.05);width: auto;margin-left: 2px;margin-right: 2px;padding: 2px 4px;border-style: none;border-width: 3px;border-color: rgb(0, 0, 0) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.4);border-radius: 4px;font-family: Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(100, 149, 237);">HGET、 LPOP、RPOP等命令來分批次讀取數(shù)據(jù)。
kafka
kafka副本了解嗎,聊聊ISR
在Kafka中是有主題概念的,而每個主題又進一步劃分成若干個分區(qū)。副本的概念實際上是在分區(qū)層級下定義的,每個分區(qū)配置有若干個副本。所謂副本(Replica),本質(zhì)就是一個只能追加寫消息的提交日志。根據(jù)Kafka副本機制的定義,同一個分區(qū)下的所有副本保存有相同的消息序列,這些副本分散保存在不同的Broker上,從而能夠?qū)共糠諦roker宕機帶來的數(shù)據(jù)不可用。在kafka中采用基于領(lǐng)導(dǎo)者(Leader-based)的副本機制來確保副本中所有的數(shù)據(jù)的一致性。基于領(lǐng)導(dǎo)者的副本機制的工作原理如下圖所示:
第一,在Kafka中,副本分成兩類:領(lǐng)導(dǎo)者副本(Leader Replica)和追隨者副本(Follower Replica)。每個分區(qū)在創(chuàng)建時都要選舉一個副本,稱為領(lǐng)導(dǎo)者副本,其余的副本自動稱為追隨者副本。
第二,Kafka的副本機制比其他分布式系統(tǒng)要更嚴格一些。在Kafka中,追隨者副本是不對外提供服務(wù)的。這就是說,任何一個追隨者副本都不能響應(yīng)消費者和生產(chǎn)者的讀寫請求。所有的請求都必須由領(lǐng)導(dǎo)者副本來處理,或者說,所有的讀寫請求都必須發(fā)往領(lǐng)導(dǎo)者副本所在的Broker,由該Broker負責(zé)處理。追隨者副本不處理客戶端請求,它唯一的任務(wù)就是從領(lǐng)導(dǎo)者副本異步拉取消息,并寫入到自己的提交日志中,從而實現(xiàn)與領(lǐng)導(dǎo)者副本的同步。
第三,當(dāng)領(lǐng)導(dǎo)者副本掛掉了,或者說領(lǐng)導(dǎo)者副本所在的Broker宕機時,Kafka依托于ZooKeeper提供的監(jiān)控功能能夠?qū)崟r感知到,并立即開啟新一輪的領(lǐng)導(dǎo)者選舉,從追隨者副本中選一個作為新的領(lǐng)導(dǎo)者。老Leader副本重啟回來后,只能作為追隨者副本加入到集群中。
一定要特別注意上面的第二點,即追隨者副本是不對外提供服務(wù)的。還記得剛剛我們談到副本機制的好處時,說過Kafka沒能提供讀操作橫向擴展以及改善局部性嗎?具體的原因就在于此。
對于客戶端用戶而言,Kafka的追隨者副本沒有任何作用,它既不能像MySQL那樣幫助領(lǐng)導(dǎo)者副本“扛讀”,也不能實現(xiàn)將某些副本放到離客戶端近的地方來改善數(shù)據(jù)局部性。
既然如此,Kafka為什么要這樣設(shè)計呢?其實這種副本機制有兩個方面的好處。
-
方便實現(xiàn)“Read-your-writes”:所謂Read-your-writes,顧名思義就是,當(dāng)你使用生產(chǎn)者API向Kafka成功寫入消息后,馬上使用消費者API去讀取剛才生產(chǎn)的消息。舉個例子,比如你平時發(fā)微博時,你發(fā)完一條微博,肯定是希望能立即看到的,這就是典型的Read-your-writes場景。如果允許追隨者副本對外提供服務(wù),由于副本同步是異步的,因此有可能出現(xiàn)追隨者副本還沒有從領(lǐng)導(dǎo)者副本那里拉取到最新的消息,從而使得客戶端看不到最新寫入的消息。 -
方便實現(xiàn)單調(diào)讀(Monotonic Reads):什么是單調(diào)讀呢?就是對于一個消費者用戶而言,在多次消費消息時,它不會看到某條消息一會兒存在一會兒不存在。如果允許追隨者副本提供讀服務(wù),那么假設(shè)當(dāng)前有2個追隨者副本F1和F2,它們異步地拉取領(lǐng)導(dǎo)者副本數(shù)據(jù)。倘若F1拉取了Leader的最新消息而F2還未及時拉取,那么,此時如果有一個消費者先從F1讀取消息之后又從F2拉取消息,它可能會看到這樣的現(xiàn)象:第一次消費時看到的最新消息在第二次消費時不見了,這就不是單調(diào)讀一致性。但是,如果所有的讀請求都是由Leader來處理,那么Kafka就很容易實現(xiàn)單調(diào)讀一致性。
在kafka中,追隨者副本不提供服務(wù),只是定期地異步拉取領(lǐng)導(dǎo)者副本中的數(shù)據(jù)而已。既然是異步的,就存在著不可能與Leader實時同步的風(fēng)險。在探討如何正確應(yīng)對這種風(fēng)險之前,我們必須要精確地知道同步的含義是什么。或者說,Kafka要明確地告訴我們,追隨者副本到底在什么條件下才算與Leader同步。
基于這個想法,Kafka引入了In-sync Replicas,也就是所謂的ISR副本集合。ISR中的副本都是與Leader同步的副本,相反,不在ISR中的追隨者副本就被認為是與Leader不同步的。
那么,到底什么副本能夠進入到ISR中呢?
我們首先要明確的是,Leader副本天然就在ISR中。也就是說,ISR不只是追隨者副本集合,它必然包括Leader副本。甚至在某些情況下,ISR只有Leader這一個副本。
另外,能夠進入到ISR的追隨者副本要滿足一定的條件。圖中有3個副本:1個領(lǐng)導(dǎo)者副本和2個追隨者副本。Leader副本當(dāng)前寫入了10條消息,F(xiàn)ollower1副本同步了其中的6條消息,而Follower2副本只同步了其中的3條消息。那么問題來了,對于這2個追隨者副本,你覺得哪個追隨者副本與Leader不同步?
答案是,要根據(jù)具體情況來定。換成英文,就是那句著名的“It depends”。看上去好像Follower2的消息數(shù)比Leader少了很多,它是最有可能與Leader不同步的。的確是這樣的,但僅僅是可能。
事實上,這張圖中的2個Follower副本都有可能與Leader不同步,但也都有可能與Leader同步。也就是說,Kafka判斷Follower是否與Leader同步的標(biāo)準(zhǔn),不是看相差的消息數(shù),而是另有“玄機”。
這個標(biāo)準(zhǔn)就是Broker端參數(shù)replica.lag.time.max.ms參數(shù)值。這個參數(shù)的含義是Follower副本能夠落后Leader副本的最長時間間隔,當(dāng)前默認值是10秒。這就是說,只要一個Follower副本落后Leader副本的時間不連續(xù)超過10秒,那么Kafka就認為該Follower副本與Leader是同步的,即使此時Follower副本中保存的消息明顯少于Leader副本中的消息。
我們都知道,F(xiàn)ollower副本唯一的工作就是不斷地從Leader副本拉取消息,然后寫入到自己的提交日志中。如果這個同步過程的速度持續(xù)慢于Leader副本的消息寫入速度,那么在replica.lag.time.max.ms時間后,此Follower副本就會被認為是與Leader副本不同步的,因此不能再放入ISR中。此時,Kafka會自動收縮ISR集合,將該副本“踢出”ISR。
值得注意的是,倘若該副本后面慢慢地追上了Leader的進度,那么它是能夠重新被加回ISR的。這也表明,ISR是一個動態(tài)調(diào)整的集合,而非靜態(tài)不變的。
Unclean領(lǐng)導(dǎo)者選舉(Unclean Leader Election)
既然ISR是可以動態(tài)調(diào)整的,那么自然就可以出現(xiàn)這樣的情形:ISR為空。
因為Leader副本天然就在ISR中,如果ISR為空了,就說明Leader副本也“掛掉”了,Kafka需要重新選舉一個新的Leader。可是ISR是空,此時該怎么選舉新Leader呢?
Kafka把所有不在ISR中的存活副本都稱為非同步副本。通常來說,非同步副本落后Leader太多,因此,如果選擇這些副本作為新Leader,就可能出現(xiàn)數(shù)據(jù)的丟失。
畢竟,這些副本中保存的消息遠遠落后于老Leader中的消息。在Kafka中,選舉這種副本的過程稱為Unclean領(lǐng)導(dǎo)者選舉。Broker端參數(shù)unclean.leader.election.enable控制是否允許Unclean領(lǐng)導(dǎo)者選舉。
開啟Unclean領(lǐng)導(dǎo)者選舉可能會造成數(shù)據(jù)丟失,但好處是,它使得分區(qū)Leader副本一直存在,不至于停止對外提供服務(wù),因此提升了高可用性。反之,禁止Unclean領(lǐng)導(dǎo)者選舉的好處在于維護了數(shù)據(jù)的一致性,避免了消息丟失,但犧牲了高可用性。
MySQL
聯(lián)合索引的實現(xiàn)原理?
將將多個字段組合成一個索引,該索引就被稱為聯(lián)合索引。比如,將商品表中的 product_no 和 name 字段組合成聯(lián)合索引(product_no, name),創(chuàng)建聯(lián)合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
聯(lián)合索引(product_no, name) 的 B+Tree 示意圖如下:可以看到,聯(lián)合索引的非葉子節(jié)點用兩個字段的值作為 B+Tree 的 key 值。當(dāng)在聯(lián)合索引查詢數(shù)據(jù)時,先按 product_no 字段比較,在 product_no 相同的情況下再按 name 字段比較。
也就是說,聯(lián)合索引查詢的 B+Tree 是先按 product_no 進行排序,然后再 product_no 相同的情況再按 name 字段排序。
因此,使用聯(lián)合索引時,存在最左匹配原則,也就是按照最左優(yōu)先的方式進行索引的匹配。在使用聯(lián)合索引進行查詢的時候,如果不遵循「最左匹配原則」,聯(lián)合索引會失效,這樣就無法利用到索引快速查詢的特性了。
比如,如果創(chuàng)建了一個 (a, b, c) 聯(lián)合索引,如果查詢條件是以下這幾種,就可以匹配上聯(lián)合索引:
-
where a=1; -
where a=1 and b=2 and c=3; -
where a=1 and b=2;
需要注意的是,因為有查詢優(yōu)化器,所以 a 字段在 where 子句的順序并不重要。
但是,如果查詢條件是以下這幾種,因為不符合最左匹配原則,所以就無法匹配上聯(lián)合索引,聯(lián)合索引就會失效:
-
where b=2; -
where c=3; -
where b=2 and c=3;
上面這些查詢條件之所以會失效,是因為(a, b, c) 聯(lián)合索引,是先按 a 排序,在 a 相同的情況再按 b 排序,在 b 相同的情況再按 c 排序。所以,b 和 c 是全局無序,局部相對有序的,這樣在沒有遵循最左匹配原則的情況下,是無法利用到索引的。
我這里舉聯(lián)合索引(a,b)的例子,該聯(lián)合索引的 B+ Tree 如下:可以看到,a 是全局有序的(1, 2, 2, 3, 4, 5, 6, 7 ,8),而 b 是全局是無序的(12,7,8,2,3,8,10,5,2)。因此,直接執(zhí)行where b = 2這種查詢條件沒有辦法利用聯(lián)合索引的,利用索引的前提是索引里的 key 是有序的。
只有在 a 相同的情況才,b 才是有序的,比如 a 等于 2 的時候,b 的值為(7,8),這時就是有序的,這個有序狀態(tài)是局部的,因此,執(zhí)行where a = 2 and b = 7是 a 和 b 字段能用到聯(lián)合索引的,也就是聯(lián)合索引生效了。
創(chuàng)建聯(lián)合索引時需要注意什么?
建立聯(lián)合索引時的字段順序,對索引效率也有很大影響。越靠前的字段被用于索引過濾的概率越高,實際開發(fā)工作中建立聯(lián)合索引時,要把區(qū)分度大的字段排在前面,這樣區(qū)分度大的字段越有可能被更多的 SQL 使用到。區(qū)分度就是某個字段 column 不同值的個數(shù)「除以」表的總行數(shù),計算公式如下:比如,性別的區(qū)分度就很小,不適合建立索引或不適合排在聯(lián)合索引列的靠前的位置,而 UUID 這類字段就比較適合做索引或排在聯(lián)合索引列的靠前的位置。
因為如果索引的區(qū)分度很小,假設(shè)字段的值分布均勻,那么無論搜索哪個值都可能得到一半的數(shù)據(jù)。在這些情況下,還不如不要索引,因為 MySQL 還有一個查詢優(yōu)化器,查詢優(yōu)化器發(fā)現(xiàn)某個值出現(xiàn)在表的數(shù)據(jù)行中的百分比(慣用的百分比界線是"30%")很高的時候,它一般會忽略索引,進行全表掃描。
聯(lián)合索引ABC,現(xiàn)在有個執(zhí)行語句是A = XXX and C < XXX,索引怎么走
根據(jù)最左匹配原則,A可以走聯(lián)合索引,C不會走聯(lián)合索引,但是C可以走索引下推
兩個事務(wù)update同一條數(shù)據(jù)會發(fā)生什么
當(dāng)事務(wù) A 進行 update 的時候會記錄加 X 型行級鎖,如果事務(wù) B 執(zhí)行 update 的時候,發(fā)現(xiàn)記錄已經(jīng)加了 X 型行級鎖之后,就會進入阻塞狀態(tài),因為發(fā)生了寫寫沖突。
事務(wù) B 會阻塞到到事務(wù)A 提交事務(wù)之后,因為事務(wù)提交之后鎖才會釋放。
sql題:給學(xué)生表、課程成績表,求不存在01課程但存在02課程的學(xué)生的成績
可以使用SQL的子查詢和LEFT JOIN或者EXISTS關(guān)鍵字來實現(xiàn),這里我將展示兩種不同的方法來完成這個查詢。假設(shè)我們有以下兩張表:
-
Student表,其中包含學(xué)生的sid(學(xué)生編號)和其他相關(guān)信息。 -
Score表,其中包含sid(學(xué)生編號),cid(課程編號)和score(分數(shù))。
方法1:使用LEFT JOIN 和 IS NULL
SELECT s.sid, s.sname, sc2.cid, sc2.score
FROM Student s
LEFT JOIN Score AS sc1 ON s.sid = sc1.sid AND sc1.cid = '01'
LEFT JOIN Score AS sc2 ON s.sid = sc2.sid AND sc2.cid = '02'
WHERE sc1.cid IS NULL AND sc2.cid IS NOT NULL;
方法2:使用NOT EXISTS
SELECT s.sid, s.sname, sc.cid, sc.score
FROM Student s
JOIN Score sc ON s.sid = sc.sid AND sc.cid = '02'
WHERE NOT EXISTS (
SELECT 1 FROM Score sc1 WHERE sc1.sid = s.sid AND sc1.cid = '01'
);
計網(wǎng)
Websocket發(fā)一條阻塞了,后面的消息會怎么樣
WebSocket發(fā)送一條消息時發(fā)生了阻塞,如果發(fā)送操作是同步的,那么發(fā)送一條消息時的阻塞會導(dǎo)致后續(xù)消息的發(fā)送被掛起,直到當(dāng)前消息被成功發(fā)送。如果是異步發(fā)送,那么消息可能被加入到發(fā)送隊列中,而不立即阻塞。
為了避免阻塞問題,因此可以使用非阻塞(異步)API,允許消息在后臺排隊和發(fā)送,而不會阻塞應(yīng)用程序的其他部分。
算法(三選二)
-
翻轉(zhuǎn)二叉樹 -
給一個字符串清除特定字符前的所有字符 -
從左到右從上到下打印二叉樹;
推薦閱讀:
