1. 程式人生 > >C#-搶紅包功能的分散式情況下處理多併發

C#-搶紅包功能的分散式情況下處理多併發

需求

需求經理設計了一個分享出去後,可以在微信群中搶優惠的活動。 簡單來說,就是每個參與活動的商品可以生成一個紅包池,分享到群裡後,可以像搶紅包一樣,去搶優惠金額。

問題

介面很快就根據需求設計開發出來了,並完善了相關活動規則。

  1. 但是多併發情況下,分享出去的紅包池的,可搶次數與可搶金額,控制的並不嚴格,會出現多搶幾次或多搶金額的情況。
  2. 並且希望噹噹前使用者因某些原因搶不到紅包時,能夠快速響應,不必走完全部程式碼。
  3. 服務使用的是分散式架構,單純的Lock無法鎖住資源。
  4. Redis暫未開放Lua指令碼,無法開發帶有邏輯的原子性操作。

思路

阻塞鎖 非阻塞鎖 redis快取

方案

  • 在商品詳情頁參與活動,生成紅包池。生成紅包池後,先將紅包池資訊寫入redis,再返回給前端。

為了合理利用快取空間,快取失效時間設定為當前時間到活動結束時間,並加一天。加一天是為了防止邊緣時間出現問題。

int dateDiffDays = checkBeforeCreateRedPacke.EndTime.Subtract(DateTime.Now).Days + 1; //當前時間到活動結束時間的天數+1
CacheProvider.SetEntry(string.Format(CacheKey.RedPackSurplusNum, entity.ID), (checkBeforeCreateRedPacke.RedPacketNum + 1).ToString(), TimeSpan.FromDays(dateDiffDays)); //快取紅包池剩餘可搶次數
CacheProvider.SetEntry(string.Format(CacheKey.RedPackSurplusMoney, entity.ID), (checkBeforeCreateRedPacke.GoodPrice).ToString(), TimeSpan.FromDays(dateDiffDays)); //快取紅包池剩餘金額
  • 搶紅包,校驗紅包活動和紅包池的有效性後。第一道阻流。用redis實現的分散式阻塞鎖。保證等待佇列最大長度為一個紅包池的可槍次數。

校驗紅包池剩餘可搶次數。此處同時作為佇列排隊作用。需要在redis中達到查詢並新增一排隊人數的作用。 因為redis還沒有放開外掛,無法開發Lua外掛,所以使用的是redis自帶的自減功能。這也是前面快取剩餘可搶次數和剩餘金額分開的原因,方便進行自減操作。 剩餘次數減1並返回減1後的結果。 若自減後返回結果小於1則為該紅包池已搶完,程式返回結果。 若自減後返回結果大於1則為紅包池可搶,進入佇列。 若自減後返回結果等於1則為剩餘最後一個紅包,直接將剩餘金額作為搶到金額,進入佇列。

PS:redis的自減操作是一個原子性操作,可以解決目前我的排隊問題,但是它在redis查詢無果時,會建立一個初始值為0的資料,減一後返回。這就導致一個問題,當這個自減操作返回-1時,我如何判斷是redis丟失資料導致的,還是正常減1減至-1導致的。任何時候都要對於第三方功能的異常情況的進行考慮

redis自減操作:

//快取控制搶紅包排隊序列
int surplusNum = (int)CacheProvider.DecrementValue(string.Format(CacheKey.RedPackSurplusNum, filter.RPId));
if (surplusNum < 1)
{
//紅包池中的紅包已經被搶完
data.State = 4;
}
else
{
CheckInfo.SurplusNum = surplusNum;
}

進入佇列:

//新增等待資料鎖key
string lockKey = string.Format("ECC:{0}:{1}", "GrabRedPack", filter.RPId);
//加分散式阻塞鎖 無鎖新增鎖,有鎖等待釋放
CacheProvider.AddWaitLock(lockKey);

封裝的自減操作:

/// <summary>
/// 數值自減1
/// </summary>
/// <param name="key">鍵</param>
/// <param name="value">值</param>
/// <param name="expiresIn">超時時間</param>
/// <returns></returns>
public static long DecrementValue(string key)
{
try
{
using (ICacheClient client = cacheService.GetClient())
{
return client.DecrementValue(key);
}
}
catch { return -1; }
}
/// <summary>
/// 數值減值
/// </summary>
/// <param name="key">鍵</param>
/// <param name="value">值</param>
/// <param name="expiresIn">超時時間</param>
/// <returns></returns>
public static long DecrementValueBy(string key, int count)
{
try
{
using (ICacheClient client = cacheService.GetClient())
{
return client.DecrementValueBy(key, count);
}
}
catch { return -1; }
}
/// <summary>
/// 數值加值
/// </summary>
/// <param name="key">鍵</param>
/// <param name="value">值</param>
/// <returns></returns>
public static long IncrementValueBy(string key, int count)
{
try
{
using (ICacheClient client = cacheService.GetClient())
{
return client.IncrementValueBy(key, count);
}
}
catch { return -1; }
}

redis自減基層原碼(解釋為啥catch中返回-1):

//
// 摘要:
// Decrements the number stored at key by one. If the key does not exist, it is
// set to 0 before performing the operation.
//
// 引數:
// key:
long DecrementValue(string key);
//
// 摘要:
// Decrements the number stored at key by decrement. If the key does not exist,
// it is set to 0 before performing the operation.
//
// 引數:
// key:
//
// count:
long DecrementValueBy(string key, int count);
  • 第二道防剩餘金額併發。

佇列中等待程序開始執行。 先重新從redis中獲取最新的剩餘金額。因為上一個程序生成紅包後剩餘金額有變化。 在進行業務邏輯後,生成了紅包後,更新redis的紅包池剩餘金額。 然後可以釋放鎖。剩餘的就是資料入庫操作,不必鎖資源。 當然佇列進行開始到生成紅包金額,全部放在try-cathc語句中執行。

PS:一定要注意程式的閉環。加鎖一定要解鎖。

//鎖開始
try
{
//生成搶紅包金額
//更新剩餘金額
}
catch (Exception)
{}
finally
{
//移除鎖
CacheProvider.RemoveLock(lockKey);
}
//鎖結束
  • 至此,就完成了。

成果

資料沒有出錯,無多發無少發,抗住了併發請求,並通過了壓力測試。

壓測結果

總結

  • 一開始我們使用的C#的Lock()。 ()內需為靜態object,不能為字串。但依舊無法有效鎖住剩餘金額。
  • 在多次併發測試中發現,紅包個數沒有問題,但是有那麼幾次生成紅包的總金額是多於紅包池的設定的,也就是說有的程序取到的剩餘金額是不對的,生成金額時因為是依舊舊金額生成的,導致搶到的金額多了。
  • 是因為生產環境使用的是兩臺伺服器,搭建了兩個host來處理請求。原本目的是希望能夠起到分流,共同承擔壓力的目的。
  • 所以lock()是鎖不住資源的。一直到使用了redis的分散式鎖,才解決問題。