字節(jié)跳動(dòng)也不過(guò)如此,狠心拒了。
共 11279字,需瀏覽 23分鐘
·
2024-05-22 14:04
大家好,我是二哥呀。
字節(jié)跳動(dòng)之所以被稱為“宇宙廠”,就是因?yàn)樽止?jié)的員工數(shù)量非常多,業(yè)務(wù)覆蓋的范圍非常廣。再加上字節(jié)的薪資待遇給的非常可觀,也就成為了很多同學(xué)的心儀之選,不只是校招還有社招哦(香,真香)。
23 屆的一個(gè)球友去字節(jié),直接開到了 SSP,讓我印象非常深刻,所以每次提到字節(jié)我第一時(shí)間就想到了他。要知道,23 屆號(hào)稱是史上最難的一屆哦,他還是雙非本,年包直接給到了 50 萬(wàn)以上,非常夸張:開發(fā)一個(gè)小軟件,月入2000
所以當(dāng)我看到牛客上這位牛友直接拒絕了字節(jié)跳動(dòng)的 offer,我是無(wú)所謂的,但我的一個(gè)朋友已經(jīng)開始汗流浹背了,他有點(diǎn)破防了。。。。。。
不管網(wǎng)上的聲音怎么說(shuō),我還是希望有能力的同學(xué)多沖一沖,尤其是字節(jié)這種愿意給錢多的廠,大不了干幾年跑路嘛,這年頭,在一家公司干一輩子估計(jì)也不太現(xiàn)實(shí)吧(??)?
就像我昨天在 VIP 群里說(shuō)的,懷揣希望,仰望星空,做一個(gè)愛(ài)自己、有思想、懂感恩,能實(shí)現(xiàn)自己人生抱負(fù)的人最好,做不到,也無(wú)所謂啊。
這次我們以《Java 面試指南-字節(jié)跳動(dòng)面經(jīng)》同學(xué) 1 的后端實(shí)習(xí)二面為例, 來(lái)看看字節(jié)的面試官都喜歡問(wèn)哪些問(wèn)題,好做到知彼知己百戰(zhàn)不殆~
題目不少,火箭造的飛起。主要圍繞 MySQL、Redis 和計(jì)算機(jī)網(wǎng)絡(luò)展開,所以大家在準(zhǔn)備的時(shí)候一定要有的放矢,知道哪些是重點(diǎn)。
1、二哥的 Linux 速查備忘手冊(cè).pdf 下載 2、三分惡面渣逆襲在線版:https://javabetter.cn/sidebar/sanfene/nixi.html
字節(jié)跳動(dòng)面經(jīng)(題目來(lái)自牛客)
對(duì)redis的數(shù)據(jù)結(jié)構(gòu)是否熟悉?
Redis 的底層數(shù)據(jù)結(jié)構(gòu)有動(dòng)態(tài)字符串(sds)、鏈表(list)、字典(ht)、跳躍表(skiplist)、整數(shù)集合(intset)、壓縮列表(ziplist) 等。
比如說(shuō) string 是通過(guò) SDS 實(shí)現(xiàn)的,list 是通過(guò)鏈表實(shí)現(xiàn)的,hash 是通過(guò)字典實(shí)現(xiàn)的,set 是通過(guò)字典實(shí)現(xiàn)的,zset 是通過(guò)跳躍表實(shí)現(xiàn)的。
講一下Sorted set的底層數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)?
跳躍表(也稱跳表)是有序集合 Zset 的底層實(shí)現(xiàn)之?。在 Redis 7.0 之前,如果有序集合的元素個(gè)數(shù)小于 128 個(gè),并且每個(gè)元素的值小于 64 字節(jié)時(shí),Redis 會(huì)使用壓縮列表作為 Zset 的底層實(shí)現(xiàn),否則會(huì)使用跳表;在 Redis 7.0 之后,壓縮列表已經(jīng)廢棄,交由 listpack 來(lái)替代。
跳表由 zskiplist 和 zskiplistNode 組成,zskiplist ?于保存跳表的基本信息(表頭、表尾、?度、層高等)。
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
zskiplistNode ?于表示跳表節(jié)點(diǎn),每個(gè)跳表節(jié)點(diǎn)的層?是不固定的,每個(gè)節(jié)點(diǎn)都有?個(gè)指向保存了當(dāng)前節(jié)點(diǎn)的分值和成員對(duì)象的指針。
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
什么是緩存穿透?如何解決?
緩存穿透是指查詢不存在的數(shù)據(jù),由于緩存沒(méi)有命中(因?yàn)閿?shù)據(jù)根本就不存在),請(qǐng)求每次都會(huì)穿過(guò)緩存去查詢數(shù)據(jù)庫(kù)。如果這種查詢非常頻繁,就會(huì)給數(shù)據(jù)庫(kù)造成很大的壓力。
緩存穿透意味著緩存失去了減輕數(shù)據(jù)壓力的意義。緩存穿透可能有兩種原因:
-
自身業(yè)務(wù)代碼問(wèn)題 -
惡意攻擊,爬蟲造成空命中
它主要有兩種解決辦法:
①、緩存空值/默認(rèn)值
在數(shù)據(jù)庫(kù)無(wú)法命中之后,把一個(gè)空對(duì)象或者默認(rèn)值保存到緩存,之后再訪問(wèn)這個(gè)數(shù)據(jù),就會(huì)從緩存中獲取,這樣就保護(hù)了數(shù)據(jù)庫(kù)。
緩存空值有兩大問(wèn)題:
-
空值做了緩存,意味著緩存層中存了更多的鍵,需要更多的內(nèi)存空間(如果是攻擊,問(wèn)題更嚴(yán)重),比較有效的方法是針對(duì)這類數(shù)據(jù)設(shè)置一個(gè)較短的過(guò)期時(shí)間,讓其自動(dòng)剔除。 -
緩存層和存儲(chǔ)層的數(shù)據(jù)會(huì)有一段時(shí)間窗口的不一致,可能會(huì)對(duì)業(yè)務(wù)有一定影響。
例如過(guò)期時(shí)間設(shè)置為 5 分鐘,如果此時(shí)存儲(chǔ)層添加了這個(gè)數(shù)據(jù),那此段時(shí)間就會(huì)出現(xiàn)緩存層和存儲(chǔ)層數(shù)據(jù)的不一致。
這時(shí)候可以利用消息隊(duì)列或者其它異步方式清理緩存中的空對(duì)象。
②、布隆過(guò)濾器
除了緩存空對(duì)象,我們還可以在存儲(chǔ)和緩存之前,加一個(gè)布隆過(guò)濾器,做一層過(guò)濾。
布隆過(guò)濾器里會(huì)保存數(shù)據(jù)是否存在,如果判斷數(shù)據(jù)不存在,就不會(huì)訪問(wèn)存儲(chǔ)。
兩種解決方案的對(duì)比:
什么是緩存擊穿?如何解決?
緩存擊穿是指某一個(gè)或少數(shù)幾個(gè)數(shù)據(jù)被高頻訪問(wèn),當(dāng)這些數(shù)據(jù)在緩存中過(guò)期的那一刻,大量請(qǐng)求就會(huì)直接到達(dá)數(shù)據(jù)庫(kù),導(dǎo)致數(shù)據(jù)庫(kù)瞬間壓力過(guò)大。
解決?案:
①、加鎖更新,?如請(qǐng)求查詢 A,發(fā)現(xiàn)緩存中沒(méi)有,對(duì) A 這個(gè) key 加鎖,同時(shí)去數(shù)據(jù)庫(kù)查詢數(shù)據(jù),寫?緩存,再返回給?戶,這樣后?的請(qǐng)求就可以從緩存中拿到數(shù)據(jù)了。
②、將過(guò)期時(shí)間組合寫在 value 中,通過(guò)異步的?式不斷的刷新過(guò)期時(shí)間,防?此類現(xiàn)象。
什么是緩存雪崩?如何解決?
緩存雪崩是指在某一個(gè)時(shí)間點(diǎn),由于大量的緩存數(shù)據(jù)同時(shí)過(guò)期或緩存服務(wù)器突然宕機(jī)了,導(dǎo)致所有的請(qǐng)求都落到了數(shù)據(jù)庫(kù)上(比如 MySQL),從而對(duì)數(shù)據(jù)庫(kù)造成巨大壓力,甚至導(dǎo)致數(shù)據(jù)庫(kù)崩潰的現(xiàn)象。
總之就是,崩了,崩的非常嚴(yán)重,就叫雪崩了(電影電視里應(yīng)該看到過(guò),非常夸張)。
如何解決緩存雪崩呢?
第一種:提高緩存可用性
01、集群部署:采用分布式緩存而不是單一緩存服務(wù)器,可以降低單點(diǎn)故障的風(fēng)險(xiǎn)。即使某個(gè)緩存節(jié)點(diǎn)發(fā)生故障,其他節(jié)點(diǎn)仍然可以提供服務(wù),從而避免對(duì)數(shù)據(jù)庫(kù)的大量直接訪問(wèn)。
可以利用 Redis Cluster。
或者第三方集群方案 Codis。
02、備份緩存:對(duì)于關(guān)鍵數(shù)據(jù),除了在主緩存中存儲(chǔ),還可以在備用緩存中保存一份。當(dāng)主緩存不可用時(shí),可以快速切換到備用緩存,確保系統(tǒng)的穩(wěn)定性和可用性。
在技術(shù)派實(shí)戰(zhàn)項(xiàng)目中,我們采用了多級(jí)緩存的策略,其中就包括使用本地緩存 Guava Cache 和 Caffeine 來(lái)作為二級(jí)緩存,在 Redis 出現(xiàn)問(wèn)題時(shí),系統(tǒng)會(huì)自動(dòng)切換到本地緩存。
這個(gè)過(guò)程稱為“降級(jí)”,意味著系統(tǒng)在失去優(yōu)先級(jí)高的資源時(shí)仍能繼續(xù)提供服務(wù)。
當(dāng)從 Redis 獲取數(shù)據(jù)失敗時(shí),嘗試從本地緩存讀取數(shù)據(jù)。
LoadingCache<String, UserPermissions> permissionsCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(this::loadPermissionsFromRedis);
public UserPermissions loadPermissionsFromRedis(String userId) {
try {
return redisClient.getPermissions(userId);
} catch (Exception ex) {
// Redis 異常處理,嘗試從本地緩存獲取
return permissionsCache.getIfPresent(userId);
}
}
第二種:過(guò)期時(shí)間
對(duì)于緩存數(shù)據(jù),設(shè)置不同的過(guò)期時(shí)間,避免大量緩存數(shù)據(jù)同時(shí)過(guò)期。可以通過(guò)在原有過(guò)期時(shí)間的基礎(chǔ)上添加一個(gè)隨機(jī)值來(lái)實(shí)現(xiàn),這樣可以分散緩存過(guò)期時(shí)間,減少同一時(shí)間對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)壓力。
第三種:限流和降級(jí)
通過(guò)設(shè)置合理的系統(tǒng)限流策略,如令牌桶或漏斗算法,來(lái)控制訪問(wèn)流量,防止在緩存失效時(shí)數(shù)據(jù)庫(kù)被打垮。
此外,系統(tǒng)可以實(shí)現(xiàn)降級(jí)策略,在緩存雪崩或系統(tǒng)壓力過(guò)大時(shí),暫時(shí)關(guān)閉一些非核心服務(wù),確保核心服務(wù)的正常運(yùn)行。
什么是緩存預(yù)熱?如何解決?
緩存預(yù)熱是指在系統(tǒng)啟動(dòng)時(shí),提前將一些預(yù)定義的數(shù)據(jù)加載到緩存中,以避免在系統(tǒng)運(yùn)行初期由于緩存未命中(cache miss)導(dǎo)致的性能問(wèn)題。
通過(guò)緩存預(yù)熱,可以確保系統(tǒng)在上線后能夠立即提供高效的服務(wù),減少首次訪問(wèn)時(shí)的延遲。
緩存預(yù)熱的方法有多種,在技術(shù)派實(shí)戰(zhàn)項(xiàng)目中,我們采用了項(xiàng)目啟動(dòng)時(shí)自動(dòng)加載和定時(shí)預(yù)熱兩種方式,比如說(shuō)每天定時(shí)更新站點(diǎn)地圖到 Redis 緩存中。
/**
* 采用定時(shí)器方案,每天5:15分刷新站點(diǎn)地圖,確保數(shù)據(jù)的一致性
*/
@Scheduled(cron = "0 15 5 * * ?")
public void autoRefreshCache() {
log.info("開始刷新sitemap.xml的url地址,避免出現(xiàn)數(shù)據(jù)不一致問(wèn)題!");
refreshSitemap();
log.info("刷新完成!");
}
@Override
public void refreshSitemap() {
initSiteMap();
}
private synchronized void initSiteMap() {
long lastId = 0L;
RedisClient.del(SITE_MAP_CACHE_KEY);
while (true) {
List<SimpleArticleDTO> list = articleDao.getBaseMapper().listArticlesOrderById(lastId, SCAN_SIZE);
// 刷新站點(diǎn)地圖信息
Map<String, Long> map = list.stream().collect(Collectors.toMap(s -> String.valueOf(s.getId()), s -> s.getCreateTime().getTime(), (a, b) -> a));
RedisClient.hMSet(SITE_MAP_CACHE_KEY, map);
if (list.size() < SCAN_SIZE) {
break;
}
lastId = list.get(list.size() - 1).getId();
}
}
Redis如何實(shí)現(xiàn)分布式鎖?
Redis 實(shí)現(xiàn)分布式鎖的本質(zhì),就是在 Redis 里面占一個(gè)“茅坑”,當(dāng)別的客戶端也來(lái)占坑時(shí),發(fā)現(xiàn)已經(jīng)有客戶端蹲在那里了,就只好放棄或者稍后再試。
可以使用 Redis 的 SET 命令實(shí)現(xiàn)分布式鎖。SET 命令支持設(shè)置鍵值對(duì)的同時(shí)添加過(guò)期時(shí)間,這樣可以防止死鎖的發(fā)生。
SET key value NX PX 30000
-
key是鎖名。 -
value是鎖的持有者標(biāo)識(shí),可以使用 UUID 作為 value。 -
NX只在鍵不存在時(shí)設(shè)置。 -
PX 30000:設(shè)置鍵的過(guò)期時(shí)間為 30 秒(防止死鎖)。
上面這段命令其實(shí)是 setnx 和 expire 組合在一起的原子命令,算是比較完善的一個(gè)分布式鎖了。
當(dāng)然,實(shí)際的開發(fā)中,沒(méi)人會(huì)去自己寫分布式鎖的命令,因?yàn)橛袑I(yè)的輪子——Redisson。
什么是回表?
回表是指在數(shù)據(jù)庫(kù)查詢過(guò)程中,通過(guò)非聚簇索引(secondary index)查找到記錄的主鍵值后,再根據(jù)這個(gè)主鍵值到聚簇索引(clustered index)中查找完整記錄的過(guò)程。
回表操作通常發(fā)生在使用非聚簇索引進(jìn)行查詢,但查詢的字段不全在該索引中,必須通過(guò)主鍵進(jìn)行再次查詢以獲取完整數(shù)據(jù)。
換句話說(shuō),數(shù)據(jù)庫(kù)需要先查找索引,然后再根據(jù)索引回到數(shù)據(jù)表中去查找實(shí)際的數(shù)據(jù)。
因此,使用非聚簇索引查找數(shù)據(jù)通常比使用聚簇索引要慢,因?yàn)樾枰M(jìn)行兩次磁盤訪問(wèn)。當(dāng)然,如果索引所在的數(shù)據(jù)頁(yè)已經(jīng)被加載到內(nèi)存中,那么非聚簇索引的查找速度也可以非常快。
例如:select * from user where name = '張三';,會(huì)先從輔助索引中找到 name='張三' 的主鍵 ID,然后再根據(jù)主鍵 ID 從主鍵索引中找到對(duì)應(yīng)的數(shù)據(jù)行。
回表記錄越多好嗎?(回表的代價(jià))
回表記錄越多并不是一件好事。事實(shí)上,回表的代價(jià)是很高的,尤其在記錄較多時(shí),回表操作會(huì)顯著影響查詢性能。
因?yàn)槊看位乇聿僮鞫夹枰M(jìn)行一次磁盤 I/O 讀取操作。如果回表記錄很多,會(huì)導(dǎo)致大量的磁盤 I/O。
索引覆蓋(Covering Index)可以減少回表操作,將查詢的字段都放在索引中,這樣不需要回表就可以獲取到查詢結(jié)果了。
性別字段要建立索引嗎?為什么?
性別字段通常不適合建立索引。因?yàn)樾詣e字段的選擇性(區(qū)分度)較低,獨(dú)立索引效果有限。
如果性別字段又很少用于查詢,表的數(shù)據(jù)規(guī)模較小,那么建立索引反而會(huì)增加額外的存儲(chǔ)空間和維護(hù)成本。
如果性別字段確實(shí)經(jīng)常用于查詢條件,數(shù)據(jù)規(guī)模也比較大,可以將性別字段作為復(fù)合索引的一部分,與選擇性較高的字段一起加索引,會(huì)更好一些。
什么是區(qū)分度?
區(qū)分度(Selectivity)是衡量一個(gè)字段在數(shù)據(jù)庫(kù)表中唯一值的比例,用來(lái)表示該字段在索引優(yōu)化中的有效性。
區(qū)分度 = 字段的唯一值數(shù)量 / 字段的總記錄數(shù);接近 1,字段值大部分是唯一的。例如,用戶的唯一 ID,一般都是主鍵索引。接近 0,則說(shuō)明字段值重復(fù)度高。
例如,一個(gè)表中有 1000 條記錄,其中性別字段只有兩個(gè)值(男、女),那么性別字段的區(qū)分度只有 0.002。
高區(qū)分度的字段更適合拿來(lái)作為索引,因?yàn)樗饕梢愿行У乜s小查詢范圍。
MySQL查看字段區(qū)分度的命令?
在 MySQL 中,可以通過(guò) COUNT(DISTINCT column_name) 和 COUNT(*) 的比值來(lái)計(jì)算字段的區(qū)分度。例如:
SELECT
COUNT(DISTINCT gender) / COUNT(*) AS gender_selectivity
FROM
users;
MySQL主從復(fù)制流程和原理?
MySQL 的主從復(fù)制(Master-Slave Replication)是一種數(shù)據(jù)同步機(jī)制,用于將數(shù)據(jù)從一個(gè)主數(shù)據(jù)庫(kù)(master)復(fù)制到一個(gè)或多個(gè)從數(shù)據(jù)庫(kù)(slave)。
廣泛用于數(shù)據(jù)備份、災(zāi)難恢復(fù)和數(shù)據(jù)分析等場(chǎng)景。
復(fù)制過(guò)程的主要步驟有:
-
在主服務(wù)器上,所有修改數(shù)據(jù)的語(yǔ)句(如 INSERT、UPDATE、DELETE)會(huì)被記錄到二進(jìn)制日志中。 -
主服務(wù)器上的一個(gè)線程(二進(jìn)制日志轉(zhuǎn)儲(chǔ)線程)負(fù)責(zé)讀取二進(jìn)制日志的內(nèi)容并發(fā)送給從服務(wù)器。 -
從服務(wù)器接收到二進(jìn)制日志數(shù)據(jù)后,會(huì)將這些數(shù)據(jù)寫入自己的中繼日志(Relay Log)。中繼日志是從服務(wù)器上的一個(gè)本地存儲(chǔ)。 -
從服務(wù)器上有一個(gè) SQL 線程會(huì)讀取中繼日志,并在本地?cái)?shù)據(jù)庫(kù)上執(zhí)行,從而將更改應(yīng)用到從數(shù)據(jù)庫(kù)中,完成同步。
MySQL如何查看查詢是否用到了索引?
可以通過(guò) EXPLAIN 關(guān)鍵字來(lái)查看是否使用了索引。
EXPLAIN SELECT * FROM table WHERE column = 'value';
其結(jié)果中的 key 值顯示了查詢是否使用索引,如果使用了索引,會(huì)顯示索引的名稱。
type 列的最好,最好級(jí)別?都代表了什么意思?
從 EXPLAIN 輸出結(jié)果來(lái)看,我們可以得到 MySQL 是如何執(zhí)行查詢的一些關(guān)鍵信息:
-
id: 查詢標(biāo)識(shí)符,這里是 1。 -
select_type: 查詢的類型,這里是 SIMPLE,表示這是一個(gè)簡(jiǎn)單的查詢,沒(méi)有使用子查詢或復(fù)雜的聯(lián)合查詢。 -
table: 正在查詢的表名,這里是 tbn。 -
type: 查詢類型,這里是 range,表示 MySQL 使用了范圍查找。這是因?yàn)椴樵儣l件包含了>操作符,使得 MySQL 需要在索引中查找滿足范圍條件的記錄。 -
possible_keys: 可能被用來(lái)執(zhí)行查詢的索引,這里是 idx_abc,表示 MySQL 認(rèn)為idx_abc索引可能會(huì)用于優(yōu)化查詢。 -
key: 實(shí)際用來(lái)執(zhí)行查詢的索引,也是 idx_abc,這意味著 MySQL 實(shí)際上使用了idx_abc聯(lián)合索引來(lái)優(yōu)化查詢。 -
key_len: 使用索引的長(zhǎng)度,這里是 15字節(jié),這提供了關(guān)于索引使用情況的一些信息,比如哪些列被用在了索引中。 -
ref: 顯示哪些列或常量被用作索引查找的參考。 -
rows: MySQL 估計(jì)為了找到結(jié)果需要檢查的行數(shù),這里是 2。 -
filtered: 表示根據(jù)表的條件過(guò)濾后,剩余多少百分比的結(jié)果,這里是 100.00%,意味著所有掃描的行都會(huì)被返回。 -
Extra: 提供了關(guān)于查詢執(zhí)行的額外信息。 Using index condition表示 MySQL 使用了索引條件推送(Index Condition Pushdown,ICP),這是 MySQL 的一個(gè)優(yōu)化方式,它允許在索引層面過(guò)濾數(shù)據(jù),減少訪問(wèn)表數(shù)據(jù)的需要。
計(jì)網(wǎng)的內(nèi)容
由于全部?jī)?nèi)容都貼出來(lái)實(shí)在太多了,我就把計(jì)網(wǎng)的答案直接放到了《Java 面試指南》中,當(dāng)然也可以通過(guò)面渣逆襲按圖索驥。
內(nèi)容來(lái)源
-
星球嘉賓三分惡的面渣逆襲:https://javabetter.cn/sidebar/sanfene/nixi.html -
二哥的 Java 進(jìn)階之路(GitHub 已有 12000+star):https://javabetter.cn
ending
一個(gè)人可以走得很快,但一群人才能走得更遠(yuǎn)。二哥的編程星球已經(jīng)有 5300 多名球友加入了,如果你也需要一個(gè)良好的學(xué)習(xí)環(huán)境,戳鏈接 ?? 加入我們吧。這是一個(gè)編程學(xué)習(xí)指南 + Java 項(xiàng)目實(shí)戰(zhàn) + LeetCode 刷題的私密圈子,你可以閱讀星球?qū)凇⑾蚨缣釂?wèn)、幫你制定學(xué)習(xí)計(jì)劃、和球友一起打卡成長(zhǎng)。
兩個(gè)置頂帖「球友必看」和「知識(shí)圖譜」里已經(jīng)沉淀了非常多優(yōu)質(zhì)的學(xué)習(xí)資源,相信能幫助你走的更快、更穩(wěn)、更遠(yuǎn)。
歡迎點(diǎn)擊左下角閱讀原文了解二哥的編程星球,這可能是你學(xué)習(xí)求職路上最有含金量的一次點(diǎn)擊。
最后,把二哥的座右銘送給大家:沒(méi)有什么使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧?kù)o的港灣,我是不系之舟。共勉 ??。
