【Redis核心知識】實現秒殺的三種方案

走向大牛的路上發表於2024-04-03

Redis秒殺方案
  Redis效能很好,被大量使用於秒殺場景下,實現秒殺有以下幾種方案:

方案一:使用商品ID作為分散式鎖,加鎖後扣減庫存
該方案的實現流程為:

  • 使用者發起秒殺請求到RedisRedis先使用商品ID作為key嘗試加鎖,保證只有一個使用者進入之後流程,保證原子性;
  • 如果加鎖成功,則查詢庫存。如果庫存充足,則扣減庫存,代表秒殺成功;若庫存不足,直接返回秒殺失敗;

  實現程式碼如下:

 1 /**
 2  * Redis秒殺方法一:先加分散式鎖,然後查詢快取,根據庫存量數量進行後續操作:如果庫存量大於零,則扣減庫存,返回true;否則返回false;
 3  * @param goodId 商品ID
 4  * @return 成功返回true,失敗返回false
 5  */
 6 @Override
 7 public Boolean secKillByRedisFun1(Integer goodId) {
 8     // 根據商品ID構造key
 9     String goodKey = "good-stock-" + goodId;
10     String userId = Thread.currentThread().getName() + "-" + System.currentTimeMillis();
11     // 使用商品作為鎖,鎖的粒度較大
12     String lockId = "sec-kill-lock-" + goodId;
13     return this.subStock(lockId, userId, goodKey);
14 }
15 
16 /**
17  * 使用分散式鎖秒殺,加鎖後再查詢redis庫存,最後扣減庫存
18  * @param lockId 鎖ID
19  * @param userId 使用者ID
20  * @param goodKey 商品ID
21  * @return 秒殺成功返回 true,否則返回 false
22  */
23 private boolean subStock(String lockId, String userId, String goodKey) {
24     // 嘗試先加鎖,如果加鎖成功再進行查詢庫存量,和扣減庫存操作,此時只能有一個執行緒進入程式碼塊
25     if (redisLock.lock(lockId, userId, 4000)) {
26         try {
27             // 查詢庫存
28             Integer stock = (Integer) redisTemplate.opsForValue().get(goodKey);
29             if (stock == null) {//商品不在快取中
30                 return false;31             }
32             // 如果剩餘庫存量大於零,則扣減庫存
33             if (stock > 0) {
34                 redisTemplate.opsForValue().decrement(goodKey);
35                 return true;
36             } else {
37                 return false;
38             }
39         } finally {
40             // 釋放鎖
41             redisLock.unlock(lockId, userId);
42         }
43     }
44     return false;
45 }

  該方案存在一些缺點

  • 使用者進來後都要搶鎖,即便是庫存量已經為零,仍然需要搶鎖,這無疑帶來了很多無用爭搶;
  • 鎖的是商品ID,鎖粒度太大,併發效能可以進一步最佳化;

  解決方案:

  • 搶鎖前先查詢庫存,如果庫存已經為零,則直接返回false,不必參與搶鎖過程;
  • 使用商品ID+庫存量作為鎖,降低鎖粒度,進一步提升併發效能;

方案二:使用商品ID+庫存量作為分散式鎖,加鎖後扣減庫存
該方案的實現流程為:

  • 使用者發起秒殺請求到RedisRedis先查詢庫存量,然後根據商品ID+庫存量作為key嘗試加鎖,保證只有一個使用者進入之後流程,保證原子性
  • 如果加鎖成功,則查詢庫存。如果庫存充足,則扣減庫存,代表秒殺成功;若庫存不足,直接返回秒殺失敗;

  注意:第一步查詢庫存量後,可以新增判斷庫存是否為零的操作,這樣就能過濾掉庫存為零後的大量請求。

  實現程式碼如下:

 1 @Override
 2 public Boolean secKillByRedisFun2(Integer goodId) {
 3     // 根據商品ID構造key
 4     String goodKey = "good-stock-" + goodId;
 5     // 查詢庫存,使用庫存量作為鎖,細化鎖粒度,提高併發量
 6     Integer curStock = (Integer) redisTemplate.opsForValue().get(goodKey);
 7     if (curStock <= 0) {
 8         return false;
 9     }
10     String userId = Thread.currentThread().getName() + "-" + System.currentTimeMillis();
11     String lockId = "sec-kill-lock-" + goodId + "-" + curStock;
12     return this.subStock(lockId, userId, goodKey);
13 }
14 
15 /**
16  * 使用分散式鎖秒殺,加鎖後再查詢redis庫存,最後扣減庫存
17  * @param lockId 鎖ID
18  * @param userId 使用者ID
19  * @param goodKey 商品ID
20  * @return 秒殺成功返回 true,否則返回 false
21  */
22 private boolean subStock(String lockId, String userId, String goodKey) {
23     // 嘗試先加鎖,如果加鎖成功再進行查詢庫存量,和扣減庫存操作,此時只能有一個執行緒進入程式碼塊
24     if (redisLock.lock(lockId, userId, 4000)) {
25         try { // 查詢庫存
27             Integer stock = (Integer) redisTemplate.opsForValue().get(goodKey);
28             if (stock == null) {//商品不在快取中
29                 return false;
} 31 // 如果剩餘庫存量大於零,則扣減庫存 32 if (stock > 0) { 33 redisTemplate.opsForValue().decrement(goodKey); 34 return true; 35 } else { 36 return false; 37 } 38 } finally { 39 // 釋放鎖 40 redisLock.unlock(lockId, userId); 41 } 42 } 43 return false; 44 }

以上兩種先加鎖再查詢庫存量扣減庫存的方案,是為了保證查詢庫存扣減庫存操作的原子性,也可以使用lua指令碼實現這兩個操作的原子性,這樣就不需要額外維護分散式鎖的開銷。

方案三:使用INCRDECR原子操作扣減庫存

該方案直接使用DECR操作扣減庫存,不需要提前查詢快取,程式碼簡潔:

  • 如果返回值大於零,說明庫存充足,表示秒殺成功;
  • 如果返回值小於零,說明庫存不足,需要使用INCR操作恢復庫存,秒殺失敗;

實現程式碼如下:

 1 /**
 2  * Redis 提前快取資料庫庫存
 3  * @param goodkey 商品ID 
 4  * @param stockCount 商品庫存量
 5 */
 6 public Boolean setRedisRepertory(String goodKey,Long stockCount){ 
 7  redisTemplate.opsForValue().set(goodKey,stockCount);   
 8   return Boolean.TRUE;
 9 }
10 /**
11  * Redis 秒殺方案三:使用原子操作DECR和INCR扣減庫存
12  * @param goodId 商品ID
13  * @return
14  */
15 @Override
16 public Boolean secKillByRedisFun3(String goodId) {
17     // 根據商品ID構造key
18     String goodKey = "good-stock-" + goodId;
19     Long stockCount = redisTemplate.opsForValue().decrement(goodKey);
20     if (stockCount >= 0) {
21         return true;
22     } else {
23         // 如果庫存不夠,則恢復庫存
24         redisTemplate.opsForValue().increment(goodKey);
25         return false;
26     }
27 }

不足:後期庫存為零後,大量請求扣減庫存後需要恢復庫存,這是一個無用操作。

解決方案:可以提前查詢庫存,如果庫存為零,直接返回false

原文地址:https://blog.csdn.net/qq_43705697/article/details/133685596

相關文章