LOADING

加载过慢请开启缓存 浏览器默认开启

秒杀系统设计

秒杀系统设计

传统系统架构设计

其中Nginx只做反向代理和负载均衡,Tomcat负责处理请求,MySQL负责存储数据,Redis负责缓存数据。运维负载生产环境的搭建,研发负责WEB服务和RPC服务的开发,测试负责测试用例的编写和测试环境的搭建。

在ToB场景下可以正常完成,但是在ToC场景下,用户量会非常大,活动开启瞬间,带宽资源很稀缺的情况下,可能会出现用户进不了结算页,或者进了结算页却不能正常渲染页面的问题,导致抢购体验大幅下降。

秒杀系统架构设计

首先依然保留了HTTP服务常用的层级调用关系,也就是Nginx->Web服务->RPC服务,其次将原先有Web服务提供的静态资源放到CDN上,来大大减轻抢购瞬时秒杀域名的压力

NGINX的优点:模块化,事件驱动,异步,非阻塞,多进程单线程

nginx是由一个master进程和多个worker进程来配合完成工作的,master进程主要负责加载配置文件和管理worker进程,worker进程主要负责处理请求。nginx的优点是模块化,事件驱动,异步,非阻塞,多进程单线程。每个进程中只有一个线程,这就省去了并发情况下的加锁以及线程切换带来的性能损耗

Nginx的工作模型。以Linux为例,其采用的是epoll模型(即事件驱动模型)该模型是IO多路复用思想的一种实现方式,是异步非阻塞的,什么意思呢?就是一个请求进来后,会由个worker进程去处理,当程序代码执行到IO时,比如调用外部服务或是通过upstream分发请求到后端Web服务时,IO是阻塞的,但是worker进程不会一直在这等着,而是等 IO 有结果了再处理,在这期间它会去处理别的请求,这样就可以
充分利用 CPU 资源去处理多个请求了。

Web/RPC服务技术选型

SpringMVC+Dubbo+MySQL+Redis

首先至少有3个系统服务

  1. Nginx服务:

    流量筛选:根据黑白名单,登录态和参数有效性等进行流量筛选,将无效的请求拦截在Nginx层,减少无效请求对后端服务的压力。
    流量分发:通过设置的负载均衡算法进行流量分发,也可以自定义算法,比如根据IP做hash分发或者根据用户ID做hash分发,这样可以保证同一个用户的请求都会分发到同一个后端服务上,从而保证秒杀的一致性。
    简单业务以及校验:提供活动数据、活动有效性校验、库存数量校验和其他业务相关的简单校验等。
    限流:根据IP或者自定义参数进行限流,比如限制每秒钟只能访问一次,或者每秒钟只能访问N次,或者每秒钟只能访问N个商品等。
    异常提示页面:当后端服务出现异常时,可以通过Nginx服务返回一个友好的提示页面,比如活动还未开始,活动已经结束,活动已经售罄等。

  2. Web服务

    提供结算页H5:灰色发布,项目功能一般不是全量发布的,而是有计划第逐步灰度上线,这样可以保证系统的稳定性
    业务聚合接口
    其他功能

  3. RPC服务

    提供基础服务,数据支持,包括活动数据、商品数据、用户维度数据、提单等,主要模拟基础服务,正常情况下,应该是按业务模块做细致划分的

demo-web服务设计

  1. 基于SpringMVC框架,新建三个子模块:gateway,common,service
  2. gateway:对外HTTP接口定义,以及SpringMVC相关文件配置
  3. service:业务逻辑实现,包括秒杀活动数据查询,活动有效性校验,库存数量校验,提单等
  4. common:用来存放公共的类,比如常量类,工具类,异常类等

当web项目搭建完成后,就可以和我们之前搭建的nginx项目联动一下了,即请求先到 Nginx,被其配置的location拦截,经过处理后将其分发给web。所以接下来我们需要对 nginx做一些修改。

  1. 在config文件夹中,新建一个upsteam.conf文件,用来配置web服务的地址和端口
upstream backend {
    server 127.0.0.1:8080;
}
  1. 修改domain.com中的server配置文件,新加一个proxy_pass配置,用来将请求分发给web服务
server {
    listen 7081;
    localtion /sayhello {
        default_type text/plain;
        proxy_pass http://backend;
    }
}
  1. 在nginx.conf中引入upstream.conf文件
worker_processes  1;
err_log logs/error.log error;
events {
    worker_connections  256;
}
http {
    lua_package_path "/usr/local/Documents/seckillproject/demo-nginx/lua/?.lua;;";
    include /usr/local/Documents/seckillproject/demo-nginx/domain/domain.com;
    include /usr/local/Documents/seckillproject/demo-nginx/upstream.conf;
}
  1. 重启nginx服务

