京東搶購服務高併發實踐
宣告:本位來自京東張開濤的微信公眾號(kaitao-1234567),授權CSDN轉載,如需轉載請聯絡作者。
作者:張子良,京東高階開發工程師,在京東負責搶購後端服務系統架構和開發工作。
責編:錢曙光,關注架構和演算法領域,尋求報導或者投稿請發郵件qianshg@csdn.net,另有「CSDN 高階架構師群」,內有諸多知名網際網路公司的大牛架構師,歡迎架構師加微信qshuguang2008申請入群,備註姓名+公司+職位。
服務介紹
限時搶購又稱閃購,英文Flash sale,起源於法國網站Vente Privée。閃購模式即是以網際網路為媒介的B2C電子零售交易活動,以限時特賣的形式,定期定時推出國際知名品牌的商品,一般以原價1-5折的價格供專屬會員限時搶購,每次特賣時間持續5-10天不等,先到先買,限時限量,售完即止。顧客在指定時間內(一般為20分鐘)必須付款,否則商品會重新放到待銷售商品的行列裡。
模式特徵:
品牌豐富 —— 推出國內外一二線名牌商品,供消費者購買選擇;
時間短暫 —— 每個品牌推出時間短暫,一般為5—10天,先到先買,限量售賣,售完即止;
折扣超低 —— 以商品原價1—5折的價格銷售,折扣力度大。
摘自【百度百科】,通過這段簡介相信對限時搶購有了一定的瞭解,我們內部稱之為搶購系統。
對於搶購系統來說,首先要有可搶購的活動,而且這些活動具有促銷性質,比如直降500元。其次要求可搶購的活動類目豐富,使用者才有充分的選擇性。618(6.1-6.20)期間增量促銷活動量非常多,可能某個活動力度特別大,大多使用者都在搶,必然對系統是一個考驗。這樣搶購系統具有秒殺特性,併發訪問量高,同時使用者也可選購多個限時搶商品,與普通商品一起進購物車結算。這種大型活動的負載可能是平時的幾十倍,所以通過增加硬體、優化瓶頸程式碼等手段是很難達到目標的,所以搶購系統得專門設計。
服務主要功能
建立促銷服務:採銷建立促銷後,促銷管理系統稽核通過後,會呼叫搶購系統建立促銷;
搶服務:為符合條件的訂單操作剩餘數,主要是扣減剩餘數;
針對哪些SKU
目前主要為單品促銷,直降或者一口價,比如:
主要渠道
移動APP、微信、手Q和主站
限購型別
限數量、限ip、限pin和限制ip與pin
系統設計要點
如何實現實時庫存?
這裡說的庫存不是真正意義上的庫存,其實是該促銷可以搶購的數量,真正的庫存在基礎庫存服務。使用者點選『提交訂單』按鈕後,在搶購系統中獲取了資格後才去基礎庫存服務中扣減真正的庫存;而搶購系統控制的就是資格/剩餘數。傳統方案利用資料庫行鎖,但是在促銷高峰資料庫壓力過大導致服務不可用,目前採用redis叢集(16分片)快取促銷資訊,例如促銷id、促銷剩餘數、搶次數等,搶的過程中按照促銷id雜湊到對應分片,實時扣減剩餘數。當剩餘數為0或促銷刪除,價格恢復原價。
如何設計搶購redis資料結構?
採銷人員釋出促銷後,在搶購redis中生成一筆記錄,給搶服務提供基本資訊。每一個促銷對應一個促銷id,促銷資訊是Hashes結構。
例如促銷A,對應的型別為單品促銷,我們暫且認為型別值為1,對應redis中的key為 C_A_1,資料結構內容類似於如下:
o: 100 // 原始數量
b: 99 // 可搶購數量,假如搶購了一個剩下了99
c: 1 // 搶購次數記錄,用來限流,後面會介紹到
如何保證不超賣?
因為扣減資格是一組操作,我們利用EVAL操作redis剩餘數實現原子化操作,虛擬碼如下:
local key = KEYS[1]
local tag = "b"
local num = tonumber(ARGV[1]);
local lastNum = redis.call('HINCRBY',key,tag,-num);
if業務性判斷ortonumber(lastNum) == 0then
return lastNum
end
如上程式碼會返回剩餘數,如果小於等於0了,則沒有庫存了。
如何提高吞吐量?
減少網路互動(一次搶資料通過 EVALSHA 一次性提交給redis叢集);資料庫操作非同步化(使用JMQ非同步記錄日誌)。
如何保證可用性?
採用JSF(京東內部SOA框架)對外開放服務(搶服務和釋出促銷服務),可降級為系統自身webservice服務;
搶購系統主要依賴於redis叢集,redis採用一主三從叢集方案,部署在兩個機房,每個叢集16個分片,每兩分片共用一臺物理機,可通過配置中心切換主從;
如果Redis掛掉了,如何恢復呢?通過彙總MySQL中的搶購和取消流水日誌,並恢復Redis的搶購數量。
系統架構
這裡主要涉及搶服務架構剖析,因為它具有典型的高併發特性,下面是基本架構概圖:
注:此處的庫存是可搶購數量設定,或者叫做資格/剩餘數,並非真正的實際庫存。
搶服務流程
Redis使用單個Lua直譯器去執行所有指令碼,並且Redis 也保證指令碼會以原子性(atomic)的方式執行:當某個指令碼正在執行的時候,不會有其他指令碼或Redis命令被執行。這種特性很好的解決了搶服務流程中併發帶來的問題。
REDIS+LUA搶購子流程:
此流程通過lua Script指令碼實現,我們暫時命名為q.lua(主要功能限流和扣減促銷活動剩餘數)。這樣把搶購流程與Script指令碼結合,一次性提交給Redis減少網路互動,使得效能大大提升。
q.lua虛擬碼:
--[[
--!@brief 促銷Id下限流:可以防止某個促銷過熱導致服務不可以用
--]]
local function limited()
-- todo: 實現
end
--[[
--!@brief 限制邏輯(ip和pin):比如有的促銷是限制ip,這裡校驗ip是否存在,如果為限ip型別搶購活動,存在丟擲異常告知ip已經存在不能搶購
--]]
local function check_ip_pin()
-- todo: 實現
end
--[[
--!@brief 記錄訂單號:主要目的實現搶方法冪等性,呼叫方網路超時可以重複呼叫,存在訂單號直接返回搶購成功,不至於超賣
--]]
local function record_order_id()
-- todo: 實現
end
--[[
--!@brief 扣減剩餘數
--]]
local function scalebuy()
--
local lastNum = redis.call('HINCRBY',key,tag,-num);
--
end
-- 呼叫順序不可調整
-- 1 限流
local status,msg = limited()
if status == 0then
return msg
end
-- 2 校驗
status,msg = check_ip_pin()
if status == 0 then
return msg
end
-- 3 記錄訂單
status,msg = record_order_id()
if status == 0 then
return msg
end
-- 4 扣減剩餘數
status,msg = scalebuy()
if status == 0 then
return msg
end
-- 5 返回成功標示
return 1
子流程具體如下:
1、解析請求引數,根據促銷Id按照Jedis中MurmurHash演算法獲取分片,然後按照分片包裝Pipeline批量傳送請求引數argList;
2、獲取系統初始化時SCRIPT LOAD載入q.lua返回的串shaValue;
3、執行EVALSHA,虛擬碼如下:
// 其他操作
Pipeline p;
// 初始化p
p.evalsha(shaValue,keyList, argList);
// 其他操作
4、處理返回結果,只要有一個分片失敗,本次搶購就失敗。
補充:詳細Script操作可以參考Jedis中 ScriptingCommandsTest。
JMQ傳送子流程:
執行REDIS+LUA搶購子流程成功僅僅代表著操作redis成功,傳送jmq(京東mq基礎服務)成功(後端非同步將實時庫存更新到MySQL)才算一筆搶購成功,否則算搶購失敗。這麼設計的原因主要是保證搶購redis和mysql記錄最終一致,傳送失敗需要回滾REDIS+LUA搶購子流程(恢復Redis的庫存和搶購資格)。當然要考慮降級,jmq不可用時,直接切到jsf服務模擬jmq,也就是直接寫MySQL庫,前提是限流次數調小,否則資料庫有壓力過大的風險。這樣雖然使用者體驗下降了,但是服務依然可用。開關都在配置中心操作,一分鐘內生效。
資格回滾子流程:
傳送JMQ失敗必須回滾,否則就出現了超賣現象,具體流程同REDIS+LUA搶購子流程類似,是它的逆向流程,只不過執行指令碼不同罷了。
限流處理
方法級限流,限流閾值通過配置中心配置,一分鐘生效,虛擬碼如下:
private static AtomicInteger atomic = new AtomicInteger(0);
public void test() {
try {
// 限流
int limitNum = XXX.getLimitNum();
int nowConcurrent = atomic.incrementAndGet();
if(nowConcurrent > limitNum) {
// 異常處理
}
// 正常業務邏輯
} catch(Exception e) {
// 異常處理
} finally {
atomic.decrementAndGet();
}
}
q.lua中促銷級別的限流,主要利用C_A_1中c的搶次數和閾值比對。比如促銷A,60秒內只能搶60000次,超過閾值60000該促銷就會搶購失敗。
到此搶購系統的核心邏輯就介紹完了,這裡邊還有一些細節問題需要大家在設計時思考,如限購(如每個人限購2個)、真實庫存不足取消、使用者取消訂單歸還資格、Redis掛了恢復資料、停促銷(時間過期停、庫存不足停)等。
相關閱讀:
2016年8月12-13日由CSDN主辦的SDCC 2016架構&運維峰會將在成都站召開,5人以上團購或者購買兩場峰會通票更有特惠,餘票不足,預購從速。(票務詳情連結)。
更多詳細內容參見官網網址:SDCC資料庫&架構峰會成都站,大會報名。
相關文章
- Laravel 高併發搶購模擬Laravel
- Redis 實現高併發下的搶購 / 秒殺功能Redis
- 關於PHP高併發搶購系統設計PHP
- 秒殺搶購思路以及高併發下資料安全
- 資料服務化在京東的實踐
- PHP開發中多種方案實現高併發下的搶購、秒殺功能PHP
- 遊戲服務端的高併發和高可用遊戲服務端
- Nginx Ingress 高併發實踐Nginx
- 從 Clickhouse 到 Apache Doris,慧策電商 SaaS 高併發資料服務的改造實踐Apache
- Redis+Lua解決高併發場景搶購秒殺問題Redis
- 服務發現與配置管理高可用最佳實踐
- 高併發解決方案orleans實踐
- 京東購物小程式cookie方案實踐Cookie
- 線上Redis高併發效能調優實踐Redis
- 小米網搶購系統開發實踐和我的個人觀察
- 微信小程式搶紅包高併發設計微信小程式
- 極驗高併發驗證服務背後的技術實現
- 高併發服務的幾條優化經驗優化
- 從服務端視角看高併發難題服務端
- Python_服務端效能高併發測試Python服務端
- 高併發服務端分散式系統設計概要服務端分散式
- 京東微信購物首頁效能優化實踐優化
- 京東短網址高可用提升最佳實踐
- [分散式][高併發]熔斷策略和最佳實踐分散式
- 高併發場景下JVM調優實踐之路JVM
- 高併發IM系統架構優化實踐架構優化
- 構建高併發高可用的電商平臺架構實踐架構
- 京東搶購失敗?試試用 python 準時自動搶購 (註釋詳盡)Python
- 基於 Python 自建分散式高併發 RPC 服務Python分散式RPC
- Go單體服務開發最佳實踐Go
- Go 單體服務開發最佳實踐Go
- Golang 高效實踐之併發實踐Golang
- Web系統大規模併發——電商秒殺與搶購Web
- 微服務5:服務註冊與發現(實踐篇)微服務
- 微服務實戰:服務發現的可行方案以及實踐案例微服務
- 京東短網址高可用提升最佳實踐 | 京東雲技術團隊
- 京東購物小程式 | Taro3 專案分包實踐
- 快速構建高併發微服務微服務