【Redis核心知识】实现秒杀的三种方案

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

posted @ 2024-04-03 11:05  走向大牛的路上  阅读(422)  评论(0编辑  收藏  举报