基於Redis實現基本搶紅包算法

簡介:

搶紅包是我們生活常用的社交功能, 這個功能最主要的特點就是用戶的併發請求高, 在系統設計上, 可以使用非常多的辦法來扛住用戶的高併發請求, 在本文中簡要介紹使用Redis緩存中間件來實現搶紅包算法, Redis是一個在內存中基於[key, value]的緩存數據庫, Redis官方性能描述非常高, 所以面對高併發場景, 使用Redis來克服高併發壓力是一個不錯的手段, 本文主要基於Redis來實現基本的搶紅包系統設計.

發紅包模塊:

1:發紅包模塊流程圖如下:



 

 

用戶首先輸入紅包金額和紅包個數, 然後生成當前紅包唯一標識, 並使用二倍均值算法生成隨機金額的紅包, 然後將生成的紅包存入緩存Redis數據庫中, Redis數據庫中會保存當前剩餘的紅包數量和每個紅包的金額, 由於Redis數據庫是作爲臨時存儲的地方, 所以發紅包記錄需要持久化存儲在數據庫中, 這裏爲加快系統響應, 使用異步的方式, 將紅包金額紀錄存儲入Mysql數據庫中, 以上就是發紅包模塊的簡要系統設計.

2:隨機生成紅包金額

對於搶紅包來說, 生成紅包金額是非常關鍵的, 這裏有許多生成隨機數方法, 在本文中介紹一種使用較多的二倍均值算法來隨機生成紅包金額.對於搶紅包來說, 如果發送一個金額爲J的紅包, 那麼對與搶紅包的N個人來說, 公平的概率是: 每個人搶到J / N 的金額的概率是相同的, 例如100元紅包發給10個人,那麼最公平的策略是使每個人搶到10元的概率相同, 二倍均值算法就是基於上面這個概率策略. 二倍均值算法流程如下: 首先設置紅包金額爲J, 搶紅包人數爲N, 接下來計算隨機數區間上U = J / N * 2, 得到隨機數區間(0,U), 從而在這個區間裏生成第一個隨機數金額M, 接下來繼續生成第二個隨機金額. 首先更新總紅包金額爲J-M,總搶紅包人數爲N-1, 然後生成第二個隨機金額區間(0, (J-M) / (N-1) *2) , 從這個區間裏面生成第二個隨機金額M2, 繼續迭代, 直到生成最後一個紅包金額, 下圖是二倍均值算法的流程





 

 

二倍均值算法案例: 紅包總金額100元, 總計10個人

計算第一個隨機金額區間: 100/10X2 = 20, 第一個隨機金額的區間是(0,20 ),區間均值爲10

假設第一個人搶到10元,剩餘金額是90 元

計算第二個隨機金額區間: 90/9X2 = 20, 第一個隨機金額的區間是(0,20 )區間均值爲10

假設第二個人搶到10元,剩餘金額是80 元 計算第三個隨機金額區間: 80/8X2 = 20, 第一個隨機金額的區間是(0,20 ),區間均值爲10

...............

所以使用二倍均值算法能夠在不論誰先搶的情況下, 都能公平保證每個人搶到平均金額的概率是相等的, 二倍均值算法生成紅包金額的代碼如下:

//這裏輸入的totalMoney單位是分,例如100元,totalMoney = 10000
public List<Integer> getRedPackage(Integer totalMoney,Integer totalPeopleCount) {
    List<Integer> moneyList = new ArrayList<>();
    //暫存剩餘金額爲紅包的總金額
    Integer restMoney = totalMoney;
    //暫存剩餘的總人數-初始化時即爲指定的總人數
    Integer restPeopleCount = totalPeopleCount;    
    //隨機數對象
    Random random = new Random();
    //開始循環迭代生成紅包
    for (int i =0;i< totalPeopleNum-1;i++){
       //加1是爲了至少搶到1分錢
       int money = random.nextInt (restMoney / restPeopleCount * 2) + 1;
       restMoney -= money;
       restPeopleCount--;
       moneyList.add(money);
    }
    //添加最後的一個紅包金額
    amountList.add(restAmount);
    return amountList;
}

3: 紅包存儲

爲了應對用戶高併發的請求, 也就是需要頻繁讀取紅包金額和數量, 所以將紅包金額和數量存儲在Mysql中是不行的, 所以只能藉助基於內存的Redis數據庫來支持高併發的讀取操作.Redis中有5種基本的數據結構分別是:String, List, Set, Sorted Set, Map這五種, 紅包金額數量是一個List集合, 所以使用List來存儲最爲合適,在發紅包時, 我們先用二倍均值算法隨機生成一定數量的紅包金額, 然後將紅包金額和紅包數量存入Redis緩存中,等待用戶搶紅包

//隨機生成全局唯一的紅包id
redId = getRedId();
//首先生成紅包金額
List<Integer> moneyList = getRedPackage(totalMoney,totalPeopleCount);
//放入redis
redisClient.lpush(redId, moneyList);
//redis中記錄紅包個數
redisClient.set(redId, moneyList.size());
//異步存儲發紅包記錄到Mysql數據庫
//將紅包id返回
return redId;

搶紅包模塊:

1:搶紅包模塊流程圖如下:



 

 

首先判斷用戶是否已經搶過紅包了, 是否還有剩餘的紅包, 如果搶過或者剩餘紅包數量小於等於0, 則代表紅包已經被搶完了, 直接結束用戶本次搶紅包流程. 如果還有剩餘的紅包數量, 則從Redis緩存列表中彈出一個紅包金額, 然後將剩餘紅包數量減1, 同時異步將用戶搶紅包記錄存入Mysql數據庫, 最後將搶到的紅包金額返回給用戶, 結束本次搶紅包流程

