前言
之前在 Java-Interview 中提到过秒杀架构的设计,这次基于其中的理论简单实现了一下。
本次采用循序渐进的方式逐步提高性能达到并发秒杀的效果
本文所有涉及的代码:
最终架构图:
先简单根据这个图谈下请求的流转,因为后面不管怎么改进这个都是没有变的。
- 前端请求进入
web
层,对应的代码就是controller
。
- 之后将真正的库存校验、下单等请求发往
Service
层(其中 RPC 调用依然采用的dubbo
,只是更新为最新版本,本次不会过多讨论 dubbo 相关的细节,有兴趣的可以查看 基于dubbo的分布式架构)。
Service
层再对数据进行落地,下单完成。
无限制
其实抛开秒杀这个场景来说正常的一个下单流程可以简单分为以下几步:
- 校验库存
- 扣库存
- 创建订单
- 支付
基于上文的架构所以我们有了以下实现:
先看看实际项目的结构:
还是和以前一样:
- 提供出一个
API
用于Service
层实现,以及web
层消费。
- web 层简单来说就是一个
SpringMVC
。
Service
层则是真正的数据落地。
SSM-SECONDS-KILL-ORDER-CONSUMER
则是后文会提到的Kafka
消费。
数据库也是只有简单的两张表模拟下单:
CREATE TABLE `stock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称', `count` int(11) NOT NULL COMMENT '库存', `sale` int(11) NOT NULL COMMENT '已售', `version` int(11) NOT NULL COMMENT '乐观锁,版本号', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; CREATE TABLE `stock_order` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `sid` int(11) NOT NULL COMMENT '库存ID', `name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8;
web 层 controller
实现:
@Autowired private StockService stockService; @Autowired private OrderService orderService; @RequestMapping("/createWrongOrder/{sid}") @ResponseBody public String createWrongOrder(@PathVariable int sid) { logger.info("sid=[{}]", sid); int id = 0; try { id = orderService.createWrongOrder(sid); } catch (Exception e) { logger.error("Exception",e); } return String.valueOf(id); }
其中 web 作为一个消费者调用看 OrderService
提供出来的 dubbo 服务。
Service 层,OrderService
实现:
首先是对 API 的实现(会在 API 提供出接口):
@Service public class OrderServiceImpl implements OrderService { @Resource(name = "DBOrderService") private com.crossoverJie.seconds.kill.service.OrderService orderService ; @Override public int createWrongOrder(int sid) throws Exception { return orderService.createWrongOrder(sid); } }
这里只是简单调用了 DBOrderService
中的实现,DBOrderService 才是真正的数据落地,也就是写数据库了。
DBOrderService 实现:
Transactional(rollbackFor = Exception.class) @Service(value = "DBOrderService") public class OrderServiceImpl implements OrderService { @Resource(name = "DBStockService") private com.crossoverJie.seconds.kill.service.StockService stockService; @Autowired private StockOrderMapper orderMapper; @Override public int createWrongOrder(int sid) throws Exception{ //校验库存 Stock stock = checkStock(sid); //扣库存 saleStock(stock); //创建订单 int id = createOrder(stock); return id; } private Stock checkStock(int sid) { Stock stock = stockService.getStockById(sid); if (stock.getSale().equals(stock.getCount())) { throw new RuntimeException("库存不足"); } return stock; } private int saleStock(Stock stock) { stock.setSale(stock.getSale() + 1); return stockService.updateStockById(stock); } private int createOrder(Stock stock) { StockOrder order = new StockOrder(); order.setSid(stock.getId()); order.setName(stock.getName()); int id = orderMapper.insertSelective(order); return id; } }
预先初始化了 10 条库存。
手动调用下 createWrongOrder/1
接口发现:
库存表:
订单表:
一切看起来都没有问题,数据也正常。
但是当用 JMeter
并发测试时:
测试配置是:300个线程并发,测试两轮来看看数据库中的结果:
请求都响应成功,库存确实也扣完了,但是订单却生成了 124 条记录。
这显然是典型的超卖现象。
其实现在再去手动调用接口会返回库存不足,但为时晚矣。