首頁  >  文章  >  資料庫  >  redis怎麼實現秒殺系統

redis怎麼實現秒殺系統

PHPz
PHPz轉載
2023-05-31 15:11:132484瀏覽

一、設計思路

秒殺系統的特點就是並發量大,一秒鐘就可能幾千幾萬的請求進來了,如果不使點兒手段,系統分分鐘就垮了。以下就探討如何設計一個能打的秒殺系統。

1、限流:

首先不考慮業務邏輯,假如有如下一個最簡單的介面:

@GetMapping("/test")
public String test() {
 return "success";
}

儘管這個介面非常簡單,沒有任何邏輯,但如果有成千上萬的請求同時訪問,伺服器也會崩潰。所以,高並發系統該做的第一件事就是限流。 springcloud專案可以使用hystrix進行限流,springcloud alibaba可以使用sentinel進行限流,那麼非springcloud專案呢? guava為我們提供了一個RateLimiter工具類,可以做限流。它主要有漏桶演算法和令牌桶演算法。

  • 漏桶演算法:一個有洞洞的桶子在水龍頭下裝水,裝一點兒就漏一點兒,但是如果水龍頭的水很大,桶裡的水遲早會溢出的,溢出就限流。這種適合做限制上傳下載速率一類的。

  • 令牌桶演算法:以恆定的速率放入桶中令牌,每次請求進來,要先從桶中拿令牌,如果沒有拿到令牌,請求就被擋掉。這種適合做限流,即限制QPS。

這裡應該要用令牌桶演算法進行限流,如果沒拿到令牌,直接回傳「人太多了,擠不進去」的提示。

2、檢查用戶是否登錄:

經過第一步的限流,進來的請求應該檢查用戶是否登錄,本項目使用JWT,即先請求登錄接口,登錄後返回token,請求其他所有介面都在請求頭中帶上token,然後透過token就可以拿到使用者資訊。當未取得使用者資訊時,提示使用者重新登入:無效的token。

3、檢查商品是否賣完:

如果前兩步校驗都通過,就需要進行商品是否售罄的檢查,如果售罄了就返回一個提示信息“抱歉,商品已經被秒殺一空」。注意,檢查商品是否賣完不能查資料庫,否則會很慢。可以使用一個字典來儲存商品ID,並用商品ID作為鍵。如果商品售罄,將其值設為True,否則為False。

4、將參加秒殺的商品加到redis中:

先搞個ISINREDIS的key,表示商品是否已經加到redis中了,避免每個請求進來都重複此操作。如果ISINREDIS值為false,表示redis中還沒有秒殺商品。那就查詢出所有參加秒殺的商品,商品id作為key,商品庫存作為value,存到redis中,同時將商品id作為key,false作為value,放到第三步的map中,表示該商品沒有售完。最後將ISINREDIS的值設為true,表示已經將所有參加秒殺的商品加到redis中了。

5、預扣庫存:

使用redis的decr函數對商品數量進行減少,並對減少後的值進行判斷。如果自減後結果小於0,表示商品已經賣完了,那麼就將map中對應的商品id的值設為true,並且傳回「來遲了,商品已秒殺完」的提示。

6、判斷是否重複秒殺:

如果用戶秒殺成功,在秒殺訂單入庫後,會將用戶id和商品id作為key,true作為value存入redis中,表示該用戶已經秒殺過該商品了。所以在這裡就根據用戶id和商品id去redis中判斷是否重複秒殺,如果是,就回傳「請勿重複秒殺」的提示。

7、非同步處理:

如果以上校驗都通過了,那麼就可以處理秒殺了。如果對每一個秒殺請求都進行扣庫存和創建訂單這種操作,那麼不僅速度非常慢,而且可能會導致資料庫崩潰。所以我們可以非同步處理,即通過了以上校驗,就將用戶id和商品id作為message發送到MQ中,然後立即給用戶返回“排隊中”的提示。然後在MQ的消費者端對訊息進行消費,拿到用戶id和商品id,可以根據商品id查詢庫存,再次確保庫存充足;然後也可以再次判斷是否重複秒殺。通過了判斷後,就操作資料庫,扣減庫存,創建秒殺訂單。注意扣減庫存和創建秒殺訂單需要在同一個事務中。

8、超賣問題:

