【Redis核心知识】实现秒杀的三种方案
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