今天我们来探讨一下商城项目当中秒杀的场景,秒杀场景可以说是商城项目当中最难的场景,也是最重要的场景,今天我们对秒杀业务的基本业务做一下梳理,并简单讲解秒杀的代码实现和需要注意的问题。

第一、秒杀业务的梳理。

秒杀模块包括秒杀商品列表,秒杀商品详情,秒杀下单,订单列表查询,订单详情,还有订单支付,我们这里暂时省略关于物流的部分。商品列表和商品详情这都好说,因为普通商城也会有这两个接口,这里比较难的是,秒杀下单。秒杀下单为什么难呢?难的地方有这么几个。

1、秒杀下单时防止超卖又不能影响性能。

我们都知道,秒杀的时候,有大量的请求直接调下单接口,那么如何做好前期的流量削峰呢?当然,入口是商品列表和详情接口,但这两个都只是简单的查询接口,加上缓存和冷热数据分离,性能上还是好解决的。但是下单的时候,出了查询,还有update,insert的操作。而且有一堆各种校验。所以,必须对下单接口进行削峰处理。我们公司采用的方式是用redis进行削峰。举个例子,比如今天这个商品,我们卖100件,那么,我会在redis里存一个商品id的key,value是100,相当于库存。当下单开始,我会创建一个另一个key,是商品id拼接“sale”,相当于销量,每次有一个人进来,我会在sale上加1,把这个值和redis里的库存比较,如果sale的value比库存100大,则直接返回用户,商品已经抢购完。这个过程中,我只要保证sale每次+1的时候,是线程安全的就行。那么怎么保证sale的线程安全呢,就是redis的incr方法,每次增加步长为购买的数量,拿返回值与100对比。当抢到这个订单的用户,没有支付,10分钟后自动取消了,我这个sale的值再减1,这个操作也是线程安全的就可以。这样,后台下单就会很轻松。下单的过程,可以再次削峰,用rabbitmq或者kafka这样的消息队列,通过订阅消息来创建订单,这样通过两个削峰,足够可以解决秒杀的性能问题,而且也不会超卖。

伪代码大致如下

long incr = 0;
String skey = Constant.getSeckillStockKey(order.getGoodsId());
incr = redisUtil.incr(skey,order.getNum());
if(CoUtil.isNotEmpty(goods.getSeckillLimitBy())) {
   if (incr > goods.getSeckillLimitBy()) { // 这个seckillLimitby就相当于库存最大值
      redisUtil.decr(stockKey, order.getNum());
      throw new Exception("商品足");
   }
}

2、秒杀下单后支付订单和下单时,同时修改库存,不能出现线程安全问题

当秒杀下单,不管是直接下单还是用mq下单,必然有一个修改库存和销量的动作,而如果同时,有人在操作退货或者支付,也会修改库存和销量,如何保证这时候的线程安全也是很重要的。我们项目中,用的是reddison的分布式锁来解决这个问题的。在修改库存的地方,可以加一个分布式锁。当然,也可以用数据库乐观锁来解决。

 

第二、秒杀需要注意的问题。

1、秒杀时出现性能问题该如何解决
我觉得可以从几个方面着手,第一个是代码层面,第二个是服务层,第三个是数据库层。代码层最好是大家一起review代码,集众人的智慧解决问题。服务层可以加内存或者加节点,尤其是用容器化部署的方式,可以通过HPA或者HPC的方式,动态加节点。数据库层面可能是因为数据库的锁导致修改语句排队而延时,影响了性能。

2、秒杀下单中缓存的使用。
秒杀最难的就是性能问题,京东,天猫,淘宝,每年都会有秒杀,这种下单量可以说是巨量。他们是充分的利用了缓存。以后可以细讲的,多级缓存,一级二级缓存,这样的好处是,数据库的压力小了。

关于秒杀下单的内容有很多,商城项目的业务中,秒杀一直是一个重中之重,以后再继续完善。