# seckill-web **Repository Path**: liaozibo-dev/seckill-web ## Basic Information - **Project Name**: seckill-web - **Description**: 秒杀系统 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-03-26 - **Last Updated**: 2022-03-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: 秒杀系统 ## README * seckill-web-gateway: 对外 HTTP 接口定义(打包部署 module) * seckill-web-service: 业务逻辑 * seckill-web-common: 公用组件 源码: * 网关层:[seckill-nginx](https://gitee.com/liaozibo-dev/seckill-nginx) * 应用层:[seckill-web](https://gitee.com/liaozibo-dev/seckill-web) * 基础设施层:[seckill-support](https://gitee.com/liaozibo-dev/seckill-support) # 秒杀系统 秒杀系统项目 [TOC] ## 秒杀系统基础 ### 技术栈 前端:HTML + jQuery + axios 后端:SpringBoot + Dubbo + MyBatis + Jedis + Redis + MySQL 网关:OpenResty(Nginx + Lua) 微服务: * seckill-web:Web 接口聚合 * 秒杀应用接口 * Mock 接口:后台商品查询功能、后台活动管理功能(这两类不属于秒杀系统,仅做演示作用) * seckill-support:基础设施层(RPC 基础服务) ### 商品信息查询 商品信息查询(后台功能、不属于秒杀系统) http://seckill.com:8080/product 页面功能: * 查询商品信息:调用接口 ![product.png](doc/img/product.png) ### 活动管理 秒杀活动管理(后台功能、不属于秒杀系统、属于运营系统) http://seckill.com:8080/activity 页面功能: * 创建秒杀活动:调用接口 * 开始秒杀活动:调用接口 * 结束秒杀活动:调用接口 * 查询活动信息:调用接口(这个接口属于秒杀系统) ![activity.png](doc/img/activity.png) ### 商品详情页 商品详情页(不属于秒杀系统) http://seckill.com:8080/buy 页面功能: * 查询商品信息:调用接口 * 查询活动信息:调用接口(这个接口属于秒杀系统) * 立即抢购:点击跳转到结算页(只有活动只在进行并且库存大于 0 才能点击) ![buy.png](doc/img/buy.png) ### 结算页 结算页(属于秒杀系统) http://seckill.com:8080/settlement?code=1000 页面功能: * 提交订单:调用接口(后台扣库存,如果成功跳转到支付页,失败跳转到失败页) ![settlement.png](doc/img/settlement.png) ### 支付页与失败页 支付页(不属于秒杀系统) ![pay.png](doc/img/pay.png) 失败页(不属于秒杀系统) ![fail.png](doc/img/fail.png) ### 接口和页面总览 ```aidl // 后台-查询商品信息 // 后台-管理秒杀活动(创建活动、开始活动、结束活动) // 秒杀流程:商品详情页(立即抢购)-> 结算页(提交订单) -> 支付页/抢购失败页 // 商品详情页根据商品标识展示普通商品或秒杀商品 PAGE = { product: SECKILL_URL + "/product", // 后台-查询商品信息,不属于秒杀系统 activity: SECKILL_URL + "/activity", // 后台-管理秒杀活动,不属于秒杀系统,属于运营系统 buy: SECKILL_URL + "/buy", // 商品详情页,不属于秒杀系统 settlement: SECKILL_URL + "/settlement", // 结算页,属于秒杀系统 pay: SECKILL_URL + "/pay", // 支付页,不秒杀系统 fail: SECKILL_URL + "/fail", // 抢购失败页,不属于秒杀系统 } API = { productData: SECKILL_URL + "/product/productData", // 查询商品数据 activityData: SECKILL_URL + "/activity/activityData", // 查询活动数据 createActivity: SECKILL_URL + "/activity/createActivity", // 创建活动 startActivity: SECKILL_URL + "/activity/startActivity", // 开始活动 endActivity: SECKILL_URL + "/activity/endActivity", // 结束活动 submitOrder: SECKILL_URL + "/settlement/submitOrder", // 提交订单 } ``` ### 数据库表结构和 Redis 数据 #### 数据库表结构 商品表: ```mysql drop table if exists product; create table product ( id int unsigned auto_increment, code varchar(255) not null, -- 商品编号 name varchar(255) not null, price int unsigned not null, -- 单位:分 tag tinyint unsigned, -- 0: 普通商品 1: 秒杀商品 PRIMARY KEY (id), UNIQUE KEY (code) ); delete from product; insert into product (code, name, price, tag) values ('1000', '荣耀V40手机', 129800, 0); ``` 活动表: ```mysql drop table if exists activity; create table activity ( id int unsigned auto_increment, name varchar(255) not null, code varchar(255) not null, -- 商品编号 price int unsigned not null, -- 滑动价格,单位分 stock int unsigned not null, -- 库存 limitNum int unsigned, -- 限购 status tinyint unsigned, -- 状态:0 未开始,1 进行中,2 已结束 start_time date not null, end_time date not null, PRIMARY KEY (id), KEY (code) ); ``` 订单表: ```mysql drop table if exists order; create table `order` ( id int unsigned auto_increment, code varchar(255) not null, buy_num int unsigned not null, address varchar(1000) not null, PRIMARY KEY (id) ); ``` #### Redis 数据 `stock_`:活动库存量 ## 高性能、高可靠、高并发秒杀系统 * 隔离策略 * 独立域名 * 独立网关(Nginx 负载均衡器) * 独立应用层(Dubbo 逻辑分组) * 流量管控 * 预约 + 熔断 * 削峰手段 * 验证码/问答题 * 消息队列异步化请求 * 限流 * 网关限流(Nginx 限流语法) * 应用层限流(线程池、RateLimiter+AOP+注解) * 降级、热点数据、容灾 * 降级:读服务降级(降级开关)、写服务降级(写缓存、MQ,再异步写数据库)、非核心功能降级 * 热点数据:读热点问题(应用本地缓存、Redis 副本、CDN、浏览器缓存)、写热点问题(累计写操作、拆分大 key、限流) * 容灾:同城双活(流量随机入口、服务调用本地闭环、写主数据库/Reids再同步到从服务) * 黑产对抗 * Nginx 限流 * Nginx Token 编排 * Nginx 黑名单 * 风控 * 防超卖 * Redis + Lua 脚本 ## Nginx 限流 ``` http { # 限流规则 # $user_id 根据 user_id 限流 # limit_by_user 限流规则名称 # 10m 内存大小 # rate 请求限制 limit_req_zone $user_id zone=limit_by_user:10m rate=3r/s; server { listen 8080; set_by_lua_block $user_id { return "zhangsan" } # 活动数据查询 location /activity/activityData { # 在该接口上应用限流规则 limit_req zone=limit_by_user nodelay; proxy_pass http://backend; } } } ``` ## Nginx Token 编排 `set_common_var.lua`:设置 token 和 timestamp 变量 ```lua --[[ 访问请求参数:ngx.var.arg_ 访问cookie:ngx.var.cookie_ 访问nginx参数;ngx.var. --]] local token = ngx.var.arg_token if token == nil then token = "" end ngx.var.token = token local timestamp = ngx.var.arg_timestamp if timestamp == nil then timestamp = "" end ngx.log(ngx.ERR, "token: "..token) ngx.log(ngx.ERR, "timestamp: "..timestamp) return timestamp ``` `token_validate.lua`:token 校验 ```lua local timestamp = ngx.time() local seq = ngx.var.except_seq local token = ngx.md5(ngx.var.timestamp..seq) if tonumber(ngx.var.timestamp) + 10 < timestamp then ngx.log(ngx.ERR, "token expire") ngx.exit(403) end if ngx.var.token ~= token then ngx.log(ngx.ERR, "token ["..ngx.var.token.."] is invalid") ngx.exit(403) end ``` server 配置: ``` server { listen 8080; set_by_lua_block $user_id { return "zhangsan" } # token set $token ""; # timestamp set_by_lua_file $timestamp seckill-nginx/lua/set_common_var.lua; # 调用顺序1 # 活动数据查询 location /activity/activityData { limit_req zone=limit_by_user nodelay; proxy_pass http://backend; # 在响应头设置 token 和 timestamp header_filter_by_lua_block { local timestamp = ngx.time() ngx.header["token"] = ngx.md5(timestamp..1) ngx.header["timestamp"] = timestamp ngx.header["Access-Control-Expose-Headers"] = "token" ngx.header["Access-Control-Expose-Headers"] = "timestamp" } } # 商详情页 location /buy { proxy_pass http://backend; } # 调用顺序2;需校验 seq 1 # 结算页 location /settlement { set $except_seq 1; rewrite_by_lua_file seckill-nginx/lua/token_validate.lua; proxy_pass http://backend; error_page 403 error.html; } # 调用顺序2:需校验 seq 1 # 结算页 token location /settlement/token { set $except_seq 1; rewrite_by_lua_file seckill-nginx/lua/token_validate.lua; default_type application/json; content_by_lua_block { json = require "cjson" local t = ngx.time() local result = {} result["token"] = ngx.md5(t..2) result["timestamp"] = t ngx.say(json.encode(result)) } error_page 403 @json_error; } # 调用顺序3:需校验 seq2 # 结算页提交订单 location /settlement/submitOrder { set $except_seq 2; rewrite_by_lua_file seckill-nginx/lua/token_validate.lua; proxy_pass http://backend; error_page 403 @json_error; } # 错误页 location = /error.html { root seckill-nginx/html; } # 以 @ 开头的是内部接口,外部无法访问 location @json_error { default_type application/json; return 200 '{"code": 2, "message": "令牌失效"}'; } # 以下是非秒杀系统的路径 # 后台-商品消息查询页、商品数据接口 location /product { proxy_pass http://backend; } # 后台-活动管页、活动管理接口 location /activity { proxy_pass http://backend; } # 支付页 location /pay { proxy_pass http://backend; } # 抢购失败页 location /fail { proxy_pass http://backend; } # js location /js { proxy_pass http://backend; } } ``` ## Nginx 黑名单 `common.conf`:申请 lua 本地缓存 ``` # lua 共享缓存,进程共享 lua_shared_dict black_list 50m; ``` `black_list.lua`:黑名单实现 ```lua local list = ngx.shared.black_list -- lua 本地缓存,进程共享 local black_list = {} local count_prefix = "count_" local black_list_prefix = "black_" local threshold = 3 -- 检查用户是否在黑名单内 function black_list.contains(user_id) local value = list:get(black_list_prefix..user_id) if value == nil then ngx.log(ngx.ERR, user_id.." not in black list") return false end ngx.log(ngx.ERR, user_id.." in black list") return true end -- 生成黑名单并返回用户是否在黑名单内 function black_list.generate(user_id) -- 参数1 key,参数2 步长,参数3 默认值,参数4 过期时间 1s -- 返回值 增加后结果 local count = list:incr(count_prefix..user_id, 1, 0, 1) if count > threshold then ngx.log(ngx.ERR, user_id.." add to black list") -- 添加到黑名单 15 秒 local suc, error = list:set(black_list_prefix..user_id, 1, 15) if not suc then return false end return true end end return black_list ``` `black_list_validate.lua`:黑名单校验 ```lua local black_list = require "black_list" local user_id = ngx.var.user_id if black_list.contains(user_id) then ngx.exit(403) end if black_list.generate(user_id) then ngx.exit(403) end ``` server 配置: ``` # 调用顺序1 # 活动数据查询 location /activity/activityData { # limit_req zone=limit_by_user nodelay; access_by_lua_file seckill-nginx/lua/black_list_validate.lua; proxy_pass http://backend; header_filter_by_lua_block { local timestamp = ngx.time() ngx.header["token"] = ngx.md5(timestamp..1) ngx.header["timestamp"] = timestamp ngx.header["Access-Control-Expose-Headers"] = "token" ngx.header["Access-Control-Expose-Headers"] = "timestamp" } } ``` ## 应用层限流 注解: ```java @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface MyRateLimiter {} ``` AOP: ```java @Component @Aspect @Slf4j public class MyRateLimiterAspect { private static final RateLimiter rateLimiter = RateLimiter.create(10); @Pointcut("@annotation(com.liaozibo.demo.seckill.web.aop.MyRateLimiter)") public void pointcut(){} @Around("pointcut()") public Object around(ProceedingJoinPoint joinPoint) { boolean success = rateLimiter.tryAcquire(); if (!success) { log.error("被限流了"); return null; } Object obj = null; try { obj = joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } return obj; } } ```