超賣問題就是商品庫存出現負數的情況。例如庫存剩餘1了,然後10個用戶同時秒殺,在判斷庫存的時候都是1,所以10個人都能下單成功,最後庫存為-9。如何解決?其實本系統中根本就不會出現這樣的問題,因為一開始用redis進行了庫存預減,而redis指令核心模組是單線程的,所以可以保證不會超賣。如果沒有用到redis,也可以為該商品增加一個version字段,每次扣減庫存前先查其version,扣減庫存的sql加上一個條件,就是version要等於剛才查出來的version。

 

二、核心代码

@RestController
@RequestMapping("/seckill")
public class SeckillController {
 
 @Autowired
 private UserService userService;
 @Autowired
 private SeckillService seckillService;
 @Autowired
 private RabbitMqSender mqSender;
 
 // 用来标记商品是否已经加入到redis中的key
 private static final String ISINREDIS = "isInRedis";
 
 // 用goodsId作为key,标记该商品是否已经卖完
 private Map<integer> seckillOver = new HashMap<integer>();
 
 // 用RateLimiter做限流,create(10),可以理解为QPS阈值为10
 private RateLimiter rateLimiter = RateLimiter.create(10);
 
 @PostMapping("/{sgId}")
 public JsonResult> seckillGoods(@PathVariable("sgId") Integer sgId, HttpServletRequest httpServletRequest){
  
  // 1. 如果QPS阈值超过10,即1秒钟内没有拿到令牌,就返回“人太多了,挤不进去”的提示
  if (!rateLimiter.tryAcquire(1, TimeUnit.SECONDS)) {
   return new JsonResult(SeckillGoodsEnum.TRY_AGAIN.getCode(), SeckillGoodsEnum.TRY_AGAIN.getMessage());
  }
  
  // 2. 检查用户是否登录(用户登录后,访问每个接口都应该在请求头带上token,根据token再去拿user)
  String token = httpServletRequest.getHeader("token");
  String userId = JWT.decode(token).getAudience().get(0);
  User user = userService.findUserById(Integer.valueOf(userId));
  if (user == null) {
   return new JsonResult(SeckillGoodsEnum.INVALID_TOKEN.getCode(), SeckillGoodsEnum.INVALID_TOKEN.getMessage());
  }
  
  // 3. 如果商品已经秒杀完了,就不执行下面的逻辑,直接返回商品已秒杀完的提示
  if (!seckillOver.isEmpty() && seckillOver.get(sgId)) {
   return new JsonResult(SeckillGoodsEnum.SECKILL_OVER.getCode(), SeckillGoodsEnum.SECKILL_OVER.getMessage());
  }
  
  // 4. 将所有参加秒杀的商品信息加入到redis中
  if (!RedisUtil.isExist(ISINREDIS)) {
   List<seckillgoods> goods = seckillService.getAllSeckillGoods();
   for (SeckillGoods seckillGoods : goods) {
    RedisUtil.set(String.valueOf(seckillGoods.getSgId()), seckillGoods.getSgSeckillNum());
    seckillOver.put(seckillGoods.getSgId(), false);
   }
   RedisUtil.set(ISINREDIS, true);
  }
  
  // 5. 先自减,预扣库存,判断预扣后库存是否小于0,如果是,表示秒杀完了
  Long stock = RedisUtil.decr(String.valueOf(sgId));
  if (stock (SeckillGoodsEnum.SECKILL_OVER.getCode(), SeckillGoodsEnum.SECKILL_OVER.getMessage());
  }
  
  // 6. 判断是否重复秒杀(成功秒杀并创建订单后,会将userId和goodsId作为key放到redis中)
  if (RedisUtil.isExist(userId + sgId)) {
   return new JsonResult(SeckillGoodsEnum.REPEAT_SECKILL.getCode(), SeckillGoodsEnum.REPEAT_SECKILL.getMessage());
  }
  
  // 7. 以上校验都通过了,就将当前请求加入到MQ中,然后返回“排队中”的提示
  String msg = userId + "," + sgId;
  mqSender.send(msg);
  return new JsonResult(SeckillGoodsEnum.LINE_UP.getCode(), SeckillGoodsEnum.LINE_UP.getMessage());
 }

}
</seckillgoods></integer></integer>
     

三、压测

用jmeter模拟并发请求,测试高并发情况下系统能否扛得住。由于只有一个id为1的商品,所以商品id固定写死1。但是每个用户都要先请求登录接口获取到token才能进行秒杀请求,有点儿麻烦,所以可以先把jwt模块注释掉,把userId当成参数传进去。jmeter配置如下图:

redis怎麼實現秒殺系統
jmeter压测配置
redis怎麼實現秒殺系統
jmeter压测配置

以上是redis怎麼實現秒殺系統的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除