Java 秒杀系统性能优化(初始化篇)

一、背景介绍

秒杀系统顾名思义是在有限的时间里售出指定数量的商品,这个功能在网上购物项目中是很常见的,同时也是很多大厂面试出现频率很高的知识点,之前自己在阅读面经和学习一些框架和中间件的时候也经常遇到这个功能,如何保证在高并发的情况下不出现超买超卖的情况,如何保证系统能进行比较快速的操作响应,这都是在设计秒杀系统的时候经常需要考虑的问题。

在结束了暑期的腾讯实习后,自己对技术有了更多地思考,同时在实习的过程中也接触了很多平时自己做项目接触不到的一些中间件和框架等,所以趁着开学的这段时间在有了一定的技术积累后打算开始进行尝试,尽可能的通过自己的思考和网上的相关思路去自己从零开始实现一个秒杀系统,并尝试着对秒杀系统进行性能的优化,使其具备更高的并发量和更快的响应速度。

关于这个秒杀系统我会通过一系列的博文来记述它的实现过程,真正做到从零开始,让大家在看过我的博文记述后也能够自己去实现一个属于自己的秒杀系统。首先这篇博文是系统的最初版本,也是在我刚刚学习完 Springboot 后的一次实践,随着这篇博文更新的是系统的最起始代码即目前的系统只是一个简单的包含商品和订单的小项目,并且只提供了一个主要的接口来对我们的秒杀功能进行测试。

初始的代码中包含了秒杀系统最基础的代码逻辑,即:

查询商品库存 ——> 创建秒杀订单(当当前的商品尚有库存时)——> 减库存

后续的博文主要来介绍怎样去对这个系统去进行优化,大家可以在 GitHub 上面找到并检出最初代码的分支(如下),然后自己也来在最基础的代码上尝试着对系统进行性能的优化。同时,该项目前期的话打算只针对于后端的逻辑进行编码,即仅对前端提供调用的接口,在后面有时间的话自己也会尝试编写前端,并在前端也进行相关的性能优化。

同时,为了提高优化过程的效率,我也会不断的向大家推荐一些小的工具去辅助我们进行编码,首先得话现在可以推荐一款 Http 请求调试工具 Postman(界面如下),我们可以通过使用这款工具来对我们的请求接口进行测试,以此来查看代码执行后的结果或者相关代码执行后所返回的结果是不是我们所想要的。

如当前项目的秒杀接口应为 Post:http://localhost:8080/spike/ 商品Id (如下图所示)

二、项目源码

GitHub:https://github.com/TIYangFan/SpikeSystem (如果我的项目可以帮到你的话,请帮我 star ^_^ ~ )

三、代码结构介绍

整个项目的目录结构现在是很简单的,仅实现了一个简单的商品秒杀的过程,代码也比较简单,很容易理解,所以这里就不过多赘述了,后面我们就在些代码的基础上来对系统进行性能优化。

 

四、数据库创建

--商品表
create table product
(
    id bigint primary key auto_increment,
    name varchar(20) not null,
    price float not null,
    stock int not null,
    pic varchar(140) not null
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

--订单表
create table spike_order
(
    id bigint primary key auto_increment,
    product_id int not null,
    amount float not null
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

 

五、细节优化

这里需要注意的是下面这段代码的执行逻辑,这段代码很容易出现商品超卖的问题,即当多个线程同时查询商品后且尚未有线程进行减库存前,这时多个线程中所查询到的结果都是商品目前还存在库存,可以进行售卖,所以会继续创建秒杀订单,然后执行到减库存,这时如果多个线程都执行到了减库存这一步,并且成功进行了商品减库存的操作,那么这时就出现了商品超卖的问题。

    @Transactional
    public void spike(Long productId) {

        // 查询商品
        Product product = productService.getProductById(productId);
        if (product.getStock() <= 0){
            throw new RuntimeException("The product has been sold out.");
        }

        // 创建秒杀订单
        Order order = new Order();
        order.setProductId(product.getId());
        order.setAmount(product.getPrice());
        saveOrder(order);

        // 减库存
        int updateNum = productService.decreaseProductStock(productId);
        if (updateNum <= 0){
            throw new RuntimeException("The product has been sold out.");
        }
    }

因此这里我们可以对其进行优化,通过对 SQL 语句的优化来借助数据库的锁机制保障商品的库存不会被超减(即库存被减为负数)。我们知道数据库的默认引擎为 InnoDB(上面我们在创建表的时候其实也有进行制定),它是支持数据库行锁的,这样我们就可以保证虽然多个线程在代码中的执行是并列的,但是进入到数据库后,因为行锁的存在,那么就可以保证数据的操作是串行进行的。

Update product set stock=stock-1 where id=#{id} and stock>0

其次通过 SQL 语句(如上)的执行条件我们可以看到,每次在进行减库存操作之前都会先进行库存是否大于零,是否可减的判断,如果当前的商品库存已经为零,那么退出后因为成功操作的行数为零,所以我们可以理解为减库存操作失败,即商品已经被销售完,那么这个线程就失败了, 我们通过注解 @Transactional 注解来进行整个操作的回滚即可。这样就可以保证只有有限的库存个线程可以成功的完成减库存的操作,也就可以避免了超卖的情况。

 

六、压测工具

因为对于这个项目我们是比较关注它的性能优化,也就是 TPS 的变化,所以我们需要一些相关的压测工具来帮助我们实时的进行并发访问的模拟,这里推荐使用的就是 jmeter ,它是 Apache 的一款压测工具,使用起来非常的方便,功能也十分的强大,对于它的使用方法和参数的相关配置可以直接阅读这篇博文:https://blog.csdn.net/u012111923/article/details/80705141

最后,放一张还未进行优化时本机使用 jmeter 对当前代码进行压测后生成的压测报告,用于性能优化后进行对比。 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值