• 秒杀思路


    因为做过电子商城类型的项目,所以经常问这个问题

    首先这是一个具体的使用场景,角色:

    1、用户(N)

    2、商城系统-订单模块(1)

    这边体现的首先是一个多对一关系

    具体的功能点:

    1、用户下单

      1.1、锁住库存成功

      1.2、已无库存

    2、订单超时或取消,库存恢复

    3、订单支付成功(这边认为在秒杀场景中,支付成功,可以权且当做一个结束点,不再考虑退款的问题)

    可以看出库存是一个关键词

    那么上面是一个比较理想和正常的业务场景,而实际生产环境中呢?

    在互联网时代,面对的经常都是几亿的网民流量,也就是说我们可能要面临几万个人抢100个库存的场景

    也就是说我们需要为秒杀考虑并发场景

    并发一般需要考虑的有三个比较大的概念性问题

    1、安全性问题:指错误的结果不会发生,这边对应的是超卖

    2、活跃性问题:指正确的事情会发生,这边指用户可以在抢到库存后成功下单并支付

    3、性能问题:指正确的事情可以尽快发生,这边指抢单不会像以前抢火车票一样,抢了一两个小时才抢到

    一、在传统单机部署系统中

    这边主要针对如何做库存管理做描述

    1、利用AtomicInteger,原子变量做库存管理

    如下:

    package com.example.demo;
    
    import org.junit.jupiter.api.Test;
    
    import java.util.concurrent.LinkedBlockingDeque;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class Test1 {
    
        static class StockManager {
            final int stockTotal;
            final AtomicInteger stockAtomic;
    
            public StockManager(int stock) {
                stockTotal = stock;
                this.stockAtomic = new AtomicInteger(stock);;
            }
    
            public boolean lockStock(int stock) {
                int currentStock = stockAtomic.get();
                int newStock = currentStock - stock;
                if (newStock < 0) { // 如果库存不足,直接返回失败
                    return false;
                } else {
                    return stockAtomic.compareAndSet(currentStock, newStock);
                }
            }
    
            public boolean unLock(int stock) {
                int currentStock = stockAtomic.get();
                int newStock = currentStock + stock;
                if (newStock > stockTotal) {
                    // 其实这个不严谨,如果存在售卖期间,自行调整库存的情况,就会有问题,但这个问题域会更复杂点,在这里先不考虑
                    // 这里更应该抛出异常
                    return false;
                } else {
                    return stockAtomic.compareAndSet(currentStock, newStock);
                }
            }
    
        }
    
    
    
    
        @Test
        public void test() {
            final StockManager stockManager = new StockManager(100);
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100, 150, 1, TimeUnit.MINUTES, new LinkedBlockingDeque());
            for (int i = 0; i < 10000; i++) {
                int finalI = i;
                threadPoolExecutor.submit(() -> {
                    if (stockManager.lockStock(1)) {
                        System.out.println("客户" + finalI + "成功抢占库存");
                        if (finalI % 2 == 0) {
                            stockManager.unLock(1);
                            System.out.println("客户" + finalI + "放弃库存");
                        }
                    }
                });
            }
            // 不再接受新的任务,执行完之后关闭
            threadPoolExecutor.shutdown(); 
            while (!threadPoolExecutor.isShutdown()) {
    
            }
        }
    
    }

    执行完之后会发现抢占成功的次数-放弃库存的次数等于100.

    这边实际用到的是硬件级别的锁

    首先我们要确定一点,原子变量在JVM、Java代码的层级是没有锁机制的,它应用的是一种CAS的机制,完整的单词是Compare And Set(也有做Compare And Swap,但我看Java中暴露的方法是前者)

    意思大概就是先比较再设置,那么了解并发的人就会意识到,先比较后设置,和先检查再执行,都是属于两个操作,是没有原子性的。

    那么为什么这个变量又叫做原子性变量呢。

    如果我们到JVM的源码中去看的话(因为在JDK中,只能看到最终调用的是一个本地方法接口),应该能看到最终这个方法调用的是C++中的一个方法,那么重点是这个方法应用的是CPU原语操作。

    简单来说的话,就是CPU保证这个操作是原子性的,现代CPU基本上都支持CAS操作的原子性

    ps:由此衍生的另外一个问题是ABA问题,这边ABA只是随便取的,重点要描述的是:一个变量,比如从5 到 10 再到5,看起来值是没变的,但实际中中途是变过的。

    也就是说理论上CAS存在过程不一致,只能保证最终一致。

    另外我们可以看出CAS实际上是一种乐观锁机制,那么与我们常规关系型数据库中使用的版本式的乐观锁不同,这边的版本号取自变量原值,所以存在上面的CAS问题。也就是说一般要解决ABA问题,版本号需要相对独立(可以使用带版本号的AtomicStampedRefence)

    使用带版本号的乐观锁的改进版本

    package com.example.demo;
    
    import org.junit.jupiter.api.Test;
    
    import java.util.concurrent.LinkedBlockingDeque;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicStampedReference;
    
    public class Test1 {
    
        static class StockManager {
            final Integer stockTotal;
            final AtomicStampedReference<Integer> stockAtomic;
    
            public StockManager(Integer stock) {
                stockTotal = stock;
                this.stockAtomic = new AtomicStampedReference(stock, 1);;
            }
    
            public boolean lockStock(Integer stock) {
                Integer stamp = stockAtomic.getStamp();
                Integer currentStock = stockAtomic.getReference();
                Integer newStock = currentStock - stock;
                if (newStock < 0) { // 如果库存不足,直接返回失败
                    return false;
                } else {
                    return stockAtomic.compareAndSet(currentStock, newStock, stamp, stamp + 1);
                }
            }
    
            public boolean unLock(Integer stock) {
                Integer stamp = stockAtomic.getStamp();
                Integer currentStock = stockAtomic.getReference();
                Integer newStock = currentStock + stock;
                if (newStock > stockTotal) {
                    // 其实这个不严谨,如果存在售卖期间,自行调整库存的情况,就会有问题,但这个问题域会更复杂点,在这里先不考虑
                    // 这里更应该抛出异常
                    return false;
                } else {
                    return stockAtomic.compareAndSet(currentStock, newStock, stamp, stamp + 1);
                }
            }
    
        }
    
    
    
    
        @Test
        public void test() {
            final StockManager stockManager = new StockManager(100);
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100, 150, 1, TimeUnit.MINUTES, new LinkedBlockingDeque());
            for (Integer i = 0; i < 10000; i++) {
                Integer finalI = i;
                threadPoolExecutor.submit(() -> {
                    if (stockManager.lockStock(1)) {
                        if (finalI % 2 == 0) {
                            stockManager.unLock(1);
                            System.out.println("客户" + finalI + "放弃库存");
                        } else {
                            System.out.println("客户" + finalI + "成功抢占库存");
                        }
                    }
                });
            }
            // 不再接受新的任务,执行完之后关闭
            threadPoolExecutor.shutdown();
            while (true) {
            }
        }
    
    }

    ** 需要注意的事抢占成功不一定是100。。因为可能在版本冲突过程中,线程已经全部执行完成。。。。这个就是和自旋锁不断重试的机制不同的地方(后者会不断取出当前值做比较)

    与我们一般使用的乐观锁还有的不同是,这边的CAS实际上是一种自旋锁机制,即不断重试,由此衍生的问题也就是说,竞争情况很多的时候会导致CPU性能瓶颈(只能说适用场景不同,并不能说自旋锁不适用于高并发场景)

    **************************************切割下,现代网站系统通常都是分布式的,所以理论上这些都没办法直接被应用***************************************

    通常我们可能会采用一种单机服务机制,比如说像授权服务,我们会有统一的一个服务,虽然可能会有多机部署,但从使用上,会保证一个登陆记录的唯一性

    所以分布式中,对于秒杀的一般思路,很多面试官提及或者想要问的是队列。我自己大概也有几种思路(还没参考过网上的,所以也不知道这几个思路是不是有可取的)

    第一种:

    1、类似于我去大丰收吃饭,高峰期,取个号等就是了,门口就不会被堵住。

    那么如何实现取号呢?

       - 在redis中预热加载库存信息,并初始化比如说100张的入场券

     - 提交订单的时候先到叫号机服务中获取一个编号,这个叫号机采用的是无边界队列模式(另外可以参考有边界的,通常是100个长度,最多取100个号,其他人直接拒绝),这个号码被绑定到登录信息中

      ps:这边其实可以在这个叫号机上面做扩展,相当于流量控制算法

     - 服务器后台自动轮询这个队列,一但入场券池中有券,就绑定到具体的号码(其实相当于关联到某个用户);超过一定时间没有被响应(消费)就清除绑定关系,被取消排号的有效性,将入场券重新返回池中

      ps:redis中使用lua脚本控制事务性,保证入场券的ACID特性

     - 用户发现没有抢成功,但还显示有库存的时候,重新点击提交订单,此时叫号机发现已经交过号了,便不再排号,而是继续等待(等待服务器轮询入场券池,有空余的时候按照顺序绑定到叫号机的队列的具体元素中)

      ps:如果此时用户第一次抢失败,但是第二次点击的时候,入场券池的后台服务已经给此用户分配了券,则可以提交成功成功

     - 用户一旦取消订单,则入场券会重新回到池中

    上面的这个做法大致体现了公平性(如果100个并发用户在第二轮抢占时,将会优先给之前排号在前的)

    第二种:我觉得比较简单粗暴,就是生产和消费者的关系

      1、用户提交订单的时候,把对应的信息放到消息队列中(这里是生产)

      2、商城的后台系统直接从队列中轮询,一一消费,这里我可以直接操作关系型数据库了,因为队列的执行保证有序和一定时间内的处理数量

      3、如果商城后台成功消费了前台用户的订单信息,则会在消息队列中另外推送一条订单创建成功的信息

      4、前台用户通过同步(前端等待抢单成功中)或者异步(收到微信通知抢单成功)进入订单支付页面,相当于消费掉这条订单创建成功的消息

    综合考虑:

    个人认为一般在现代的分布式系统中会有如下几个考虑

    1、利用网关的特性,限制指定时间内流量的数量,这个和服务没有任何直接关系,用的特性也是队列的机制;可以考虑线程池任务队列满的时候的执行策略(比如说限制1分钟内10000个访问量,那么多过的部分怎么处理,是直接拒绝,还是等待,还是分发一个类似于叫号的方式)

    2、使用生产+消费的双工流程,保证操作的有序性,且拆解出不同的操作步骤;比如下单和库存锁定,我把它切割开,各自集中处理各自的逻辑(在这边的思路中可能下单的逻辑没有深入,所以看上去只是序列化下下单信息而已)

  • 相关阅读:
    实验8 SQLite数据库操作
    实验7 BindService模拟通信
    实验6 在应用程序中播放音频和视频
    实验6 在应用程序中播放音频和视频
    实验5 数独游戏界面设计
    实验4 颜色、字符串资源的使用
    实验五存储管理实验
    实验四:主存空间的分配和回收
    实验三 进程调度模拟程序
    实验8 SQLite数据库操作
  • 原文地址:https://www.cnblogs.com/gabin/p/13569536.html
Copyright © 2020-2023  润新知