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