【從一台 Server 到分散式架構】第 27 篇:用同樣的思維看 Uber——即時位置與派車系統
跟 YouTube 不同,YouTube 的挑戰主要是「讀多寫少」——觀看遠多於上傳。
Uber 的挑戰是:讀多、也寫多、而且高度即時。
- 每台上路的司機,每 4 秒更新一次位置
- 全球幾百萬台司機,每秒幾十萬筆位置更新
- 乘客叫車後,要在幾秒內找到合適的司機並發送派單
- 派單要剛好派給一位司機,不能重複,不能漏
把 Uber 拆成幾個子問題
- 位置更新:司機 / 乘客的位置怎麼存、怎麼查
- 附近司機搜尋:給定一個位置,如何快速找到方圓 N 公里的司機
- 配對與派單:找到候選司機後,如何決定派給誰、如何確保只派給一人
- 行程管理:行程開始、進行中、結束的狀態機
- 即時通知:乘客和司機的即時更新(位置、狀態)
問題一:位置更新——高寫入頻率
全球幾百萬台司機,每 4 秒更新一次位置,是每秒幾十萬筆的寫入。
普通的關聯式資料庫扛不住這種頻率。
解法:專門的位置服務 + 記憶體儲存
- 用 Redis 存每台司機的「當前位置」(不需要歷史,只需要最新位置)
- Redis 的寫入速度足以應付這個頻率
- 同時,位置更新也可以先寫進 Message Queue,再由 Worker 處理——削峰填谷(第 08 篇)
對於位置的歷史記錄(例如行程軌跡),再另外存到適合寫多的 DB(Cassandra / InfluxDB)。
問題二:附近司機搜尋——地理空間索引
「找出方圓 5 公里內的空閒司機」,如果用普通的資料庫查詢:
SELECT * FROM drivers
WHERE lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?
AND status = 'available'這樣的範圍查詢,在幾百萬筆資料裡非常慢,而且「方形範圍」不是真正的圓形距離。
解法:Geohash 或 S2 地理空間索引
Geohash 把地球表面切成一格一格,每個格子有一個字串編碼。相鄰的格子,字串前綴也相同(大致):
台北市中心 → Geohash: "wqqm"
附近某點 → Geohash: "wqqk" (前綴 wqq 相同)
把司機依 Geohash 分組存在 Redis 裡,搜尋附近司機就變成「找同一格或相鄰格的所有司機」,速度快很多。
Redis 本身也支援 GEO 指令(GEOADD、GEORADIUS),可以直接做到「找半徑 N 公里內的所有司機」,是很多簡易版本的選擇。
問題三:配對與派單——不能重複派
找到幾個候選司機後,要選一個發送派單。
問題:如果同時有兩個乘客叫車,找到的候選司機有重疊,可能兩個乘客都把 A 司機選為最佳選項,同時向 A 發派單——A 只能接一個,另一個就白等了。
解法:分散式鎖(第 18 篇)
派單前,對「司機 ID」加鎖:
嘗試對 driver:A 加鎖
→ 成功 → 向 A 發派單,等 A 確認後釋放鎖(或拒絕後釋放)
→ 失敗 → A 已被鎖定,選下一個候選司機
這樣就算多個派單系統同時競爭,也只有一個能成功向 A 發出請求。
問題四:乘客和司機的即時通訊——WebSocket
行程進行中,乘客要看到司機的位置在移動;司機要收到新的派單通知。
這就是第 17 篇的 WebSocket + Pub/Sub——完全一樣的架構:
- 乘客 App 和司機 App 各自建立 WebSocket 連線
- 位置更新透過 Pub/Sub 廣播給相關的 WebSocket Server
- WebSocket Server 推送給對應的 App
Uber 的高層架構
Uber 的核心取捨
| 挑戰 | 解法 | 取捨 |
|---|---|---|
| 司機位置高頻寫入 | Redis 存當前位置,MQ 做緩衝 | 位置可接受幾秒的延遲,不需要強一致 |
| 附近司機查詢 | Geohash / Redis GEO | 速度換精確度:邊界情況可能找不到幾百公尺外的司機 |
| 派單不重複 | 分散式鎖 | 鎖的粒度影響效率;司機拒絕後要快速釋放 |
| 即時推送 | WebSocket + Pub/Sub | 長連線資源消耗,離線要有補推機制 |
小結
Uber 看起來複雜,但拆開來都是熟悉的工具組合:
- 高頻位置寫入 → Redis + Message Queue(第 05、08 篇)
- 地理空間搜尋 → Geohash / Redis GEO(索引的一種)
- 派單不重複 → 分散式鎖(第 18 篇)
- 即時通訊 → WebSocket + Pub/Sub(第 17 篇)
下一篇,我們換個場景:Twitter / 社群動態系統——讀多寫也多,加上時序和關注關係,又是一套不同的取捨。