秒杀系统简单实现

92 阅读3分钟

一般Web基本系统涉及的方面

  • 用户
  • 前端
  • 服务
  • 数据库

秒杀系统的特点

  1. 高并发 有很多用户同时请求秒杀接口
  2. 事务的特点
  3. 一般是对单一的商品进行库存扣除

我们可以改变的地方

  • 前端
  • 服务
  • 数据库

秒杀流程

用户发起秒杀请求,请求到达后端服务器。

首先,检查用户是否已经参与过秒杀,如果是,则拒绝请求或给出相应提示。

然后,检查商品库存数量是否大于0,如果不是,则拒绝请求或给出相应提示。

如果上述检查通过,说明用户可以参与秒杀,开始执行以下操作:

-   生成一个唯一的订单号,用于标识秒杀订单。
-   减少商品库存数量。
-   创建秒杀订单并写入订单表。
-   返回秒杀成功的响应给用户。

秒杀问题点

  1. 服务器是否可以抗住突发流量
  2. 数据库是否可以抗住这个流量
  3. 怎么保证库存可以安全的扣除 不会出现超卖问题 也就是并发控制问题

本质

  1. 在链路中尽可能的来拦截不需要的请求 因为越到后面的链路 抗突发流量能力低
  2. 来增大组件的处理请求的能力 这里延迟 增大都算处理

服务器

  1. 对流量进行缓冲 用MQ进行处理
  2. 采用分布式架构
  3. 对单机进行限流 熔断或者降级

数据库

1.可以使用库存前置 提前到redis上 扣除库存的操作的redis+lua 来解决并发控制问题 再秒杀结束之前定时任务进行一个对数据库的写入

结构图

未命名文件.jpg

简单实现

package ms;

import lombok.SneakyThrows;

import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    /**
     * 限流数
     */
    private static final Integer limit = 5;

    /**
     * 设置秒杀商品 库存为10 商品Id是1
     */
    private static final Product product = new Product(10,1);

    /**
     * 并发用户数
     */
    private static final Integer USERNUM = 100;

    /**
     * 初始化请求数 
     */
    private static final AtomicInteger count = new AtomicInteger(0);

    /**
     * 队列实现
     */
    private static final LinkedBlockingQueue<Callable<Boolean>> queue = new LinkedBlockingQueue<>();

    /**
     * 去重处理
     */
    private static final HashSet<Integer> uSet = new HashSet<>();

    /**
     *  reids锁
     */
    private static final HashMap<Integer,Object> lock = new HashMap<>();

    /**
     * 填充对象
     */
    private static final Object objects = new Object();

    /**
     * 成功抢到的用户
     */
    private static final List<Integer> successList = new LinkedList<>();

    /**
     * 失败抢到的用户
     */
    private static final Set<Integer> failSet = new HashSet<>();
    
    
    public static void main(String[] args) {

        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10,100,10, 
                TimeUnit.SECONDS,new LinkedBlockingQueue<>());

        // 模拟 MQ 监听
        poolExecutor.execute(Main::processQueue);
        
        // 模拟用户秒杀
        for (int i = 0;i < USERNUM;i++) {
            
            // 模拟不同用户id
            int finalI = new Random().nextInt(100);
            
            poolExecutor.execute(()->{
                try {
                    seckill(1, finalI,false);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        
    }
    
    @SneakyThrows
    public static void processQueue() {
        try {
            while (true) {
                // 检查队列是否有元素
                System.out.println("queue size = " + queue.size() + " stock = " + product.getStock());
                if (!queue.isEmpty()) {
                    // 处理队列中的元素
                    Callable<Boolean> element = queue.take();
                    element.call();
                }

                // 可以添加一些适当的延迟,避免过于频繁地检查队列
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    
    public static boolean seckill(Integer productId, Integer uid,boolean isDeque) throws InterruptedException {
        count.getAndIncrement();
        // 限制流量 超过流量的使用队列处理 对流量进行缓冲
        if (count.intValue() > limit && !isDeque) {
            queue.put(() -> seckill(productId,uid,true));
            return false;
        }
        
        // 模拟redis+lua脚本 synchronize来模拟lua的原子性
        synchronized (objects) {
            // 加锁
            Object loc = lock.get(productId);
            
            if (Objects.isNull(loc)) {
                return false;
            }
            
            lock.put(productId,objects);
            
            // 去重
            if (uSet.contains(uid)) {
                return false;
            }
            
            // 检查库存
            if (product.getStock() <= 0) {
                failSet.add(uid);
                return false;
            }
            
            // 先成订单 
            Order order = new Order();
            order.setProduct(productId);
            order.setUid(uid);
            order.setOrderId(String.valueOf(UUID.randomUUID()));

            // 扣除库存
            product.setStock(product.getStock()-1);

            uSet.add(uid);

            successList.add(uid);

            // 解放锁
            lock.remove(productId);

            System.out.println("用户Id为: " + uid + "抢到商品: " + productId);
            return true;
        }
        
    }
}