电商秒杀系统设计

本章主要介绍高并发业务(秒杀活动)系统是如何设计的。

设计针对于该虚拟命题:有10000件商品,每个用户最多购买2件。五分钟未付款直接退单,可手动退单。

问题

在看设计方案时我们先来整理下秒杀活动会带来什么样的高并发

  1. 秒杀开始前:可能会有大量请求商品详情页。
  2. 秒杀进行时:大量请求下单。

设计方案

请求流控

第一层 :设置浏览器缓存与CDN

商品详情页相关接口设置浏览器缓存。相关图片,CSS,JS一些静态资源存储CDN。

浏览器缓存的设计方案:设置1分钟的强制缓存,然后通过协商缓存你判断该缓存是否有效。(既能限流也能即使更新页面最新情况)

浏览器缓存:当你请求HTTP请求后收到一个HTTP响应体的时候,浏览器会判断响应头中是否有缓存的标识,如果有,则会把请求内容存入硬盘中(或者内存中),下次的准备发起相同请求时浏览器会自行判断缓存内容是否有效,如果有效则不进行请求,直接获取缓存内容。
https://zhuanlan.zhihu.com/p/29750583

CDN:把一些静态资源交由第三方平台存储。这样请求时无需访问自己的服务器并且响应速度也比较快。

第二层 :设置接口请求频率

该设置主要是过滤一些非常规可能请求(模拟请求)。

  1. 前端JS控制下单按钮不能重复点击。
  2. 通过nginx服务器限流配置使同一IP频率限制,如果超过限制则返回500状态码。

    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; 
    location /login/ {
      limit_req zone=mylimit burst=20 nodelay;
      limit_req_status 444;
    }
    // limit_req_zone 限流的配置
    // $binary_remote_addr 为用ip地址, zone=mylimit:10m 开辟一个10M的名为mylimit的空间 rate=10r/s 每秒10请求
    // limit_req //使用限流配置
    // burst=20 //接受20个缓存请求, nodelay 不延迟执行

提示业务响应速度

通过请求流程可以过滤无效请求,但是如果用户体量太大,通过流控仍有大量请求。

此时我们就需要加快程序的处理速度进而提升请求的响应速度,响应速度提升后单位时间内处理的请求就变多了。

读优化

通过redis缓存秒杀过程中会用到的数据。

写优化

解耦相关的入库操作通过消息队列中间件RabbitMQ

逻辑流程优化

及时响应无效的请求(越容易判断写在越前面)

具体实现流程

下单接口逻辑流程

//redis 相关数据的格式定义
'is_start' => 0  //  0 未开始,1开始, 2结束
'buy:'.$userID.':'.$goodID => 0 // userID用户已购买goodID商品的数量
'stock'.$goodID => {
    'stock' => 10000 // 商品库存量
    'sales' => 0 //已售量
}
'order':$userID:$order_no => {
     订单信息
}
  • 判断秒杀活动是否开启
  • 判断用户是否还能购买 checkBuy 脚本返回0表示不能购买
  • 判断库存是否满足 checkStock 脚本返回0表示无库存,无库存的时候要把用户已购买数量添加回去
  • 将订单请求放入下单队列,并且响应订单号给前端。 消息队列内容 用户id, 商品id,商品数量,订单号。
  • 异步消费下单队列,写入数据库,并且写入redis 'order':$order_on。
    • 如果成功redis存储订单信息,并且添加过期队列延迟5分钟。过期队列内容 用户id, 商品id,商品数量,订单号。
    • 如果失败存储失败原因(释放库存 'stock'.$goodID,释放已购买数 'buy:'.$userID.':'.$goodID )。
      *异步消费过期队列,如果已过期则更改订单状态,释放库存 'stock'.$goodID,释放已购买数 'buy:'.$userID.':'.$goodID

获取订单接口逻辑流程

  • 通过请求的order_no与用户身份标识去redis 查询 'order':$userID:$order_no数据并返回。

取消订单与退单接口

  • 更改订单状态,释放库存 'stock'.$goodID,释放已购买数 'buy:'.$userID.':'.$goodID

前端接收下单响应结果处理

  • 如果响应状态吗!200, 提示抢购失败

  • 如果是200响应,下单接口会返回order_no。前端可以展示订单创建中。

  • 然后ajax去轮询获取订单接口,请求参数order_no.获取redis信息。如果成功则展示结算页。如果失败则展示失败原因。

结算页

  • 支付选择收货地址并且进行支付。

订单页

  • 可以进行取消订单,退单

服务器架构 (暂缓)

nginx负载均衡配置代码

 upstream mysvr {   //服务器池和权重配置
      server IP1:PORT weight=1;
      server IP2:PORT weight=1;
      server IP3:PORT weight=1;
      server IP4:PORT weight=1;
      server IP5:PORT weight=1;
}

location / {  
    add_header Cache-Control no-cache;  //设置响应头,这里是设置不使用浏览器缓存
    proxy_set_header   Host $host;  //设置请求目的主机名
    proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for; //设置发起请求的IP
    proxy_set_header   X-Real-IP        $remote_addr;  //设置客户端的IP
    proxy_pass  http://mysvr;  //使用服务器进行负载均衡
    proxy_connect_timeout 30s; //请求超时时间
 }

lua 脚本

//checkBuy脚本
    script load "lua code"
    //lua code
    local n =  tonumber(ARGV[1]);
    if not n  or n == 0 then
        return 0       
    end    
    local val = tonumber(redis.call('GET', KEYS[1]));        
    if  (not val) or (val + n <= 2) then
        redis.call('INCRBY', KEYS[1], n);                 
        return n;   
    end                
    return 0;
    //49d185704d033c30e29b09a72e687d4c322e7801
    evalsha 49d185704d033c30e29b09a72e687d4c322e7801 1 'buy:'.$userID.':'.$goodID 1
//checkStock脚本
    script load "lua code"
    //lua code
    local n = tonumber(ARGV[1])
    if not n  or n == 0 then
        return 0       
    end                
    local vals = redis.call('HMGET', KEYS[1], 'stock', 'sales');
    local stock = tonumber(vals[1])
    local sales = tonumber(vals[2])
    if not stock or not sales then
        return 0       
    end                
    if sales + n <= stock then
        redis.call('HINCRBY', KEYS[1], 'sales', n)                                   
        return n;   
    end                
    return 0
    //51046114c9b5b102554a381969343493e29dcb34
    evalsha 51046114c9b5b102554a381969343493e29dcb34 1 'stock'.$goodID 1
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!