100萬QPS短鏈系統(tǒng)如何設(shè)計(jì)?
前言
凌晨兩點(diǎn),監(jiān)控大屏突然飆紅——短鏈服務(wù)QPS突破80萬! 數(shù)據(jù)庫連接池告急,Redis集群響應(yīng)延遲突破500ms。
這不是演習(xí),而是某電商平臺(tái)大促的真實(shí)場(chǎng)景。
當(dāng)每秒百萬級(jí)請(qǐng)求涌向你的短鏈服務(wù),你該如何設(shè)計(jì)系統(tǒng)?
今天這篇文章跟大家一起聊聊100萬QPS短鏈系統(tǒng)要如何設(shè)計(jì)?
希望對(duì)你會(huì)有所幫助。
最近建了一些工作內(nèi)推群,各大城市都有,歡迎各位HR和找工作的小伙伴進(jìn)群交流,群里目前已經(jīng)收集了不少的工作內(nèi)推崗位。
1.短鏈系統(tǒng)的核心挑戰(zhàn)
首先我們一起看看設(shè)計(jì)一個(gè)高并發(fā)的短鏈系統(tǒng),會(huì)遇到哪些核心的挑戰(zhàn)。
如下圖所示:
圖片
百萬QPS下的三大生死關(guān):
- ID生成瓶頸:傳統(tǒng)數(shù)據(jù)庫自增ID撐不住百萬并發(fā)
- 跳轉(zhuǎn)性能黑洞:302重定向的TCP連接成本
- 緩存雪崩風(fēng)險(xiǎn):熱點(diǎn)短鏈瞬間擊穿Redis
2.短鏈生成
2.1 發(fā)號(hào)器的設(shè)計(jì)
發(fā)號(hào)器是短鏈系統(tǒng)的發(fā)動(dòng)機(jī)。
方案對(duì)比:
方案 | 吞吐量 | 缺點(diǎn) | 適用場(chǎng)景 |
UUID | 5萬/s | 長度長,無法排序 | 小型系統(tǒng) |
Redis自增ID | 8萬/s | 依賴緩存持久化 | 中型系統(tǒng) |
Snowflake | 12萬/s | 時(shí)鐘回?fù)軉栴} | 中大型系統(tǒng) |
分段發(fā)號(hào) | 50萬/s | 需要預(yù)分配 | 超大型系統(tǒng) |
分段發(fā)號(hào)器實(shí)現(xiàn)(Java版):
public class SegmentIDGen {
privatefinal AtomicLong currentId = new AtomicLong(0);
privatevolatilelong maxId;
privatefinal ExecutorService loader = Executors.newSingleThreadExecutor();
public void init() {
loadSegment();
loader.submit(this::daemonLoad);
}
private void loadSegment() {
// 從DB獲取號(hào)段:SELECT max_id FROM alloc WHERE biz_tag='short_url'
this.maxId = dbMaxId + 10000; // 每次取1萬個(gè)號(hào)
currentId.set(dbMaxId);
}
private void daemonLoad() {
while (currentId.get() > maxId * 0.8) {
loadSegment(); // 號(hào)段使用80%時(shí)異步加載
}
}
public long nextId() {
if (currentId.get() >= maxId) thrownew BusyException();
return currentId.incrementAndGet();
}
}
關(guān)鍵優(yōu)化:
- 雙Buffer異步加載(避免加載阻塞)
- 監(jiān)控號(hào)段使用率(動(dòng)態(tài)調(diào)整步長)
- 多實(shí)例分段隔離(biz_tag區(qū)分業(yè)務(wù))
2.2 短鏈映射算法
短碼映射將長ID轉(zhuǎn)換成62進(jìn)制的字符串。
轉(zhuǎn)換原理:
2000000000 = 2×62^4 + 17×62^3 + 35×62^2 + 10×62 + 8
= "Cdz9a"
原始ID: 2000000000,轉(zhuǎn)換為62進(jìn)制的值為Cdz9a。
// Base62編碼(0-9a-zA-Z)
publicclass Base62Encoder {
privatestaticfinal String BASE62 =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public static String encode(long id) {
StringBuilder sb = new StringBuilder();
while (id > 0) {
sb.append(BASE62.charAt((int)(id % 62)));
id /= 62;
}
return sb.reverse().toString();
}
// 測(cè)試:生成8位短碼
public static void main(String[] args) {
long id = 1_000_000_000L;
System.out.println(encode(id)); // 輸出:BFp3qQ
}
}
編碼優(yōu)勢(shì):
- 6位短碼可表示 62^6 ≈ 568億種組合
- 8位短碼可表示 62^8 ≈ 218萬億種組合
- 無意義字符串避免被猜測(cè)
3.存儲(chǔ)架構(gòu)
3.1 數(shù)據(jù)存儲(chǔ)模型設(shè)計(jì)
圖片
3.2 緩存層級(jí)設(shè)計(jì)
圖片
3.3 緩存擊穿解決方案
// Redis緩存擊穿防護(hù)
public String getLongUrl(String shortCode) {
// 1. 布隆過濾器預(yù)檢
if (!bloomFilter.mightContain(shortCode)) {
returnnull;
}
// 2. 查Redis
String cacheKey = "url:" + shortCode;
String longUrl = redis.get(cacheKey);
if (longUrl != null) {
return longUrl;
}
// 3. 獲取分布式鎖
String lockKey = "lock:" + shortCode;
if (redis.setnx(lockKey, "1", 10)) { // 10秒超時(shí)
try {
// 4. 二次檢查緩存
longUrl = redis.get(cacheKey);
if (longUrl != null) return longUrl;
// 5. 查數(shù)據(jù)庫
longUrl = db.queryLongUrl(shortCode);
if (longUrl != null) {
// 6. 回填Redis
redis.setex(cacheKey, 3600, longUrl);
}
return longUrl;
} finally {
redis.del(lockKey);
}
} else {
// 7. 等待重試
Thread.sleep(50);
return getLongUrl(shortCode);
}
}
防護(hù)要點(diǎn):
- 布隆過濾器攔截非法短碼
- 分布式鎖防止緩存擊穿
- 雙重檢查減少DB壓力
- 指數(shù)退避重試策略
4.跳轉(zhuǎn)優(yōu)化
4.1 Nginx層直接跳轉(zhuǎn)
server {
listen 80;
server_name s.domain.com;
location ~ ^/([a-zA-Z0-9]{6,8})$ {
set $short_code $1;
# 查詢Redis
redis_pass redis_cluster;
redis_query GET url:$short_code;
# 命中則直接302跳轉(zhuǎn)
if ($redis_value != "") {
add_header Cache-Control "private, max-age=86400";
return 302 $redis_value;
}
# 未命中轉(zhuǎn)發(fā)到后端
proxy_pass http://backend;
}
}
性能收益:
- 跳轉(zhuǎn)延遲從100ms降至5ms
- 節(jié)省后端服務(wù)器資源
- 支持百萬級(jí)并發(fā)連接
4.2 連接池優(yōu)化
連接池優(yōu)化可以用Netty實(shí)現(xiàn):
// Netty HTTP連接池配置
publicclass HttpConnectionPool {
privatefinal EventLoopGroup group = new NioEventLoopGroup();
privatefinal Bootstrap bootstrap = new Bootstrap();
public HttpConnectionPool() {
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new HttpClientInitializer());
}
public Channel getChannel(String host, int port) throws InterruptedException {
return bootstrap.connect(host, port).sync().channel();
}
// 使用示例
public void redirect(ChannelHandlerContext ctx, String longUrl) {
Channel channel = getChannel("target.com", 80);
channel.writeAndFlush(new DefaultFullHttpRequest(
HttpVersion.HTTP_1_1,
HttpMethod.GET,
longUrl
));
// 處理響應(yīng)...
}
}
優(yōu)化效果:
- TCP連接復(fù)用率提升10倍
- 減少80%的TCP握手開銷
- QPS承載能力提升3倍
5.百萬QPS整體架構(gòu)
百萬QPS整體架構(gòu)如下圖所示:
圖片
核心組件解析:
1)接入層
- CDN:緩存靜態(tài)資源
- Nginx:處理302跳轉(zhuǎn),本地緩存熱點(diǎn)數(shù)據(jù)
2)緩存層
- Redis集群:緩存短鏈映射
- 布隆過濾器:攔截非法請(qǐng)求
3)服務(wù)層
- 短鏈生成:分布式ID服務(wù)
- 映射查詢:高并發(fā)查詢服務(wù)
4)存儲(chǔ)層
- MySQL:分庫分表存儲(chǔ)映射關(guān)系
- TiKV:分布式KV存儲(chǔ)ID生成狀態(tài)
6.容災(zāi)設(shè)計(jì)
6.1 限流熔斷策略
基于Sentinel的熔斷降級(jí):
public class RedirectController {
@GetMapping("/{shortCode}")
@SentinelResource(
value = "redirectService",
fallback = "fallbackRedirect",
blockHandler = "blockRedirect"
)
public ResponseEntity redirect(@PathVariable String shortCode) {
// 跳轉(zhuǎn)邏輯...
}
// 熔斷降級(jí)方法
public ResponseEntity fallbackRedirect(String shortCode, Throwable ex) {
return ResponseEntity.status(503)
.body("服務(wù)暫時(shí)不可用");
}
// 限流處理方法
public ResponseEntity blockRedirect(String shortCode, BlockException ex) {
return ResponseEntity.status(429)
.body("請(qǐng)求過于頻繁");
}
}
6.2 多級(jí)降級(jí)方案
使用多級(jí)降級(jí)方案:
圖片
保證服務(wù)的高可用。
6.3 數(shù)據(jù)分片策略
基于短碼分庫分表:
public int determineDbShard(String shortCode) {
// 取短碼首字母的ASCII值
int ascii = (int) shortCode.charAt(0);
// 分16個(gè)庫
return ascii % 16;
}
public int determineTableShard(String shortCode) {
// 取短碼的CRC32值
CRC32 crc32 = new CRC32();
crc32.update(shortCode.getBytes());
// 每庫1024張表
return (int) (crc32.getValue() % 1024);
}
這里成了16個(gè)庫,每個(gè)庫有1024張表。
7.性能壓測(cè)數(shù)據(jù)對(duì)比
優(yōu)化點(diǎn) | 優(yōu)化前QPS | 優(yōu)化后QPS | 提升倍數(shù) |
原始方案 | 12,000 | - | 1x |
+Redis緩存 | 120,000 | 10x | |
+Nginx直跳 | 350,000 | 2.9x | |
+連接池優(yōu)化 | 780,000 | 2.2x | |
+布隆過濾器 | 1,200,000 | 1.5x |
壓測(cè)環(huán)境:32核64G服務(wù)器 × 10臺(tái),千兆內(nèi)網(wǎng)
總結(jié)
百萬QPS短鏈架構(gòu)核心要點(diǎn)如圖所示:
圖片
四大設(shè)計(jì)原則:
- 無狀態(tài)設(shè)計(jì):跳轉(zhuǎn)服務(wù)完全無狀態(tài),支持無限擴(kuò)展
- 讀多寫少優(yōu)化:將讀性能壓榨到極致
- 分而治之:數(shù)據(jù)分片,流量分散
- 柔性可用:寧可部分降級(jí),不可全線崩潰
真正的架構(gòu)藝術(shù)不在于復(fù)雜,而在于在百萬QPS洪流中,用最簡單的路徑解決問題。當(dāng)你的系統(tǒng)能在流量風(fēng)暴中優(yōu)雅舞蹈,才是架構(gòu)師的巔峰時(shí)刻。