demo-rpc服务设计

新建5个子模块:common,service,dao,export,launcher

common 存放公共类,比如常量类,工具类,异常类等
dao:存放数据访问层代码,比如数据库访问层代码,redis访问层代码等
export:存放对外提供的接口,比如dubbo接口,http接口等
service:存放业务逻辑代码,比如秒杀活动数据查询,活动有效性校验,库存数量校验,提单等
launcher:项目文件配置,监控,拦截器等,同时也是打包部署的入口

秒杀项目流程

所以秒杀系统的主要接口有:

  1. 活动数据查询接口:查询活动相关信息,包括开始,结束时间
  2. 进结算页页面接口:结算页H5,并通过AJAX异步加载结算页数据
  3. 结算页页面初始化渲染锁需数据的接口:大体上包括活动信息,商品信息,结算信息等
  4. 结算页页面用户行为操作接口:支持地址列表查看和选择,虚拟资产的查看和使用等,并在操作后更新页面价格相关信息
  5. 结算页提交订单接口:支持秒杀活动商品下单

因此数据库需要如下三张表:

上述接口配置到Nginx中,为此修改domain.com中的server配置文件,新加一个proxy_pass配置,用来将请求分发给web服务

server {
    listen 7081;
    localtion /sayhello {
        default_type text/plain;
        proxy_pass http://backend;
    }
    # 活动数据查询
    location /activity/query {
        default_type text/plain;
        proxy_pass http://backend;
    }
    # 进结算页页面(H5)
    location /settlement/page {
        default_type text/plain;
        proxy_pass http://backend;
    }
    # 结算页页面初始化渲染所需数据
    location /settlement/initData {
        default_type text/plain;
        proxy_pass http://backend;
    }
    # 结算页提交订单
    location /settlement/submitData {
        default_type text/plain;
        proxy_pass http://backend;
    }
    # 结算页用户行为操作,模糊匹配
    location ~*/useAction/ {
        default_type text/plain;
        proxy_pass http://backend;
    }
}

隔离策略

  1. 业务隔离:由于秒杀商品的稀缺性,决定了业务不会像普通商品那样进行投放售卖,一般是有计划的进行营销策划,所以会有个申报的过程,有了申报的基本信息来预估活动当天的流量,并发数,以及活动的时长,从而来决定是否需要对业务进行隔离,比如是否需要单独部署,是否需要单独的数据库,是否需要单独的缓存等。
  2. 系统隔离:我们知道,用户的秒杀习惯,一般是打开商品详情页进行倒计时等待,时间到了点击秒杀按钮进行抢购。因此第一个需要关注的系统就是商品详情页,我们需要申请独立的秒杀详情页域名,独立的 Nginx 负载均衡器,以及独立的详情页后端服务,并采用 Dubbo 独立分组的方式单独提供秒杀服务
  3. 数据隔离:在数据层面,我们也应该进行相应的隔离,否则如果共用缓存或者共用数据库,一旦瞬时流量把它们冲垮,照样会影响无辜商品的交易。数据层的专有部署,需要结合秒杀的场景来设计部署拓扑结构,比如 Redis 缓存,一般的场景一主一从就够了,但是在秒杀场景,需要一主多从来扛读热点数据。

如何让秒杀流量正确地路由到秒杀系统专有环境中

通过对商品进行打标,在商品的主数据上增加一个秒杀标识,比如秒杀商品的商品ID前面加上秒杀标识,这样就可以通过商品ID来判断是否是秒杀商品,从而将秒杀商品的请求路由到秒杀系统专有环境中。打标的基本思路:我们可以使用一个 long 型字段 skuTags 来保存,long 是 64 位,每一位代表一种类型的活动,0 代表否,1 代表是,通过对 skuTags 进行二进制操作即可完成商品的打标和去标。假设秒杀的标识我们定义在 skuTags 的第 11 位,那么要给一个sku 打上秒杀标,我们就可以对这个标实际进行“或”操作:skuTags=skuTags|1024,这样 skuTags 字段的第 11 位就变成了 1,对其它 bit 位没影响。去标过程相反,同样进行位操作,skuTags=skuTags&~1024,把第 11 位置为 0。

流量管控

  1. 活动前期采用预约制:
    因此我们需要一个预约管理后台,进行活动的设置和关闭;需要一个预约 worker 系统,根据时间调用商品系统进行预约打标和去标,向预约过的用户发短信或消息提醒;还需要一个面向 C 端的预约核心微服务,提供给用户预约和取消预约能力,商详在展示时获取预约信息的能力,秒杀下单时检查预约资格的能力,以及获取用户的预约列表能力
    为此设计数据库表:
CREATE TABLE `t_reserve_info` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '预约活动id',
`sku_id` bigint(20) unsigned DEFAULT NULL COMMENT '商品编号',
`reserve_start_time` datetime DEFAULT NULL COMMENT '预约开始时间',
`reserve_end_time` datetime DEFAULT NULL COMMENT '预约结束时间',
`seckill_start_time` datetime DEFAULT NULL COMMENT '秒杀开始时间',
`seckill_end_time` datetime DEFAULT NULL COMMENT '秒杀结束时间',
`creator` varchar(255) DEFAULT NULL COMMENT '活动创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`yn` tinyint(255) unsigned DEFAULT NULL COMMENT '是否删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_reserve_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '关系id',
`reserve_info_id` bigint(20) unsigned DEFAULT NULL COMMENT '预约活动id',
`sku_id` bigint(20) unsigned DEFAULT NULL COMMENT '商品编号',
`user_name` varchar(255) DEFAULT NULL COMMENT '用户名称',
`reserve_time` datetime DEFAULT NULL COMMENT '预约时间',
`yn` tinyint(255) unsigned DEFAULT NULL COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `reserve_id_ref` (`reserve_info_id`),
CONSTRAINT `reserve_id_ref` FOREIGN KEY (`reserve_info_id`) REFERENCES `t_re 
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

缓存设计:Redis存储内容:Redis key:t_reserve_info:skuId:userId,Redis value使用JSON string存储
t_reserve_user可以设置为hash结构,key为skuId,value为userId的集合

预约期间为更好的限制活动人数:限制预约人数,当人数达到上限后进行预约熔断机制,将活动状态改为等待秒杀

秒杀的削峰

秒杀活动开始后的流程

  1. 验证码+问答题:快速拦截掉部分刷子流量,防止机器人刷单,另一方面就是平滑秒杀的毛刺请求,延缓并发,对流量进行削峰设计验证码流程,一般是在用户进入详情页时,先判别秒杀活动是否已经开始,如果已经开始,同时秒杀活动也配置了需要校验验证码标识,那么就需要从秒杀系统获取图片验证码,并进行渲染;用户手工输入验证码后,提交给秒杀系统进行验证码校验,如果通过就跳转至秒杀结算页。验证码的实现调用google提供的开源工具kaptcha,kaptcha是一个生成验证码的工具,可以生成图片验证码,也可以生成语音验证码,同时也可以自定义验证码的样式,比如字体,字体颜色,字体大小,背景颜色,干扰线颜色等。验证码的校验,可以通过redis来实现,将验证码存储到redis中,key为用户ID,value为验证码,设置过期时间为5分钟,当用户提交验证码后,从redis中获取验证码,进行比对,如果一致,就返回true,否则返回false。
  1. 异步消息队列:秒杀活动开始后,用户进入详情页,点击秒杀按钮,会发送请求到秒杀系统,秒杀系统会根据用户ID和商品ID来判断用户是否有资格参与秒杀,如果有资格,就会将用户ID和商品ID发送到消息队列中,然后返回用户秒杀成功的页面,如果没有资格,就会返回秒杀失败的页面。消息队列的消费者会从消息队列中获取消息,然后调用秒杀系统的下单接口,进行下单操作。消息队列的实现,可以使用RocketMQ,RocketMQ是阿里巴巴开源的消息中间件,具有高吞吐量,高可用性,适合大规模分布式系统应用的特点。
  2. 限流:令牌桶和漏洞算法,线程池限流提供设置最大连接数来实现

服务降级

  1. 简化系统功能,干掉不必要的流程:比如各种附加信息的展示,比如商品的评价,商品的推荐,商品的咨询等,这些都可以在秒杀活动期间暂时干掉,只保留最核心的功能,比如商品的基本信息,商品的价格,商品的库存等。
  2. 建立多级缓存:比如在秒杀活动期间,可以将商品的基本信息,商品的价格,商品的库存等信息都缓存到Redis中,这样就可以减少对数据库的访问,从而减少数据库的压力。
  3. 写服务降级,暂时接受数据不一致性,使用最终一致性数据方案

容灾设计

  1. 同城双活:在同一个城市部署两套系统,一套为主系统,一套为备系统,主系统和备系统之间通过数据库的主从同步来保证数据的一致性,当主系统出现故障时,可以将流量切换到备系统上,从而保证秒杀系统的高可用性。
本文作者:GWB
当前时间:2023-11-09 11:11:10
版权声明:本文由gwb原创,本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 国际许可协议。
转载请注明出处!