4次優化,我的 Redis 差點飛起來

我們有個這樣的需求:每天每一個搶購商品只能買一次,並且全場搶購商品總購買次數不允許超過5次。

那麼,整個商品限購的流程大概如下圖所示:

4次優化,我的 Redis 差點飛起來

那麼,在每次購買成功商品成功後,發送的MQ大概是這樣的(假設當前這筆訂單有兩件搶購商品):

<code>這條消息表示860000000000001這個用戶在1581001673012這個時間點(北京時間為2020/02/06 23:07:53)在A045這個商戶分別購買了商品ID為599055114591和599055114592兩樣商品。

那麼,當消費這條信息後,更新頻控的幾條關鍵Redis命令如下(上面的需求不是重點,優化下面5條命令才是本文的重點):[{
"orderId": "2020020622000001",
"orderTime": "1581001673012",
"productId": "599055114591",
"userId": "860000000000001",
"merchantCode": "A045"
}, {
"orderId": "2020020622000001",
"orderTime": "1581001673012",
"productId": "599055114592",
"userId": "860000000這條消息表示860000000000001這個用戶在1581001673012這個時間點(北京時間為2020/02/06 23:07:53)在A045這個商戶分別購買了商品ID為599055114591和599055114592兩樣商品。

那麼,當消費這條信息後,更新頻控的幾條關鍵Redis命令如下(上面的需求不是重點,優化下面5條命令才是本文的重點):0001",
"merchantCode": "A045"
}]
/<code>

這條消息表示860000000000001這個用戶在1581001673012這個時間點(北京時間為2020/02/06 23:07:53)在A045這個商戶分別購買了商品ID為599055114591和599055114592兩樣商品。

那麼,當消費這條信息後,更新頻控的幾條關鍵Redis命令如下(上面的需求不是重點,優化下面5條命令才是本文的重點):

<code>命令1:hset mall:sale:freq:ctrl:860000000000001 599055114591 1(hash結構,field表示購買的商品ID,value表示購買次數)
命令2:hset mall:sale:freq:ctrl:860000000000001 599055114592 2
命令3:expire mall:sale:freq:ctrl:860000000000001 3127(設置過期時間)
命令4:set mall:total:freq:ctrl:860000000000001 3
命令5:expire mall:total:freq:ctrl:860000000000001 3127(設置過期時間)/<code>

我們首先了解一下執行一條Redis命令耗時由哪幾部分組成:

發送命令網絡傳輸時間,命令在Redis服務端隊列中等待的時間,命令執行的時間(Redis中的slowlog只是檢測這一步驟的時間),結果返回的Redis客戶端的時間。

如下圖所示:

4次優化,我的 Redis 差點飛起來

上面的業務總計涉及5條Redis命令,每條命令都需要經過這些步驟,可想而知性能真的弱爆了(可能整個執行過程還不需要10ms,但還是弱爆了)。

  • 第1次優化

第一次優化非常簡單,稍微有點經驗就能看出來,利用hmset命令將兩條hmset命令合二為一,優化後的Redis命令如下:

<code>hmset mall:sale:freq:ctrl:860000000000001 599055114591 1 599055114592 2
expire mall:sale:freq:ctrl:860000000000001 3127
set mall:total:freq:ctrl:860000000000001 3
expire mall:total:freq:ctrl:860000000000001 3127/<code>
  • 第2次優化
  • 第二次優化將set和expire命令合二為一,這個一般對Redis有點了解的也知道如何優化:

    <code>hmset mall:sale:freq:ctrl:860000000000001 599055114591 1 599055114592 2
    expire mall:sale:freq:ctrl:860000000000001 3127
    setex mall:total:freq:ctrl:860000000000001 3127 3/<code>
  • 第3次優化
  • 第3次優化需要藉助pipeline,簡直就是Redis優化的一大殺器。

    不過,需要注意的是在RedisCluster中使用pipeline時必須滿足pipeline打包的所有命令key在RedisCluster的同一個slot上

    如果打包命令的key不在同一個slot上,就會報錯。所以我們需要分兩批打包:

    <code>-- 這兩條命令的key都是一樣的,肯定在同一個slot上
    pipeline(
    hmset mall:sale:freq:ctrl:860000000000001 599055114591 1 599055114592 2
    expire mall:sale:freq:ctrl:860000000000001 3127
    )
    -- mall:total:freq:ctrl:860000000000001和mall:sale:freq:ctrl:860000000000001兩條命令不在同一個slot上,所以需要單獨執行下面這條命令
    setex mall:total:freq:ctrl:860000000000001 3127 3/<code>

    經過第3次的優化後,這些命令還是需要2次網絡交互。較勁的我還是不甘心,想要將其優化到只需要一次網絡交互即可,有沒有辦法?

    當然有!

    • 第4次優化

    這次優化利用了一個高級特性:hashtag

    是啥子意思呢?我們知道,RedisCluster總計有16*1024=16384個slot。那麼執行一條Redis命令時,其key對應的是哪個slot呢?是利用這樣一個計算公式得到的:

    slot = CRC16(key)%16384

    示意圖如下:

    4次優化,我的 Redis 差點飛起來

    也就是說,默認情況下,key在哪個slot上,與key有關。那麼,我們能否只讓key在哪個slot上與部分key有關呢?

    當然可以,這就是hashtag特性。用法非常簡單,假設一個key是mall:sale:freq:ctrl:860000000000001,我們只需要用{}將key中我們需要的那部分包括起來即可。

    例如,我們只想讓其根據用戶IMEI計算即可,那麼key是這樣的:mall:sale:freq:ctrl:{860000000000001}。只要key中有{860000000000001}這一部分,就一定落在同一個slot上。

    所以,第四次優化以後的命令執行如下所示:

    <code>pipeline(
    hmset mall:sale:freq:ctrl:${860000000000001} 599055114591 1 599055114592 2
    expire mall:sale:freq:ctrl:${860000000000001} 3127
    setex mall:total:freq:ctrl:${860000000000001} 3127 3
    )/<code>


    4次優化,我的 Redis 差點飛起來

    優化後,5條Redis命令壓縮到3條Redis命令,並且3條Redis命令只需要發送一次,並且結果也一次就能全部返回。簡直

    完美!

    • 注意事項

    我們在使用hashtag特性時,一定要注意,不能把key的離散性變得非常差

    以本文為例,沒有利用hashtag特性之前,key是這樣的:mall:sale:freq:ctrl:860000000000001,很明顯這種key由於與用戶相關,所以離散性非常好。

    而使用hashtag以後,key是這樣的:mall:sale:freq:ctrl:{860000000000001},這種key還是與用戶相關,所以離散性依然非常好。

    我們千萬不要這樣來使用hashtag特性,例如將key設置為:mall:{sale:freq:ctrl}:860000000000001。

    這樣的話,無論有多少個用戶多少個key,其{}中的內容完全一樣都是sale:freq:ctrl,也就是說,所有的key都會落在同一個slot上,導致整個Redis集群出現嚴重的傾斜問題。


    分享到:


    相關文章: