电商秒杀系统设计
本章主要介绍高并发业务(秒杀活动)系统是如何设计的。
设计针对于该虚拟命题:有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 协议》,转载必须注明作者和本文链接