- 5 台 2 核 CPU 8 G 内存机器
- 所有接口以 1s 超时作为压测基线
- 6000 万商品数据
- 500 万正常用户
- 恶意流量直接返回 403
接口 | 功能描述 |
---|---|
/product | 通过商品 ID 查询商品信息 |
/order | 下单接口返回订单号 |
/pay | 支付订单 |
/result | 查询全部订单 |
/reset | 多轮压测间 |
- 服务器1:MySQL
- 服务器2:Nginx, Tomcat
- 服务器3:Tomcat
- 服务器4:Tomcat
- 服务器5:Redis
- 服务需要支持横向扩容
- 并发资源争夺:超卖,少卖
- 服务器并发性能调优
- 反作弊防捣乱策略
- 系统,缓存预热机制
- ......
- 缓存:使用 Redis 缓存
- 预热:提前将 400 万数据装入 Redis 中,提高缓存命中率
- 缓存淘汰策略:选择了 LFU 策略
-
方案一 (master 分支)
- 通过 MQ 异步下单,订单存储至数据库中
- MQ 异常、MySQL 异常需要回滚库存
- 支付与下单间隔时间短时订单未入库:缓存订单信息
-
方案二 (redis 分支)
- 订单数据存储在 Redis 中
-
优化:售罄商品信息保存在内存中
-
订单号生成算法:时间戳,机器id,商品id
-
防止超卖
- 超卖原因
- 对单个用户超卖:同用户对同商品连续发出多个请求,成功重复下单
- 对所有用户超卖:不同用户对同一商品发出请求,将库存减为负数
- 解决方案
- 使用 Redis 原子操作以 uid 与 pid 的组合为键值设置键值对,解决同用户同商品同时下单问题
- 使用 Redis 的 DECR 方法,进行商品库存的原子性减 1 操作,小于 0 则判断为售罄
- 超卖原因
-
防止少卖
- 少卖原因
- 方案一中MQ 异常、MySQL 异常需要回滚库存
- 解决方案
- 使用 Redis + LUA 脚本来实现原子性操作
- 库存小于 0 设定为 1
- 库存大于 0 INCR
- 少卖原因
Nginx 层配置
-
针对 IP 限制单位时间访问频率和并发限制
-
空 IP、UA 为空或 Spider ,直接返回 403
-
非 GET 或 POST 方式请求,直接返回 403
-
Limit_req_zone rate 3r/s 限制同一ip每秒请求不能超过3次 Limit_conn_zone 10 限制同一ip与Nginx的连接不超过10 Limit_rate 500k 限制连接的带宽为500k
Web 服务层实现
- 黑名单机制:相同 uid 对应 ip 发生变化
- 请求链路检查:未查询商品直接下单
- 检查参数是否合法
- 请求的接口不存在
- POST 请求没有请求内容
- Session 为空、Session 不存在或者 Session 与 用户 uid 不匹配
- 各种参数的范围和格式(如用户 id、商品 id、商品 price、订单号格式)检查
- 模块功能自测
- 抢购流程测试
- 并发购买压测
- JMeter 压测
- 压测中心集群压测
- ByteCamp 2019 工程赛道三等奖(入营 Top 150 in 6000+,Team top 3 in 16 )
只是从工程化的角度去做,搭了一套 Java 工程,各种 Nginx,Tomcat,连接池性能调优,优化接口,怼代码,后续考虑到预热等优化。没有更为针对性的思考, Mentor 高估了我们...
6000 万商品数据 53 G,全是汉字,Mentor 的做法是对数据压缩,自己写存储到内存中。
golang 真香吧...
Mentor 们都很负责,优秀,ByteDance NB 啊...
项目基于 SpringBoot + MyBatis + ActiveMQ + Jeds 技术栈。
分支 Master 为使用队列异步下单的版本,Redis 为订单只保存在 Redis 的版本。
在web/src/main/resources
目录下复制一份开发配置文件application-dev.yml.example
,复制后的文件命名为application-dev.yml
。
修改配置文件中相关配置, 运行 SpringBoot 入口文件 WebApplication 中的 main 方法即可启动服务。
服务启动后访问 http://127.0.0.1:8080/test
查看 Hello World!
数据库结构及测试:database