京東搶購服務高併發實踐

做者:張子良,京東高級開發工程師,在京東負責搶購後端服務系統架構和開發工做。mysql

 

服務介紹web

限時搶購又稱閃購,英文Flash sale,起源於法國網站Vente Privée。閃購模式便是以互聯網爲媒介的B2C電子零售交易活動,以限時特賣的形式,按期定時推出國際知名品牌的商品,通常以原價1-5折的價格供專屬會員限時搶購,每次特賣時間持續5-10天不等,先到先買,限時限量,售完即止。顧客在指定時間內(通常爲20分鐘)必須付款,不然商品會從新放到待銷售商品的行列裏。redis

 

 

模式特徵:
品牌豐富 —— 推出國內外一二線名牌商品,供消費者購買選擇;
時間短暫 —— 每一個品牌推出時間短暫,通常爲5—10天,先到先買,限量售賣,售完即止;
折扣超低 —— 以商品原價1—5折的價格銷售,折扣力度大。
摘自【百度百科】,經過這段簡介相信對限時搶購有了必定的瞭解,咱們內部稱之爲搶購系統。算法

 

對於搶購系統來講,首先要有可搶購的活動,並且這些活動具備促銷性質,好比直降500元。其次要求可搶購的活動類目豐富,用戶纔有充分的選擇性。618(6.1-6.20)期間增量促銷活動量很是多,可能某個活動力度特別大,大多用戶都在搶,必然對系統是一個考驗。這樣搶購系統具備秒殺特性,併發訪問量高,同時用戶也可選購多個限時搶商品,與普通商品一塊兒進購物車結算。這種大型活動的負載多是平時的幾十倍,因此經過增長硬件、優化瓶頸代碼等手段是很難達到目標的,因此搶購系統得專門設計。sql

 

服務主要功能數據庫

建立促銷服務:採銷建立促銷後,促銷管理系統審覈經過後,會調用搶購系統建立促銷;後端

搶服務:爲符合條件的訂單操做剩餘數,主要是扣減剩餘數;緩存

 

針對哪些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

 

子流程具體以下:

一、解析請求參數,根據促銷Id按照Jedis中MurmurHash算法獲取分片,而後按照分片包裝Pipeline批量發送請求參數argList;

二、獲取系統初始化時SCRIPT LOAD加載q.lua返回的串shaValue;

三、執行EVALSHA,僞代碼以下:

// 其餘操做

Pipeline p;

// 初始化p

p.evalsha(shaValue,keyList, argList);

// 其餘操做

四、處理返回結果,只要有一個分片失敗,本次搶購就失敗。

 

補充:詳細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() {

        int limitNum = XXX.getLimitNum();

        int nowConcurrent = atomic.incrementAndGet();

    try {        

        // 限流

        if(nowConcurrent > limitNum) {

            // 異常處理

        }  

        // 正常業務邏輯

    } catch(Exception e) {

        // 異常處理

    } finally {

        atomic.decrementAndGet();

    }

}

 

q.lua中促銷級別的限流,主要利用C_A_1中c的搶次數和閾值比對。好比促銷A,60秒內只能搶60000次,超過閾值60000該促銷就會搶購失敗。

 

到此搶購系統的核心邏輯就介紹完了,這裏邊還有一些細節問題須要你們在設計時思考,如限購(如每一個人限購2個)、真實庫存不足取消、用戶取消訂單歸還資格、Redis掛了恢復數據、停促銷(時間過時停、庫存不足停)等等。

相關文章
相關標籤/搜索