秒杀这个东西虽然快被玩“烂”了,但如果仅仅是浏览网上的文章的话,并不能真正理解那些文章中说到的各种方案。例如都说要消息队列来削峰,那该如何做?就算知道如何做,那真正上手写的时候,情况真的那么简单么?所以,计算机这个玩意,尤其是软件工程,实践是非常非常非常重要的,理论背的再熟也不如上手尝试开发来的实在。
其实很久之前我就想做这个了,但一直没有太好的环境,因为之前做的那个项目有点“大”了,还用了各种组件(ES、Kafka,Redis等),单单启动就能让我电脑的内存占用达到90%,即使做了也没法测试。就在昨天,我重新开了一个小项目,仅仅写了一些简单的业务逻辑,对于“浅尝”秒杀这个场景来说足够了。
下面我会把我在实现的过程中所思考的和遇到的坑分享给大家。
下面的内容都假设大家心中已经了秒杀架构的理论知识,如果对秒杀架构的理论还不是太了解,建议先到网上搜索相关资料学习(这样的资料网上非常多)。
1 准备阶段
在做之前,得必须想明白整体架构,不求完美,只求至少合理,毕竟一个好的架构是迭代出来的而不是一开始就设计出来的。下面是项目的初步架构图:
图画的比较丑(实在不太会画架构图),从图中看出架构比较简单,比最简单的MVC架构仅仅多了Redis和消息队列层而已。我大致描述一下整个流程:
- 前端发送HTTP请求
- 前端负载均衡器接受请求,将根据某种规则将请求转发到对应的机器上
- 服务器收到一个请求,开始着手处理业务。
- 首先先到Redis中查看Redis是否有库存的缓存,如果有,就取出来判断库存是否充足,否则就需要到数据库去查询,查询完毕后将其放入缓存中。
- 如果缓存中的数据表示库存充足,就发送一条消息到消息队列里,并返回下单成功的消息给前端,如果库存不足,就直接返回下单失败给前端,不再发送消息到消息队列。
- 此时消息接受者会收到消息,消息接受者会根据消息来生成订单,并存入数据库,完成本次下单。
2 开始编写业务逻辑
有了基本架构之后,写业务逻辑应该是一件非常简单的事了,为了简单,我仅仅写了三个实体类,User、Order、Product。分别代表用户,订单和商品,而且也仅仅包含了几个必要的字段。然后就是数据访问接口了,每个实体类对应一个接口,我项目中使用的是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());
}
}
然后就是对应的业务处理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("同学,来晚了,东西都被其他人抢走了....");
}
//发送异步消息
sendToQueue(productId, userId);
//不用等待消息处理完毕,就可以直接返回下单成功了。
return ServerResponse.createBySuccessMessage("下单成功!");
}
//发送消息的具体逻辑
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());
}
//校验库存的业务逻辑
private boolean checkStock(Long productId) {
//先尝试去缓存中取库存
Long stock = (Long) redisTemplate.opsForHash().get("SK_ORDER", productId);
//如果缓存中不存在该行缓存
if (stock == null) {
//就到数据库中取
Product product = productRepository.findStockById(productId);
//如果数据库中的库存小于等于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);
//写回数据库
productRepository.saveAndFlush(product);
//新生成的订单存入数据库
orderRepository.save(order);
//还要记得更新缓存的值
redisTemplate.opsForHash().put("SK_ORDER", productId, product.getStock());
}
}
这个是核心的处理方法,基本上就是按照上面描述的流程编写的,注释写的也的比较清楚了,直接看注释吧,不再赘述。
因为写的太着急了,没认真好好写,一些变量的命名是有问题的,建议各位如果要自己尝试的话,最好认真一些,这样以后还能看懂自己的代码,哈哈。
3 测试一下
写完了代码之后肯定要测试一下(对自己的代码负责)。我使用的是JMetter这个测试工具,下图是线程组的配置:
在单机上搞那么激进的配置,在使用消息队列之前,我想都不敢想,那时候开个300个线程,就各种连接失败了,错误率高达80%以上。这个配置是我先从小的200开始慢慢增加的,各位最好不要一开始就搞这样(弄不好就死机了),慢慢增加,让压力慢慢上去。
下图是测试的结果:
主要看看吞吐量,order这里是220/s,对于单机来说已经不算低了。
4 小结
秒杀这个场景虽然已经被玩“烂”了,但还是非常值得学习的。还是开头的那句话,不要只看理论而不上手实践,上手实践才能加深对理论的理解,而且实践之后的成就感也是不实践所没有的。本文描述的仅仅是总多秒杀架构方案的其中一种,其实还有很多种方案,例如用Redis而不是消息队列来做,或者采用服务熔断,服务降级结合消息队列来做.....,以后有机会再写吧。