首页 > 其他 > 详细

秒杀思路

时间:2020-08-27 09:45:48      阅读:68      评论:0      收藏:0      [点我收藏+]

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

 

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

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个并发用户在第二轮抢占时,将会优先给之前排号在前的)

 

第二种:我觉得比较简单粗暴,就是生产

秒杀思路

原文:https://www.cnblogs.com/gabin/p/13569536.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!