第七节 用户商城抢单并发实战(流量削峰实战)

一、基本思路

         数据库有一张商品表,库存量是100。现在有1000个消费者准备开抢这100个库存。

         t_product表维护商品编号与商品库存剩余数量。编号No123321的这种商品的库存量有100个。

第七节 用户商城抢单并发实战(流量削峰实战)_第1张图片

         t_product_record维护抢到商品的用户ID。理论上t_product表开抢后的 记录数量应该是100条(共有100个人抢到了商品)。

第七节 用户商城抢单并发实战(流量削峰实战)_第2张图片

         我们使用线程池创建1000个线程,模拟一千个人同时开抢。 

二、遇到的问题

        构造的一千个线程,同时操作数据库,可能将库存表的total字段变成负的。

        在下面的实测过程中,发现了100库存最终变成了-4,这是有问题的。

        其中的核心原因是:在抢单方法中,既有读数据库操作,又有写数据库操作。A线程从数据库查到的Product的库存数量是1,B线程同样是查到了是1。结果两个线程都执行了更新库存减一的操作,那么库存量就变成了 -1 了。实际情况会更糟糕。

        下面是有问题的代码。

    /**
     * 抢单方法实现
     *
     * @param userId 抢单用户id
     */
    @Override
    public void robbingProduct(int userId) {
        //先查询商品
        Product product = productDao.selectProductByNo(PRODUCT_NO);
        if (product != null && product.getTotal() > 0) {
            //原因:多个线程可能同时进入此方法体
            //再更新库存表
            productDao.updateProduct(PRODUCT_NO);
            //插入记录
            productDao.insertProductRecord(new ProductRecord(PRODUCT_NO, userId));
            //发送短信
            LOGGER.info("用户{}抢单成功", userId);
        } else {
            LOGGER.error("用户{}抢单失败", userId);
        }
     }

三、解决办法

         并发出现的问题,一般情况下,可以从 SQL角度代码角度中间件角度 来解决。

SQL优化

         在更新库存的代码后面,追加 AND total > 0 

         追加的这个total大于0的条件非常重要。在mysql数据库中,它会让我们的t_product表的库存total字段,更新到0为止。

    
        UPDATE t_product SET total = total - 1 where productNo = #{productNo}
        
        AND total > 0
    
    

代码优化

 int updateResult = productDao.updateProduct(PRODUCT_NO);

         在SQL优化中,如果total>0条件不成立,也就是说库存量total字段的值已经到了0。

         因此,当上述代码的执行结果的返回值是0的时候,说明更新失败,数据库中total字段已经为0,商品已经被抢光。

            Product product = productDao.selectProductByNo(PRODUCT_NO);
            if (product != null && product.getTotal() > 0) {
                //更新库存表,库存量减少1。返回1说明更新成功。返回0说明库存已经为0
                int updateResult = productDao.updateProduct(PRODUCT_NO);
                //如果商品没被抢光
                if (updateResult > 0) {
                    //插入记录
                    productDao.insertProductRecord(new ProductRecord(PRODUCT_NO, userId));
                    //发送短信
                    LOGGER.info("用户{}抢单成功", userId);
                } else {
                    LOGGER.error("用户{}抢单失败", userId);
                }
            } else {
                LOGGER.error("用户{}抢单失败", userId);
            }

          优化后效果:

 四、使用RabbitMQ进行流量削峰

         最后一个问题是,如果并发量实在太大,会给我的应用程序带来非常大的压力。

         首先因为频繁创建对象,对我们的堆内存造成压力。GC需要频繁销毁对象,对GC的压力也很大。

         其次对数据库的压力也很大。

         解决办法就是,把用户的抢单请求发送到RabbitMQ消息中间件中。因为RabbitMQ是一个消息队列,队列会按照先进先出的特点进行操作。RabbitMQ服务器一般部署在另外一个电脑上,所以就把这个并发压力转移到了另外电脑的RabbitMQ服务器上,而不是我们的抢单应用程序。

         使用RabbitMQ的最主要变化就是:以前抢单操作请求直接由我们抢单应用程序执行,现在请求被转移到了RabbitMQ服务器中。RabbitMQ服务器把接收到的抢单请求进行排队,最后由RabbitMQ服务器把抢单请求转发到我们的抢单应用程序,这样的好处就是避免我们的抢单应用程序短时间直接处理大量请求。RabbitMQ服务器主要作用是减缓抢单应用程序的并发压力,相当于在我们的抢单程序之前加了一道请求缓冲区。

第七节 用户商城抢单并发实战(流量削峰实战)_第3张图片

         配置后的效果预览

         RabbitMQ服务器的集成,也可参考上一节:第六节 SpringBoot集成RabbitMQ综合运用(SSM框架集成RabbitMQ)

五、源码下载

        源代码地址:https://github.com/hairdryre/Study_RabbitMQ

        下一篇:第八节 使用RabbitMQ异步解耦(提高性能)

        阅读更多:从头开始学RabbimtMQ目录贴

你可能感兴趣的:(跟着大宇学RabbitMQ)