2:首先判斷是否已經搶過紅包

通過在Redis中以用戶ID構建一個唯一Key來判斷是否搶過紅包, Key的構建規則是:業務前綴+紅包id+用戶id

redMoney = redisClient.get("rob" + redId + useId)
//如果不爲空,則說明已經搶過了,直接返回搶過的紅包金額
if (redMoney != null) {
    return redMoney
}

3:判斷是否還有紅包

通過在Redis中以紅包id記錄一個數量來判斷是否還有紅包, key的構建規則是:業務前綴+紅包id

totalNum = redisClient.get("totalNum" + redId)
//如果爲空或者小於等於0則代表沒有了
if (totalNum == null || totalNum <= 0) {
    return null
}

4:彈出一個紅包金額

因爲我們是把紅包金額存儲到Redis的List列表中的, 所以直接使用列表的Pop操作就行了

money = redisClient.rpop(redId)
//如果不爲空,則說明搶到了
if (money != null) {
    ....
    紅包個數減1
    存儲搶紅包記錄
    設置該用戶已經搶過紅包
    ....
    //返回搶到的金額
    return money
}
//沒搶到
return null

5:減少紅包個數

紅包總數是以一個[key, value] 鍵值對存儲在Redis中的, 所以這裏使用Redis的DECR命令就行了

money = redisClient.rpop(redId)
//如果不爲空,則說明搶到了
if (money != null) {
    //紅包個數減1
    redisClient.decr(redId)
    ....
    存儲搶紅包記錄
    設置該用戶已經搶過紅包
    ....
    //返回搶到的金額
    return money
}
//沒搶到
return null

6:異步記錄搶紅包記錄

採用異步的方式將記錄存入Mysql數據庫, 異步的方式可以採用消息隊列或者多線程的方式來實現

money = redisClient.rpop(redId)
//如果不爲空,則說明搶到了
if (money != null) {
    //紅包個數減1
    redisClient.decr(redId)
    //異步存儲搶紅包記錄
    這裏可以使用mq或者多線程的方式來實現
    ....
    設置該用戶已經搶過紅包
    ....
    //返回搶到的金額
    return money
}
//沒搶到
return null

7:設置該用戶已經搶過紅包

money = redisClient.rpop(redId)
//如果不爲空,則說明搶到了
if (money != null) {
    //紅包個數減1
    redisClient.decr(redId)
    //異步存儲搶紅包記錄
    這裏可以使用mq或者多線程的方式來實現
    //設置該用戶已經搶過紅包
    redisClient.set("rob" + redId + useId, money)
    //返回搶到的金額
    return money
}
//沒搶到
return null

8: 整體的僞代碼邏輯如下:

redMoney = redisClient.get("rob" + redId + useId)
//如果不爲空,則說明已經搶過了,直接返回搶過的紅包金額
if (redMoney != null) {
    return redMoney
}
totalNum = redisClient.get("totalNum" + redId)
//如果紅包總數小於0, 則代表已經搶完了, 直接返回空
if (totalNum == null || totalNum <= 0) {
    return null
}
money = redisClient.rpop(redId)
//如果不爲空,則說明搶到了
if (money != null) {
    //紅包個數減1
    redisClient.decr(redId)
    //異步存儲搶紅包記錄
    這裏可以使用mq或者多線程的方式來實現
    //設置該用戶已經搶過紅包
    redisClient.set("rob" + redId + useId, money)
    //返回搶到的金額
    return money
} 
//沒搶到
return null


9:分佈式鎖

這裏涉及到了同一個用戶多次高併發來搶紅包的情況, 並且代碼邏輯中包含了下面這種邏輯: 判斷條件成立然後進行業務操作,最後設置條件. 這種業務邏輯如果不防止併發的話, 就會產生重複操作, 所以需要使用鎖來限制每一個用的訪問頻率, 加鎖的方式是使用分佈式鎖, 這是因爲我們搶紅包服務不可能只在一臺服務器上部署, 同時基於Redis也能很容易的實現分佈式鎖, 使用Redis命令setNx命令就可以實現簡單分佈式鎖

redMoney = redisClient.get("rob" + redId + useId)
//如果不爲空,則說明已經搶過了,直接返回搶過的紅包金額
if (redMoney != null) {
    return redMoney
}
totalNum = redisClient.get("totalNum" + redId)
//如果紅包總數小於0, 則代表已經搶完了, 直接返回空
if (totalNum == null || totalNum <= 0) {
    return null
}
//加分佈式鎖
lockResut = redisClient.setNx(useId,redId,timeOut);
//加鎖失敗,直接返回
if(!lockResult){
    return;
}
try{
    money = redisClient.rpop(redId)
    //如果不爲空,則說明搶到了
    if (money != null) {
        //紅包個數減1
        redisClient.decr(redId)
        //異步存儲搶紅包記錄
        這裏可以使用mq或者多線程的方式來實現
        //設置該用戶已經搶過紅包
        redisClient.set("rob" + redId + useId, money)
        //返回搶到的金額
        return money
    }     
} finally {
    //刪除鎖
    redisClient.del(useId)
}
//沒搶到
return null

總結

以上就是完整的搶紅包僞代碼流程, 可以基本實現發紅包以及搶紅包功能, 該方法基於Redis來實現紅包的存儲和搶紅包的操作, 基於二倍均值算法來實現紅包金額的隨即生成, 在整體功能上還有很多不完善的地方, 可以基於整體框架進行擴展開發, 實現更加完整的算法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章