zoukankan      html  css  js  c++  java
  • 秒杀思路

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

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

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

  • 相关阅读:
    POJ-1189 钉子和小球(动态规划)
    POJ-1191-棋盘分割(动态规划)
    Java实现 LeetCode 730 统计不同回文子字符串(动态规划)
    Java实现 LeetCode 730 统计不同回文子字符串(动态规划)
    Java实现 LeetCode 729 我的日程安排表 I(二叉树)
    Java实现 LeetCode 729 我的日程安排表 I(二叉树)
    Java实现 LeetCode 729 我的日程安排表 I(二叉树)
    Java实现 LeetCode 728 自除数(暴力)
    Java实现 LeetCode 728 自除数(暴力)
    Java实现 LeetCode 728 自除数(暴力)
  • 原文地址:https://www.cnblogs.com/gabin/p/13569536.html
Copyright © 2011-2022 走看看