Redis秒殺方案
Redis效能很好,被大量使用於秒殺場景下,實現秒殺有以下幾種方案:
方案一:使用商品ID作為分散式鎖,加鎖後扣減庫存
該方案的實現流程為:
- 使用者發起秒殺請求到Redis,Redis先使用商品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+庫存量作為分散式鎖,加鎖後扣減庫存
該方案的實現流程為:
- 使用者發起秒殺請求到Redis,Redis先查詢庫存量,然後根據商品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
指令碼實現這兩個操作的原子性,這樣就不需要額外維護分散式鎖的開銷。
方案三:使用INCR
和DECR
原子操作扣減庫存
該方案直接使用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