记一次秒杀项目

最近公司准备做一个秒杀,翻阅了一些资料,通过不断地改进优化,终于设计出了一个比较稳定功能,我们项目中单独开了秒杀的自服务项目,以便于后期扩展,今天跟大家分享一下项目心得:

1.基本功能实现

最开始做出的1.0版本,话不多说上代码

@Override

public Object confirm(Integer productId,String accountId) {

    if (StringUtils.isEmpty(productId)) {

        return new CtrlResp<>("-1","商品id参数为空",null);

    }



    Address address = getUserDefaultAddress(accountId);

    if (address == null) {

        return new CtrlResp<>("-1","秒杀前请先填写默认地址",null);

    }



    Product product = productService.getProductById(productId);

    if (product == null) {

        return new CtrlResp<>("-1","商品不存在",null);

}



    validSeckillTime(product);//时间验证

    if (product.getStock() > 0) {
        return new CtrlResp<>("-1","商品已抢完",null);
    }


    return confirmOrder(product,address);
}
/**

 * 生成订单

 * @param product

 * @param address

 * @return

 */

private CtrlResp confirmOrder(Product product,Address address) {

//填充订单信息

Order order = Order.builder().build();

//填充订单配送信息



//生产订单

Order orderData = null;

try {

    orderData = orderService.createSeckillOrder(order);

}catch (Exception e) {

    throw BizEx.build(BizExEnum.ERROR_001.getCode(),"创建订单失败");

}



return new CtrlResp<>("0","成功",orderData);

}

 

@Override

public Order createSeckillOrder(Order order) {

    //订单信息插库



    //减库存

    List<OrderDetail> orderDetailList = order.getOrderDetailList();

    for (OrderDetail orderDetail:orderDetailList) {

        int updateNum = productService.updateStaockAfterSeckillSuccess(orderDetail.getProductId());

        if (updateNum<=0) {

            throw BizEx.build(BizExEnum.ERROR_001.getCode(),"更新库存失败");

        }

    }



    return null;

}

 

<update id="updateStaockAfterSeckillSuccess">

    update t_product set stock=stock-1.sellcount=sellcount+1 where 1=1 and id=#{productId} and stock>0

</update>

 

相信大家不难看出,这里使用的是数据库做的库存控制,到库存减少到0的时候后面的更新都不会在更新了,当然这个办法可以达到目的,由于数据库并不适合做并发量很大的操作,当并发量很高的情况下,这个方式肯定是不行的,显然秒杀这种业务场景都是发生在很高的并发量下,所以接下来进行

2. 加入缓存

我们加入缓存,将库存数用缓存进行控制,首先我们先将每个秒杀商品的库存加载到缓存中

@PostConstruct

public void init() {

    List<Product>  products =  productMapper.selectByMap(new ImmutableMap.Builder<String,Object>()

            .put("type", ProductType.SECKILL)

            .build());



    for (Product product:products) {

        keyValueService.set(RedisConstants.PRODUCT_STOCK+"_"+product.getId(),product.getStock());

    }

}

请求过来时查看缓存中的库存是否充足,充足则放行,不充足则提示商品已抢完,如果下单存在异常或退单的时候,我们要将库存进行还原,这里还有一个问题,就是随着最后一个商品被消耗掉,后面的请求都会被一一拦截掉,导致缓存stock会被减出一个很大的负数,这个时候如果有退单的情况,缓存中数量加1,这个时候的库存书依然为负数,后面在来请求依然无法获取到退掉的这个商品,所有我们在发现商品耗尽时,将缓存中数量及时还原成0,这个在发生退单时,库存数从0进行累加,下一个请求就可以获取到

 

 

 

 

通过Jmeter压测,这个时候性能已经提升了很多,虽然访问redis的速度很快,但还是会带来一定的性能消耗,其实在商品库存被消耗完的那一刻,库存对缓存的依赖就不需要了,这个时候我们完全可以在jvm内存在为商品是否已经被消耗完打个标记,这个就大大减少了对redis访问的压力,响应速度也会进一步提升

首先定义一个map

private static ConcurrentHashMap<String,Boolean> productSoldOutMap = new ConcurrentHashMap<>();

private static ConcurrentHashMap<String,Boolean> getProductSoldOutMap() {

    return productSoldOutMap;

}

当请求过来时在检查缓存之前,检查一下内存中的标识,库存消耗完时,我们将该商品已消耗完的表示写道内存中,一旦出现有退单或下单失败,我们清除掉商品对应的标识

 

这里有产生了一个问题,一般我们使用的微服务系统,同一个服务不止一台,各自维护各自的内存,比如a机器上可能标记已经是消耗完,b机器上还没有这个标记,还有退单的情况,如果对产品要求不是很严格的情况下,因为还有缓存的控制,这个问题也不是那么严重,秒杀本身也是一个运气活。

当然我们有解决的办法,这个时候我们可以使用zookeeper,利用zookeeper回调达到多级缓存的一致性

3.异步下单

运行可以看出,下单的过程是个相对耗时的操作,我们能不能将下单异步化喃,答案是可定的,我们继续进优化,将下单异步处理

 

定义监听器,将订单生成的逻辑搬移过来

 

由于下单的过程需要处理相应的逻辑,这里完全会有失败的情况,怎么办喃?这个我们要结合前端同步优化,当抢购成功后,让前端页面进入等待状态,定时轮询后端结果,我们可以订单对应的状态码,-1抢购失败,0成功,1继续等待,等待的过程中前端可以给一个友好的提示,比如排队中,当处理成功后,将信息放入缓存

加入防重复校验

 

到此基本功能已经完成,上面的代码只是将核心的逻辑展示给大家,具体下单业务还请大家根据自己的业务填充。还有一部分安全优化和限流防刷,下期继续和大家分享。。。

关注下方公众号,了解更多,回复关键字「面试」,更有好礼相送

👇👇👇

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值