秒殺這個東西雖然快被玩“爛”了翘县,但如果僅僅是瀏覽網(wǎng)上的文章的話最域,并不能真正理解那些文章中說到的各種方案。例如都說要消息隊列來削峰炼蹦,那該如何做羡宙?就算知道如何做,那真正上手寫的時候掐隐,情況真的那么簡單么狗热?所以钞馁,計算機這個玩意,尤其是軟件工程匿刮,實踐是非常非常非常重要的僧凰,理論背的再熟也不如上手嘗試開發(fā)來的實在。
其實很久之前我就想做這個了熟丸,但一直沒有太好的環(huán)境训措,因為之前做的那個項目有點“大”了,還用了各種組件(ES光羞、Kafka绩鸣,Redis等),單單啟動就能讓我電腦的內(nèi)存占用達到90%纱兑,即使做了也沒法測試因妇。就在昨天卫键,我重新開了一個小項目陶舞,僅僅寫了一些簡單的業(yè)務邏輯司抱,對于“淺嘗”秒殺這個場景來說足夠了。
下面我會把我在實現(xiàn)的過程中所思考的和遇到的坑分享給大家铐炫。
下面的內(nèi)容都假設大家心中已經(jīng)了秒殺架構的理論知識垒手,如果對秒殺架構的理論還不是太了解,建議先到網(wǎng)上搜索相關資料學習(這樣的資料網(wǎng)上非常多)倒信。
1 準備階段
在做之前科贬,得必須想明白整體架構,不求完美堤结,只求至少合理唆迁,畢竟一個好的架構是迭代出來的而不是一開始就設計出來的鸭丛。下面是項目的初步架構圖:
圖畫的比較丑(實在不太會畫架構圖)竞穷,從圖中看出架構比較簡單,比最簡單的MVC架構僅僅多了Redis和消息隊列層而已鳞溉。我大致描述一下整個流程:
- 前端發(fā)送HTTP請求
- 前端負載均衡器接受請求瘾带,將根據(jù)某種規(guī)則將請求轉(zhuǎn)發(fā)到對應的機器上
- 服務器收到一個請求,開始著手處理業(yè)務熟菲。
- 首先先到Redis中查看Redis是否有庫存的緩存看政,如果有,就取出來判斷庫存是否充足抄罕,否則就需要到數(shù)據(jù)庫去查詢允蚣,查詢完畢后將其放入緩存中。
- 如果緩存中的數(shù)據(jù)表示庫存充足呆贿,就發(fā)送一條消息到消息隊列里嚷兔,并返回下單成功的消息給前端森渐,如果庫存不足,就直接返回下單失敗給前端冒晰,不再發(fā)送消息到消息隊列同衣。
- 此時消息接受者會收到消息,消息接受者會根據(jù)消息來生成訂單壶运,并存入數(shù)據(jù)庫耐齐,完成本次下單。
2 開始編寫業(yè)務邏輯
有了基本架構之后蒋情,寫業(yè)務邏輯應該是一件非常簡單的事了埠况,為了簡單,我僅僅寫了三個實體類棵癣,User询枚、Order、Product浙巫。分別代表用戶金蜀,訂單和商品,而且也僅僅包含了幾個必要的字段的畴。然后就是數(shù)據(jù)訪問接口了渊抄,每個實體類對應一個接口,我項目中使用的是JPA這個框架丧裁,搭建起來非常簡單护桦。
還要編寫對應的Controller,下面我只貼出OrderController的代碼煎娇,其他的Controller都非常簡單二庵,玩過Spring的朋友應該都能快速解決:
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private IOrderService orderService;
private static final String CURRENT_USER = "CURRENT_USER";
@PostMapping
public ServerResponse<Order> createOrder(Long productId, HttpSession session) {
if (session.getAttribute(CURRENT_USER) == null) {
return ServerResponse.createByErrorMessage("請先登錄");
}
User user = (User) session.getAttribute(CURRENT_USER);
return orderService.createOrder(productId, user.getId());
}
}
然后就是對應的業(yè)務處理orderService了:
@Service
public class OrderService implements IOrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductRepository productRepository;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisTemplate<String, Long> redisTemplate;
@Override
@Transactional
public ServerResponse<Order> createOrder(Long productId, Long userId) {
//校驗庫存
if (!checkStock(productId)) {
return ServerResponse.createBySuccessMessage("同學,來晚了缓呛,東西都被其他人搶走了....");
}
//發(fā)送異步消息
sendToQueue(productId, userId);
//不用等待消息處理完畢催享,就可以直接返回下單成功了。
return ServerResponse.createBySuccessMessage("下單成功哟绊!");
}
//發(fā)送消息的具體邏輯
private void sendToQueue(Long productId, Long userId) {
OrderInfo orderInfo = new OrderInfo();
orderInfo.setProductId(productId);
orderInfo.setUserId(userId);
rabbitTemplate.convertAndSend(
RabbitMQConfig.DEFAULT_DIRECT_EXCHANGE,
RabbitMQConfig.ORDER_ROUTE_KEY,
orderInfo);
}
//消息接受者
@RabbitListener(queues = RabbitMQConfig.ORDER_QUEUE)
private void orderReceiver(OrderInfo orderInfo) {
addOrder(orderInfo.getProductId(), orderInfo.getUserId());
}
//校驗庫存的業(yè)務邏輯
private boolean checkStock(Long productId) {
//先嘗試去緩存中取庫存
Long stock = (Long) redisTemplate.opsForHash().get("SK_ORDER", productId);
//如果緩存中不存在該行緩存
if (stock == null) {
//就到數(shù)據(jù)庫中取
Product product = productRepository.findStockById(productId);
//如果數(shù)據(jù)庫中的庫存小于等于0了因妙,就直接返回false,表示庫存不足
if (product == null || product.getStock() <= 0)
return false;
//否則票髓,將庫存信息存入緩存
redisTemplate.opsForHash().put("SK_ORDER", productId, product.getStock());
} else if (stock <= 0){
//如果存在緩存攀涵,就直接判斷,如果小于等于0洽沟,就表明庫存不足以故,返回false即可
return false;
}
//走到這表示庫存充足,返回true即可
return true;
}
@Transactional
public void addOrder(Long productId, Long userId) {
//獲取Product對象
Product product = productRepository.findById(productId).orElse(null);
if (product == null)
return;
//生成新的訂單
Order order = new Order();
order.setUserId(userId);
order.setStatus(OrderStatus.NO_PAY.getCode());
order.setOrderNo(UUID.randomUUID().toString());
//將庫存減1
product.setStock(product.getStock() - 1);
//寫回數(shù)據(jù)庫
productRepository.saveAndFlush(product);
//新生成的訂單存入數(shù)據(jù)庫
orderRepository.save(order);
//還要記得更新緩存的值
redisTemplate.opsForHash().put("SK_ORDER", productId, product.getStock());
}
}
這個是核心的處理方法裆操,基本上就是按照上面描述的流程編寫的怒详,注釋寫的也的比較清楚了鳄乏,直接看注釋吧,不再贅述棘利。
因為寫的太著急了橱野,沒認真好好寫,一些變量的命名是有問題的善玫,建議各位如果要自己嘗試的話水援,最好認真一些,這樣以后還能看懂自己的代碼茅郎,哈哈蜗元。
3 測試一下
寫完了代碼之后肯定要測試一下(對自己的代碼負責)。我使用的是JMetter這個測試工具系冗,下圖是線程組的配置:
在單機上搞那么激進的配置奕扣,在使用消息隊列之前,我想都不敢想掌敬,那時候開個300個線程惯豆,就各種連接失敗了,錯誤率高達80%以上奔害。這個配置是我先從小的200開始慢慢增加的楷兽,各位最好不要一開始就搞這樣(弄不好就死機了),慢慢增加华临,讓壓力慢慢上去芯杀。
下圖是測試的結(jié)果:
主要看看吞吐量,order這里是220/s雅潭,對于單機來說已經(jīng)不算低了揭厚。
4 小結(jié)
秒殺這個場景雖然已經(jīng)被玩“爛”了,但還是非常值得學習的扶供。還是開頭的那句話筛圆,不要只看理論而不上手實踐,上手實踐才能加深對理論的理解诚欠,而且實踐之后的成就感也是不實踐所沒有的顽染。本文描述的僅僅是總多秒殺架構方案的其中一種,其實還有很多種方案轰绵,例如用Redis而不是消息隊列來做,或者采用服務熔斷尼荆,服務降級結(jié)合消息隊列來做.....左腔,以后有機會再寫吧。