好好學習,每天向上前端
本文已收錄至個人Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Starjava
上面這張圖是整個秒殺系統的流程。簡單介紹一下:git
秒殺是一個併發量很大的系統,數據吞吐量都很大,MySQL的數據是保存在硬盤中的,數據吞吐的能力知足不了整個秒殺系統的需求。爲了提升系統的訪問速度,咱們定時將秒殺商品從MySQL加載進Redis,由於Redis的數據是保存在內存中的,速度很是快,能夠知足很高的吞吐量。github
用戶訪問秒殺系統,請求到了OpenResty,OpenResty從Redis中加載秒殺商品,而後用戶來到了秒殺列表頁。當用戶點擊某個秒殺商品時,OpenResty再從Redis中加載秒殺商品詳情信息,接着用戶就來到了秒殺商品詳情頁。redis
當進入到商品詳情頁以後用戶就能夠點擊下單了,點擊下單的時候,OpenResty會檢查商品是否還有庫存,沒有庫存就下單失敗。有庫存的話還須要檢查一下用戶是否登陸,沒有登陸的話再到OAuth2.0認證服務那邊去登陸,登陸成功後再進入到秒殺微服務中,開始正式的下單流程。sql
理論上這時候還要對用戶進行一些合法性檢測,好比帳號是否異常等,可是這太耗時了,爲了減小系統響應的時間,用戶檢測這一步先省略。直接讓用戶進行排隊,排隊就是將用戶id和商品id存入Redis隊列,成功排隊後給用戶返回一個 「正在排隊」的信息。數據庫
當排隊成功後就開啓多線程搶單,爲每一個排隊的用戶分配一個線程。在排隊用戶本身的線程中開始檢測帳號的狀態是否正常,而後從Redis中檢測庫存時候足夠,當全部條件都知足的時候,下單成功,將訂單信息存入Redis。並將Redis中的排隊信息從「排隊中」改成「待支付」,這樣前端在查詢狀態的時候就知道能夠開始支付了,而後跳轉到支付頁面進行支付。當用戶支付成功後,將搶單信息從Redis中刪除,並同步到MySQL中。後端
最後一個問題,有的用戶成功搶單後並不去付款,因此咱們須要定時去處理未支付的訂單。方案和上一篇文章中提到的同樣,使用RabbitMQ死信隊列。在搶單成功後將訂單id、用戶id和商品id存到RabbitMQ的隊列1,設置半個小時後過時,過時後將信息發送給隊列2,咱們去監聽隊列2。當監聽到隊列2中的消息的時候,說明半個小時已經到了,這時候咱們再去Redis中查詢訂單的狀態,若是已經支付了就不去管它;若是沒有支付就向微信服務器發送請求關閉支付,而後回滾庫存,並將Redis中的搶單信息刪除。服務器
這樣整個秒殺流程就結束了。微信
怎麼搭建秒殺微服務就不記錄了,沒什麼好說的,秒殺微服務名爲changgou-service-seckill。定時任務我也是第一次接觸,因此在這裏記錄一下。
首先在啓動類上添加一個註解@EnableScheduling去開始對定時任務的支持。而後建立一個類SeckillGoodsPushTask,在這個類上添加@Component註解,將其注入Spring容器。而後再添加一個方法,加上@Scheduled註解,聲明這個方法是一個定時任務。
/** * SeckillGoodsPushTask * 定時將秒殺商品加載到redis中 */ @Scheduled(cron = "0/5 * * * * ?") public void loadGoodsPushRedis() { List<Date> dateMenu = DateUtil.getDateMenus(); for (Date date : dateMenu) { date.setYear(2019-1900); //2019-6-1 爲了方便測試 date.setMonth(6-1); date.setDate(1); String dateString = SystemConstants.SEC_KILL_GOODS_PREFIX +DateUtil.data2str(date,"yyyyMMddHH"); BoundHashOperations boundHashOperations = redisTemplate.boundHashOps(dateString); Set<Long> keys = boundHashOperations.keys(); //獲取Redis中已有的商品的id集合 List<SeckillGoods> seckillGoods; //將秒殺商品的信息從數據庫中加載出來 if (keys!=null && keys.size()>0) { seckillGoods = mapper.findSeckillGoodsNotIn(date,keys); } else { seckillGoods = mapper.findSeckillGoods(date); } //遍歷秒殺商品集合,將商品依次放入Redis中 for (SeckillGoods seckillGood : seckillGoods) { boundHashOperations.put(seckillGood.getId(),seckillGood); } } } ---------------------------------------------------------------------------------------------------------------- @Repository("seckillGoodsMapper") public interface SeckillGoodsMapper extends Mapper<SeckillGoods> { //查找符合條件的秒殺商品 @Select("SELECT" + " * " + " FROM " + " tb_seckill_goods " + " WHERE " + " status = 1 " + " AND stock_count > 0 " + " AND start_time >= #{date} " + " AND end_time < DATE_ADD(#{date},INTERVAL 2 HOUR)") List<SeckillGoods> findSeckillGoods(@Param("date") Date date); //查詢出符合條件的秒殺商品,排除以前已存入的 @SelectProvider(type = SeckillGoodsMapper.SeckillProvider.class, method = "findSeckillGoodsNotIn") List<SeckillGoods> findSeckillGoodsNotIn(@Param("date") Date date, @Param("keys") Set<Long> keys); class SeckillProvider { public String findSeckillGoodsNotIn(@Param("date") Date date, @Param("keys") Set<Long> keys) { StringBuilder sql = new StringBuilder("SELECT" + " * " + " FROM " + " tb_seckill_goods " + " WHERE " + " status = 1 " + " AND stock_count > 0 " + " AND start_time >= "); sql.append("'").append(date.toLocaleString()).append("'") .append(" AND end_time < DATE_ADD(") .append("'").append(date.toLocaleString()).append("'") .append(" ,INTERVAL 2 HOUR) ") .append(" AND id NOT IN ("); for (Long key : keys) { sql.append(key).append(","); } sql.deleteCharAt(sql.length() - 1).append(")"); System.out.println(sql.toString()); return sql.toString(); } } }
(cron = "0/5 * * * * ?")
中幾個參數分別表明秒-分-時-日-月-周-年。年能夠省略,因此是6個。*
表示全部值,好比 「分」 是*就表明每分鐘都執行。?
表示不須要關心這個值是多少。/
表示遞增觸發,0/5表示從0秒開始每5秒觸發一次。因此這段代碼配置的就是每5秒執行一次定時任務。
上面這段代碼的意思是:將MySQL中的秒殺商品放入Redis,爲了不添加劇復的商品,先獲取Redis中已有商品的id集合,而後在查詢數據庫的時候將已有的排除掉。redis中存入商品的鍵爲秒殺開始的時間,例如 "2020100110"表示2020年10月1日10點,獲取時間菜單用的是資料提供的一個工具類DateUtil。DateUtil的代碼不難,我就不介紹了,開調試模式跟着走一遍就能看懂。爲了方便測試,我將日期定在了2019年6月1日,實際開發中應該用當前日期。
將商品加載到Redis中後就能夠開始下單流程了,首先須要有個秒殺頻道頁,就是將對應時間段的秒殺商品加載到頁面上展現出來。前端將當前時間的字符串(yyyyMMddHH)傳到後端,後端從Redis中查詢出對應的商品返回到前端,前端進行展現。
// SeckillGoodsController //根據時間段(2019090516) 查詢該時間段的全部的秒殺的商品 @GetMapping("/list") public Result<List<SeckillGoods>> list(@RequestParam("time") String time){ List<SeckillGoods> list = seckillGoodsService.list(time); return new Result<>(true,StatusCode.OK,"查詢成功",list); } ----------------------------------------------------------------------------------- // SeckillGoodsServiceImpl @Override public List<SeckillGoods> list(String time) { return redisTemplate.boundHashOps(SystemConstants.SEC_KILL_GOODS_PREFIX+time).values(); }
代碼很簡單,就是根據鍵將商品從Redis中查詢出來。
當用戶點擊秒殺頻道頁的商品後,就會進入到秒殺商品詳情頁。前端將當前時間段和商品的id傳到後端,後端從Redis中將商品信息查詢出來,而後返回給前端進行展現。
// SeckillGoodsController //根據時間段 和秒殺商品的ID 獲取商品的數據 @GetMapping("/one") public Result<SeckillGoods> one(String time,Long id){ SeckillGoods seckillGoods = seckillGoodsService.one(time, id); return new Result<>(true,StatusCode.OK,"查詢商品數據成功",seckillGoods); } ------------------------------------------------------------------------------------------ // SeckillGoodsServiceImpl @GetMapping("/one") public Result<SeckillGoods> one(String time,Long id){ SeckillGoods seckillGoods = seckillGoodsService.one(time, id); return new Result<>(true,StatusCode.OK,"查詢商品數據成功",seckillGoods); }
上面兩個小節內容都很少,如今正式進入下單的流程。由於在秒殺環境中,併發量都很大,若是隻開一個線程的話,用戶不知道要等到猴年馬月,因此爲每一個下單的用戶分配一個線程去進行處理是比較穩當的。
要在SpringBoot中開啓多線程,首先在啓動類上添加一個註解@EnableAsync去開啓對異步任務的支持。
//SeckillOrderController //下單 @RequestMapping("/add") public Result<Boolean> add(String time,Long id){ //1.獲取當前登陸的用戶的名稱 String username ="robod";//測試用寫死 boolean flag = seckillOrderService.add(id, time, username); return new Result(true,StatusCode.OK,"排隊中。。。",flag); }
前端將時間段和商品的id傳進來,用戶名暫時寫死,方便測試。
// SeckillOrderServiceImpl @Override public boolean add(Long id, String time, String username) { SeckillStatus seckillStatus = new SeckillStatus(username,LocalDateTime.now(),1,id,time); //將seckillStatus存入redis隊列 redisTemplate.boundListOps(SystemConstants.SEC_KILL_USER_QUEUE_KEY).leftPush(seckillStatus); redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_STATUS_KEY).put(username,seckillStatus); multiThreadingCreateOrder.createOrder(); return true; }
在這段代碼中,先根據已有的信息建立了一個SeckillStatus對象,這個類中存放了秒殺的一些狀態信息。而後將seckillStatus放入redis隊列中,若是及時地處理訂單系統響應速度就會變慢,因此先建立一個SeckillStatus放入redis,而後調用multiThreadingCreateOrder.createOrder()去開啓一個線程處理訂單。
@Component public class MultiThreadingCreateOrder { ………… //異步搶單 @Async //聲明該方法是個異步任務,另開一個線程去運行 public void createOrder() { //從redis隊列中取出seckillStatus SeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundListOps(SystemConstants.SEC_KILL_USER_QUEUE_KEY).rightPop(); BoundHashOperations seckillGoodsBoundHashOps = redisTemplate.boundHashOps(SystemConstants.SEC_KILL_GOODS_PREFIX + seckillStatus.getTime()); //從redis中查詢出秒殺商品 SeckillGoods seckillGoods = (SeckillGoods)seckillGoodsBoundHashOps.get(seckillStatus.getGoodsId()); if (seckillGoods == null || seckillGoods.getStockCount() <=0 ) { throw new RuntimeException("已售罄"); } //建立秒殺訂單 SeckillOrder seckillOrder = new SeckillOrder(); seckillOrder.setSeckillId(seckillGoods.getId()); seckillOrder.setMoney(seckillGoods.getCostPrice()); seckillOrder.setUserId(seckillStatus.getUsername()); seckillOrder.setCreateTime(LocalDateTime.now()); seckillOrder.setStatus("0"); //將秒殺訂單存入redis,鍵爲用戶名,確保一個用戶只有一個秒殺訂單 redisTemplate.boundHashOps(SystemConstants.SEC_KILL_ORDER_KEY) .put(seckillStatus.getUsername(),seckillOrder); //減庫存,若是庫存沒了就從redis中刪除,並將庫存數據寫到MySQL中 seckillGoods.setStockCount(seckillGoods.getStockCount()-1); if (seckillGoods.getStockCount() <= 0) { seckillGoodsBoundHashOps.delete(seckillStatus.getGoodsId()); seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods); } else { seckillGoodsBoundHashOps.put(seckillStatus.getGoodsId(),seckillGoods); } //下單成功,更改seckillstatus的狀態,再存入redis中 seckillStatus.setOrderId(seckillOrder.getId()); seckillStatus.setMoney(Float.valueOf(seckillGoods.getCostPrice())); seckillStatus.setStatus(2); //等待支付 redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_STATUS_KEY) .put(seckillStatus.getUsername(),seckillStatus); } }
在這個方法上添加了一個@Async註解,說明該方法是個異步任務,每次執行該方法的時候都會另開一個線程去運行。以前不是將訂單存入redis隊列中了嗎,如今從redis隊列中取出。而後根據商品id查詢出商品信息。接着進行庫存判斷,若是沒有商品或者庫存沒了說明已經賣完了,拋出已售罄的異常。若是有庫存的話,就建立一個秒殺訂單,將status置爲0表示未支付。 而後將訂單存入redis中,這樣訂單就算建立完成了。成功建立訂單後就應該減去相應的庫存。若是減完庫存後發現庫存沒了,說明最後一件商品已經賣完了,這時候就能夠將redis中的該商品刪除,並更新到MySQL中。
最後修改seckillstatus的內容,並更新到redis中。以前沒說把seckillstatus存入redis的做用,其實它的做用就是供前端查詢訂單狀態。
既然是查詢訂單狀態,得提供一個接口吧👇
// SeckillOrderController //查詢當前登陸的用戶的搶單信息(狀態) @GetMapping("/query") public Result<SeckillStatus> queryStatus(String username) { SeckillStatus seckillStatus = seckillOrderService.queryStatus(username); if (seckillStatus == null) { return new Result<>(false,StatusCode.NOT_FOUND_ERROR,"未查詢到訂單信息"); } return new Result<>(true,StatusCode.OK,"訂單查詢成功",seckillStatus); } ------------------------------------------------------------------------------------------- //SeckillOrderServiceImpl @Override public SeckillStatus queryStatus(String username) { return (SeckillStatus) redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_STATUS_KEY).get(username); }
前端將用戶名傳入進來,而後查詢訂單狀態,若是查詢出來的狀態是待支付的話,就能夠進入支付流程了。
好了,這篇文章到這裏就結束了,主要介紹了一下秒殺的流程,而後實現了定時任務,秒殺頻道頁,秒殺商品詳情頁和多線程搶單的功能。這個秒殺系統尚未結束,還存在不少問題,在下一篇文章中,將會修改現有的問題並繼續完善秒殺的流程。讓咱們下期再見!
碼字不易,能夠的話,給我來個
點贊
,收藏
,關注