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

京东云开发者發表於2024-04-17

簡介:

搶紅包是我們生活常用的社交功能, 這個功能最主要的特點就是使用者的併發請求高, 在系統設計上, 可以使用非常多的辦法來扛住使用者的高併發請求, 在本文中簡要介紹使用 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 getRedPackage(Integer totalMoney,Integer totalPeopleCount) {
List 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 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 來實現紅包的儲存和搶紅包的操作, 基於二倍均值演算法來實現紅包金額的隨即生成, 在整體功能上還有很多不完善的地方, 可以基於整體框架進行擴充套件開發, 實現更加完整的演算法

相關文章