电商秒杀系统设计 
                                                    
                        
                    
                    
  
                    
                    本章主要介绍高并发业务(秒杀活动)系统是如何设计的。
设计针对于该虚拟命题:有10000件商品,每个用户最多购买2件。五分钟未付款直接退单,可手动退单。
问题
在看设计方案时我们先来整理下秒杀活动会带来什么样的高并发
- 秒杀开始前:可能会有大量请求商品详情页。
 - 秒杀进行时:大量请求下单。
 
设计方案
请求流控
第一层 :设置浏览器缓存与CDN
商品详情页相关接口设置浏览器缓存。相关图片,CSS,JS一些静态资源存储CDN。
浏览器缓存的设计方案:设置1分钟的强制缓存,然后通过协商缓存你判断该缓存是否有效。(既能限流也能即使更新页面最新情况)
浏览器缓存:当你请求HTTP请求后收到一个HTTP响应体的时候,浏览器会判断响应头中是否有缓存的标识,如果有,则会把请求内容存入硬盘中(或者内存中),下次的准备发起相同请求时浏览器会自行判断缓存内容是否有效,如果有效则不进行请求,直接获取缓存内容。
https://zhuanlan.zhihu.com/p/29750583CDN:把一些静态资源交由第三方平台存储。这样请求时无需访问自己的服务器并且响应速度也比较快。
第二层 :设置接口请求频率
该设置主要是过滤一些非常规可能请求(模拟请求)。
- 前端JS控制下单按钮不能重复点击。
 - 
通过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 协议》,转载必须注明作者和本文链接
          
 wuzhenhuai 的个人博客
        
          
          
                关于 LearnKu