新客領取優惠券邏輯
判斷是否新客
若為新客 查詢資料庫是否已領取
未領過 發給使用者券
同一用戶並發領券的情況下 會有超領的情況 因為並發進來時查詢資料庫均沒有領取記錄
而又不能針對用戶和優惠券做唯一索引 因為有些券允許用戶領取多張。
於是決定採用redis計數來防同一用戶並發超領 新客領券邏輯改成如下
判斷是否新客
若為新客 基於使用者ID計數 incr {user_id}_receive_count
如果計數大於1 表示同一用戶並發領券 直接回傳
如果計數==1 表示第一次領取
查詢資料庫是否已領取
未領過 發給使用者券
上述邏輯沒有問題 但糾結該如何設定計數key的過期時間
設定過期時間為1分鐘
基本上大部分並發操作都是在同一秒 過期時間1分鐘能滿足要求了
設定過期時間為1小時
萬一資料庫負載高呢並不能在一分鐘內完成一次領券事務操作如雙十一這種場景還是設的長一點更保險一點不管怎樣1小時足夠完成一次領券事務操作了吧但時間設的長了會不會因為其他原因(如網絡超時)導致領券失敗回滾用戶要等一小時後才能重新領券
有沒其他防並發領取方案呢? 可以不用糾結這種過期時間的設定。
PHP中文网2017-04-27 09:04:40
建議將防併發超領與判斷是否新客 分開判斷
防並發超領可以使用以下(上鎖), 每次操作完成記得刪除key(釋放鎖)
// 操作的原子性,如该key在有效时间30秒被设置过返回0,一般请求超时为30秒
$redis->set($key, 1, array("NX", "EX"=>'30'));
判斷是否是新客(用戶是否領取過,有效時間設定到活動結束時間),可以設定一個新的KEY作用戶的領取狀態,領取過直接返回給用戶已經領取過優惠(追求並發性能時,不連接資料庫查詢)
獲取優惠卷(提前把優惠卷塞入隊列,從隊列裡獲取,不從數據庫獲取,此處的好處是,隊列數據取完就沒,不會出現超領取情況),寫入redis隊列
後台進程消費隊列,我用鍊錶阻塞類型進行消費(將領取資訊存放入庫,入庫前對用戶是否領取過優惠卷進行查詢判斷)
黄舟2017-04-27 09:04:40
具體的方案還是需要根據業務來實施
如果業務沒有要求100%,每個人只能領一張券,並且你的Redis集群是高可用的.那麼用Redis
來擋就足夠了,具體Key的失效時間,等活動下載再整體失效,因為這是一個不落庫的方案,所以如果需要判斷是否重複請求需要依賴Redis裡面的數據(萬一Redis掛了呢,雖然Redis有比價不完善的持久化功能,但是丟的數據量比較少,如果如果業務上允許,還是可以接受的),但是很多場景業務都需要要求落庫
業務上要求,必須一人一券(可能券的面值很大),那麼前面還是用題主的Redis
方案,在判斷完是第一次以後,直接插/更新數據庫(這裡就會有異步的方案,但是異步的方案也會出現,異步的方案也會出現與1類似的問題:丟數據),按照這個方案,可以保證每個人只能領一張券(由數據庫來保證數據的一致性) 。如題主所說,其實你不需要把Redis裡面的東西失效掉,因為如果一個用戶重複惡意請求抽券,直接走Redis來擋,不用去數據庫查詢(這裡還會一個問題,該方案是先更新緩存再更新資料庫,所以需要考慮快取更新成功,但是資料庫更新失敗的異常情況,如果資料庫異常需要清理該Key對應的快取)
如果實在扛不住(硬體跟不上),可以用隊列來解決,用隊列來削峰
@大嗚 的方案也是可以行的,但是需要考慮某一時刻Redis
這種異常情況
淡淡烟草味2017-04-27 09:04:40
這種事情你敢用nosql資料庫我也是醉了。
建議先學學資料庫的理論,重點看看事務概念、事務的幾種隔離等級、行鎖表鎖,然後用傳統關係型資料庫來做這個事情。
如果一台資料庫伺服器的效能不夠,那就設計成水平分錶架構,不同的伺服器,每台伺服器承擔一部分使用者資料。水平分錶的理論來源是本科層級的資料結構的hash章節,你可以補習一下。