一. 單品限流
1. 含義
某件商品n秒内隻接受m個請求, 比如:限制商品A在2s内隻接受500個下單請求。
2.設計思路
利用Redis自增的Api,該商品的第一個請求進來的時候設定緩存過期時間,限制内正常走業務,限制外傳回限流提示;時間到了,原緩存内容消失,下一次第一個請求進來重新設定過期時間
3.分析
單品限流屬于商品層次的限流,後面會有Nginx全局限流
4.壓測結果
要求:1秒内該商品隻能接收100個下單請求。
代碼分享:

/// <summary>
/// 05-單品限流
/// </summary>
/// <param name="userId">使用者編号</param>
/// <param name="arcId">商品編号</param>
/// <param name="totalPrice">訂單總額</param>
/// <param name="goodNum">使用者購買的商品數量</param>
/// <returns></returns>
public string POrder5(string userId, string arcId, string totalPrice, int goodNum = 1)
{
try
{
//一. 業務完善優化
//1. 單品限流
{
int tLimits = 100; //限制請求數量
int tSeconds = 1; //限制秒數
string limitKey = $"LimitRequest{arcId}";//受限商品ID
long myLimitCount = _redisDb.StringIncrement(limitKey, 1); //key不存在則會自動建立,第一次建立傳回值為1
if (myLimitCount > tLimits)
{
throw new Exception($"不能購買了,{tSeconds}秒内隻能請求{tLimits}次");
//return $"不能購買了,{tSeconds}秒内隻能請求{tLimits}次";
}
else if (myLimitCount == 1)
{
//設定過期時間
_redisDb.KeyExpire(limitKey, TimeSpan.FromSeconds(tSeconds));
}
}
#endregion
//二. 邏輯優化
//1. 直接自減1
int iCount = (int)_redisDb.StringDecrement($"{arcId}-sCount", 1);
if (iCount >= 0)
{
//2. 将下單資訊存到消息隊列中
var orderNum = Guid.NewGuid().ToString("N");
_redisDb.ListLeftPush(arcId, $"{userId}-{arcId}-{totalPrice}-{orderNum}");
//3. 把部分訂單資訊傳回給前端
return $"下單成功,訂單資訊為:userId={userId},arcId={arcId},orderNum={orderNum}";
}
else
{
//賣完了
return "賣完了";
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
View Code
測試:1s内對商品發送500個請求,異常率80%,說明指接收了100個請求,同時庫存扣減和訂單建立也正确。
二. 購買商品限制
每位使用者在秒殺期間對某商品隻能購買m件.
PS:哪件商品限制購買多少件依靠DB設計,事先錄好,不同商品的限制數量不同。
2. 設計思路
A. 同樣是利用Redis自增API, 1個使用者對應1件商品 存一條記錄
B. 也要設定一下過期時間,設計一個合理的數值,秒殺結束後,資料失效消失即可
C. 配合前端購買框内的設計限制
3. 分析
購買商品限制可以防止黃牛大量囤貨
4. 壓測結果
要求:1件商品一個使用者隻能購買3件。

/// <summary>
/// 06-限制購買數量
/// </summary>
/// <param name="userId">使用者編号</param>
/// <param name="arcId">商品編号</param>
/// <param name="totalPrice">訂單總額</param>
/// <param name="goodNum">使用者購買的商品數量</param>
/// <returns></returns>
public string POrder6(string userId, string arcId, string totalPrice, int goodNum = 1)
{
try
{
//一. 業務完善優化
//1. 單品限流
#region 2. 限制使用者購買數量
{
//表示使用者商品可以購買的數量
//(秒殺商品表中有個limitNum字段,同步到redis中,這裡從redis中讀取這個限制),這裡臨時先寫死
int tGoodBuyLimits = 3; //這裡先臨時寫死
string userBuyGoodLimitKey = $"userBuyGoodLimitKey-{userId}-{arcId}";
long myGoodLimitCount = _redisDb.StringIncrement(userBuyGoodLimitKey, goodNum);
if (myGoodLimitCount > tGoodBuyLimits)
{
throw new Exception($"不能購買了,一個使用者隻能買{tGoodBuyLimits}件");
}
else
{
//這裡設定10min,表示10min後秒殺結束,使用者可以繼續購買了,這個緩存消失 (這裡緩存是否覆寫影響不大)
_redisDb.KeyExpire(userBuyGoodLimitKey, TimeSpan.FromMinutes(10));
}
}
#endregion
//二. 邏輯優化
//1. 直接自減1
int iCount = (int)_redisDb.StringDecrement($"{arcId}-sCount", 1);
if (iCount >= 0)
{
//2. 将下單資訊存到消息隊列中
var orderNum = Guid.NewGuid().ToString("N");
_redisDb.ListLeftPush(arcId, $"{userId}-{arcId}-{totalPrice}-{orderNum}");
//3. 把部分訂單資訊傳回給前端
return $"下單成功,訂單資訊為:userId={userId},arcId={arcId},orderNum={orderNum}";
}
else
{
//賣完了
return "賣完了";
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
測試:模拟同一個使用者發送100個請求,異常率為97%,說明該使用者隻能搶3件
三. 方法幂等
使用者在下單頁面,假設網絡延遲多次點選按鈕,服務端僅處理第一次請求(第一次成功則成功,失敗則失敗),退出該頁面重新進入,又可以重新點選下單了
A.前端生成一個requestId,規則:時間戳+arcId,存放到SessionStorage中。
B.後端存到redis中string中,也是利用自增api,判斷值是否大于1,但要設定一個過期時間,否則就一直在redis中了。
C.前端頁面:點選變灰,拿到傳回結果後 或者 5s後才可以繼續點選。
PS:前端的頁面業務和效果在後續業務中完善,這裡單純優化接口!!!
方法幂等是防錯的一種措施,防止網絡延遲或使用者誤操作多次下單出錯的問題
要求:1個requestId隻能生成一條訂單記錄

/// <summary>
///07-方法幂等
/// </summary>
/// <param name="userId">使用者編号</param>
/// <param name="arcId">商品編号</param>
/// <param name="totalPrice">訂單總額</param>
/// <param name="requestId">請求ID</param>
/// <param name="goodNum">使用者購買的商品數量</param>
/// <returns></returns>
public string POrder7(string userId, string arcId, string totalPrice, string requestId = "125643", int goodNum = 1)
{
try
{
//一. 業務完善優化
//1. 單品限流-同上
//2. 限制使用者購買數量-同上
//3. 方法幂等-防止網絡延遲多次送出問題
//(也可以考慮存hash,把訂單号也存進去,回頭改造, 但是HashIncrement沒法把value也存進去)
var orderNum = Guid.NewGuid().ToString("N");
int requestIdNum = (int)_redisDb.StringIncrement(requestId, 1);
if (requestIdNum == 1)
{
//僅第一次進來的時候設定過期時間,用于定期删除
_redisDb.KeyExpire(requestId, TimeSpan.FromMinutes(10));
}
else if (requestIdNum > 1)
{
throw new Exception($"您已經下過單了,不能重複下單");
}
else
{
throw new Exception($"其它異常。。。。");
}
//二. 邏輯優化
//1. 直接自減1
int iCount = (int)_redisDb.StringDecrement($"{arcId}-sCount", 1);
if (iCount >= 0)
{
//2. 将下單資訊存到消息隊列中
_redisDb.ListLeftPush(arcId, $"{userId}-{arcId}-{totalPrice}-{orderNum}");
//3. 把部分訂單資訊傳回給前端
return $"下單成功,訂單資訊為:userId={userId},arcId={arcId},orderNum={orderNum}";
}
else
{
//賣完了
return "賣完了";
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
測試:模拟同一個使用者發送100個請求,異常率為99%,說明該使用者隻生成了一條訂單記錄
!
- 作 者 : Yaopengfei(姚鵬飛)
- 部落格位址 : http://www.cnblogs.com/yaopengfei/
- 聲 明1 : 如有錯誤,歡迎讨論,請勿謾罵^_^。
- 聲 明2 : 原創部落格請在轉載時保留原文連結或在文章開頭加上本人部落格位址,否則保留追究法律責任的權利。