的3個(gè)硬核技術(shù)點(diǎn))
華為公司招聘避坑指南:面試必問(wèn)的3個(gè)硬核技術(shù)點(diǎn)
看了一堆教程還是不會(huì)寫(xiě)項(xiàng)目?這是很多準(zhǔn)備沖擊大廠校招或社招的同學(xué)最大的痛點(diǎn)。尤其是面對(duì)華為公司招聘這種高門(mén)檻、高標(biāo)準(zhǔn)的選拔流程,光背八股文根本不夠用。HR和技術(shù)面試官最看重的是你能不能把知識(shí)落地成可運(yùn)行的代碼。
面試必問(wèn)的環(huán)節(jié)里,基礎(chǔ)不扎實(shí)會(huì)被秒拒,但基礎(chǔ)扎實(shí)卻不會(huì)工程化,同樣會(huì)掛。今天我們就以華為公司招聘中常見(jiàn)的后端開(kāi)發(fā)崗位為例,從零搭建一個(gè)高并發(fā)場(chǎng)景下的“短鏈接生成服務(wù)”。這個(gè)項(xiàng)目不大,但涵蓋了緩存、數(shù)據(jù)庫(kù)索引、異步處理等核心考點(diǎn),足以幫你打通“從教程到項(xiàng)目”的任督二脈。
項(xiàng)目目標(biāo)
我們要做的不是一個(gè)簡(jiǎn)單的CRUD練習(xí),而是一個(gè)能抗住一定并發(fā)壓力的短鏈接服務(wù)。
為什么選這個(gè)?因?yàn)樵谌A為公司招聘的歷年面試真題庫(kù)中,系統(tǒng)設(shè)計(jì)題占比很高,而短鏈接是經(jīng)典的入門(mén)級(jí)高并發(fā)案例。它能考察你對(duì)Redis緩存策略、MySQL自增ID與雪花算法的權(quán)衡、以及后端異步IO的理解。
核心目標(biāo)拆解:輸入:長(zhǎng)URL。
處理:生成一個(gè)6位以內(nèi)的短碼,存入Redis和MySQL。
輸出:返回短鏈接 http://s.example.com/{code}。
查詢:訪問(wèn)短鏈接時(shí),先查Redis,未命中再查MySQL并回填緩存。
性能指標(biāo):?jiǎn)螜C(jī)QPS至少達(dá)到5000,平均響應(yīng)時(shí)間10ms。這個(gè)目標(biāo)設(shè)定很務(wù)實(shí)。很多新手喜歡搞分布式微服務(wù),但如果你連單機(jī)高并發(fā)都沒(méi)吃透,面試官根本不會(huì)給你機(jī)會(huì)展示架構(gòu)能力。華為公司招聘的技術(shù)面往往從最基礎(chǔ)的原理開(kāi)始深挖,直到你答不上來(lái)為止。
目錄結(jié)構(gòu)
工程化是區(qū)分“學(xué)生代碼”和“生產(chǎn)代碼”的分水嶺。在面試必問(wèn)的工程實(shí)踐環(huán)節(jié),目錄混亂會(huì)被直接扣分。我們采用標(biāo)準(zhǔn)的Spring Boot分層架構(gòu),同時(shí)引入Maven管理依賴。
short-link-service/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── shortlink/
│ │ │ ├── ShortLinkApplication.java # 啟動(dòng)類
│ │ │ ├── config/
│ │ │ │ ├── RedisConfig.java # Redis配置
│ │ │ │ └── WebConfig.java # Web配置
│ │ │ ├── controller/
│ │ │ │ └── ShortLinkController.java
│ │ │ ├── service/
│ │ │ │ ├── ShortLinkService.java # 接口
│ │ │ │ └── impl/
│ │ │ │ └── ShortLinkServiceImpl.java
│ │ │ ├── mapper/
│ │ │ │ └── ShortLinkMapper.java # MyBatis Plus
│ │ │ ├── entity/
│ │ │ │ └── ShortLink.java
│ │ │ └── util/
│ │ │ └── SnowflakeIdGenerator.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── mapper/
│ │ │ └── ShortLinkMapper.xml
│ │ └── sql/
│ │ └── init.sql
│ └── test/
│ └── java/
│ └── com/
│ └── example/
│ └── shortlink/
│ └── ShortLinkServiceTest.java注意util包下的SnowflakeIdGenerator。為什么不用數(shù)據(jù)庫(kù)自增ID?因?yàn)樵诟卟l(fā)下,自增ID會(huì)產(chǎn)生熱點(diǎn)行競(jìng)爭(zhēng),導(dǎo)致數(shù)據(jù)庫(kù)性能急劇下降。而雪花算法生成的ID是全局唯一的,且趨勢(shì)遞增,非常適合做短碼的基數(shù)。這一點(diǎn)在華為公司招聘的數(shù)據(jù)庫(kù)優(yōu)化面試中,幾乎必考。
核心代碼實(shí)現(xiàn)
代碼是項(xiàng)目的靈魂。我們不看花哨的框架封裝,直接看核心邏輯。
1. 實(shí)體類與數(shù)據(jù)映射
@Data
@TableName(t_short_link)
public class ShortLink {@TableId(type = IdType.INPUT) // 使用自定義ID,非自增private Long id;private String url; // 原始長(zhǎng)鏈接private String shortCode; // 短碼private Integer hitCount; // 點(diǎn)擊次數(shù)private LocalDateTime createTime;
}這里用了MyBatis Plus,IdType.INPUT表示ID由代碼生成,而非數(shù)據(jù)庫(kù)生成。
2. 雪花算法生成ID
public class SnowflakeIdGenerator {private final long twepoch = 1288834974657L;private final long workerIdBits = 5L;private final long datacenterIdBits = 5L;private final long maxWorkerId = -1L ^ (-1L workerIdBits);private final long maxDatacenterId = -1L ^ (-1L datacenterIdBits);private final long sequenceBits = 12L;private long workerId;private long datacenterId;private long sequence = 0L;private long lastTimestamp = -1L;public SnowflakeIdGenerator(long workerId, long datacenterId) {if (workerId maxWorkerId || workerId 0) {throw new IllegalArgumentException(String.format(worker Id can't be greater than %d or less than 0, maxWorkerId));}if (datacenterId maxDatacenterId || datacenterId 0) {throw new IllegalArgumentException(String.format(datacenter Id can't be greater than %d or less than 0, maxDatacenterId));}this.workerId = workerId;this.datacenterId = datacenterId;}public synchronized long nextId() {long timestamp = currentTimeMillis();if (timestamp lastTimestamp) {throw new RuntimeException(String.format(Clock moved backwards. Refusing to generate id for %s milliseconds, lastTimestamp - timestamp));}if (lastTimestamp == timestamp) {sequence = (sequence + 1) sequenceMask;if (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);}} else {sequence = 0L;}lastTimestamp = timestamp;return ((timestamp - twepoch) timestampLeftShift)| (datacenterId datacenterIdShift)| (workerId workerIdShift)| sequence;}// ... 輔助方法省略
}這段代碼來(lái)自Twitter的開(kāi)源實(shí)現(xiàn),是行業(yè)事實(shí)標(biāo)準(zhǔn)。在官方文檔如Spring Boot或Netty的并發(fā)處理章節(jié)中,都推薦類似的時(shí)間戳+機(jī)器ID+序列號(hào)的組合方式。面試時(shí),如果面試官問(wèn)“如果時(shí)鐘回?fù)茉趺崔k”,你要能答出“等待時(shí)鐘追上”或“使用備用機(jī)器ID”等策略,而不是只會(huì)背代碼。
3. 核心服務(wù)邏輯
@Service
public class ShortLinkServiceImpl implements ShortLinkService {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate ShortLinkMapper shortLinkMapper;@Autowiredprivate SnowflakeIdGenerator idGenerator;private static final String CACHE_KEY_PREFIX = shortlink:;private static final int CACHE_EXPIRE_SECONDS = 3600;@Overridepublic String generateShortLink(String longUrl) {// 1. 校驗(yàn)URL合法性if (!isValidUrl(longUrl)) {throw new IllegalArgumentException(Invalid URL format);}// 2. 檢查緩存是否已存在該長(zhǎng)鏈接String cacheKey = CACHE_KEY_PREFIX + longUrl;String existingCode = redisTemplate.opsForValue().get(cacheKey);if (existingCode != null) {return existingCode;}// 3. 生成短碼String shortCode = generateShortCode();// 4. 保存至數(shù)據(jù)庫(kù)ShortLink entity = new ShortLink();entity.setId(idGenerator.nextId());entity.setUrl(longUrl);entity.setShortCode(shortCode);entity.setHitCount(0);entity.setCreateTime(LocalDateTime.now());shortLinkMapper.insert(entity);// 5. 寫(xiě)入緩存redisTemplate.opsForValue().set(cacheKey, shortCode, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);return shortCode;}private String generateShortCode() {long id = idGenerator.nextId();// 將ID轉(zhuǎn)換為36進(jìn)制字符串,縮短長(zhǎng)度return Long.toString(id, 36);}private boolean isValidUrl(String url) {// 簡(jiǎn)單的正則校驗(yàn),實(shí)際生產(chǎn)環(huán)境建議使用更嚴(yán)格的URI解析return url != null url.matches(https?://.*);}
}逐行解析關(guān)鍵點(diǎn):緩存穿透防護(hù):這里沒(méi)有做布隆過(guò)濾器,因?yàn)槎替溄邮恰跋炔楹髮?xiě)”,如果URL不存在,我們直接生成新的。但在真實(shí)場(chǎng)景中,如果擔(dān)心惡意查詢不存在的URL,需要加布隆過(guò)濾器。
36進(jìn)制轉(zhuǎn)換:Long.toString(id, 36) 是生成短碼的關(guān)鍵。二進(jìn)制轉(zhuǎn)十進(jìn)制太長(zhǎng),轉(zhuǎn)36進(jìn)制(0-9, a-z)可以顯著縮短字符串長(zhǎng)度。
原子性:注意,insert和set之間不是原子的。如果DB插入成功但Redis寫(xiě)入失敗,下次請(qǐng)求會(huì)再次生成新短碼,導(dǎo)致同一個(gè)長(zhǎng)URL對(duì)應(yīng)多個(gè)短碼。這在業(yè)務(wù)上通常可接受,但如果要求嚴(yán)格唯一,需要使用Redis的SETNX或數(shù)據(jù)庫(kù)唯一索引兜底。運(yùn)行與測(cè)試
代碼寫(xiě)完,必須跑通。在華為公司招聘的現(xiàn)場(chǎng)筆試或編程題中,代碼能跑通、輸出正確,是及格線。
1. 數(shù)據(jù)庫(kù)初始化
CREATE TABLE `t_short_link` (`id` bigint(20) NOT NULL COMMENT '主鍵ID',`url` varchar(255) NOT NULL COMMENT '原始鏈接',`short_code` varchar(10) NOT NULL COMMENT '短碼',`hit_count` int(11) DEFAULT '0' COMMENT '點(diǎn)擊次數(shù)',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',PRIMARY KEY (`id`),UNIQUE KEY `uk_short_code` (`short_code`),KEY `idx_url` (`url`(191)) -- 前綴索引,因?yàn)閡rl可能很長(zhǎng)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;避坑提示:url字段如果直接加唯一索引,會(huì)占用大量空間且效率低。使用前綴索引idx_url(191)可以解決長(zhǎng)字符串索引問(wèn)題。這也是MySQL優(yōu)化的經(jīng)典考點(diǎn)。
2. JUnit單元測(cè)試
@SpringBootTest
public class ShortLinkServiceTest {@Autowiredprivate ShortLinkService shortLinkService;@Testpublic void testGenerateShortLink() {String longUrl = https://www.example.com/very/long/path?id=123name=test;String shortCode = shortLinkService.generateShortLink(longUrl);// 斷言1:短碼不為空assertNotNull(shortCode);// 斷言2:短碼長(zhǎng)度在合理范圍內(nèi)assertTrue(shortCode.length() = 10);// 斷言3:再次生成,應(yīng)返回相同的短碼(緩存生效)String shortCode2 = shortLinkService.generateShortLink(longUrl);assertEquals(shortCode, shortCode2);}
}測(cè)試時(shí),務(wù)必使用@SpringBootTest加載完整上下文,包括Redis連接。如果本地沒(méi)裝Redis,可以用Docker快速啟動(dòng):
docker run -d -p 6379:6379 redis:alpine在面試必問(wèn)的測(cè)試環(huán)節(jié),面試官會(huì)問(wèn):“如果Redis掛了,你的服務(wù)會(huì)怎樣?”
答案應(yīng)該是:服務(wù)降級(jí)。捕獲Redis異常,直接查MySQL。雖然性能下降,但保證可用性。這叫“熔斷”思想的初級(jí)應(yīng)用。
優(yōu)化擴(kuò)展
項(xiàng)目能跑,不代表項(xiàng)目好。在華為公司招聘的終面或架構(gòu)面中,優(yōu)化能力決定了你能拿多高的Offer。
1. 異步落庫(kù)優(yōu)化
上面的代碼是同步寫(xiě)DB和Redis。在高并發(fā)下,DB寫(xiě)入會(huì)成為瓶頸。
優(yōu)化方案:引入消息隊(duì)列(如Kafka或RabbitMQ)。生成短碼后,只寫(xiě)Redis。
發(fā)送消息到MQ。
消費(fèi)者異步消費(fèi)消息,寫(xiě)入MySQL。這樣,主鏈路(用戶請(qǐng)求)的耗時(shí)從“Redis+MySQL”變成了“Redis”,延遲降低50%以上。
2. 緩存雪崩防護(hù)
所有短鏈接的過(guò)期時(shí)間如果設(shè)置成一樣的,一旦大量key同時(shí)過(guò)期,流量會(huì)瞬間打到DB。
優(yōu)化方案:在CACHE_EXPIRE_SECONDS基礎(chǔ)上,增加一個(gè)隨機(jī)數(shù)(例如0-300秒)。
int randomExpire = ThreadLocalRandom.current().nextInt(300);
redisTemplate.opsForValue().set(cacheKey, shortCode, CACHE_EXPIRE_SECONDS + randomExpire, TimeUnit.SECONDS);3. 點(diǎn)擊計(jì)數(shù)異步化
hitCount字段如果在每次訪問(wèn)時(shí)都更新DB,性能極差。
優(yōu)化方案:使用Redis的INCR命令原子遞增計(jì)數(shù),定時(shí)任務(wù)(如每5分鐘)將Redis中的計(jì)數(shù)同步回MySQL。
這些優(yōu)化點(diǎn),每一個(gè)都足以作為面試必問(wèn)的深入話題。不要怕答不出來(lái),要敢于展示你的思考過(guò)程。面試官喜歡的是“我知道哪里慢,我嘗試過(guò)這樣優(yōu)化,雖然沒(méi)上線但我知道原理”的候選人。
小結(jié)
這個(gè)項(xiàng)目雖然簡(jiǎn)單,但覆蓋了華為公司招聘后端崗的三大核心:并發(fā)控制(雪花算法、同步鎖)、緩存策略(穿透、雪崩、一致性)、數(shù)據(jù)庫(kù)優(yōu)化(索引、異步)。
記住,教程是死的,項(xiàng)目是活的。你不需要寫(xiě)出百萬(wàn)行代碼,但你需要把一個(gè)百行代碼的項(xiàng)目,講出深度,講出坑,講出優(yōu)化思路。
你在項(xiàng)目里踩過(guò)這個(gè)坑嗎?比如雪花算法時(shí)鐘回?fù)軐?dǎo)致ID重復(fù),或者Redis與MySQL數(shù)據(jù)不一致?評(píng)論區(qū)聊聊,看看有多少人中招。