文章目录

  • 秒杀系统
  • 业务特点 & 技术挑战
  • 架构原则 & 设计
  • 前端设计
  • 网关设计
  • 服务端设计
  • 数据库层设计
  • 服务重启与服务降级
  • 其他优化点
  • 案例:利用消息中间件和缓存实现简单的秒杀系统


本文旨在系统性的梳理两个经典系统的实现。

秒杀系统

业务特点 & 技术挑战

正常电子商务流程:

(1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货

秒杀业务流程:

(1)低廉价格;(2)大幅推广;(3)瞬时售空;(4)一般是定时上架;(5)时间短、瞬时并发量高

结合业务特点,总结技术挑战:

  1. 对现有网站业务造成冲击
    秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。
  2. 高并发下的应用、数据库负载
    用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。
  3. 突然增加的网络及服务器带宽
    假设商品页面大小200K(主要是商品图片大小),那么需要的网络和服务器带宽是2G(200K×10000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。
  4. 直接下单
    秒杀的游戏规则是到了秒杀才能开始对商品下单购买,在此时间点之前,只能浏览商品信息,不能下单。而下单页面也是一个普通的URL,如果得到这个URL,不用等到秒杀开始就可以下单了。
  5. 如何控制秒杀商品页面购买按钮的点亮
    购买按钮只有在秒杀开始的时候才能点亮,在此之前是灰色的。如果该页面是动态生成的,当然可以在服务器端构造响应页面输出,控制该按钮是灰色还是点亮,但是为了减轻服务器端负载压力,更好地利用CDN、反向代理等性能优化手段,该页面被设计为静态页面,缓存在CDN、反向代理服务器上,甚至用户浏览器上。秒杀开始时,用户刷新页面,请求根本不会到达应用服务器。
  6. 秒杀人数限制,超卖问题
    库存会带来“超卖”的问题:售出数量多于库存数量。

架构原则 & 设计

原则

  1. 尽量将请求拦截在系统上游。
    防止请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小。
  2. 读多写少的常用多使用缓存
    这是一个典型的读多写少的应用场景(比如200w个人来买,最多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%),非常适合使用缓存。

java 秒杀生产方案 java秒杀系统设计_数据

前端设计

首先要有一个展示秒杀商品的页面, 在这个页面上做一个秒杀活动开始的倒计时, 在准备阶段内用户会陆续打开这个秒杀的页面, 并且可能不停的刷新页面。
这里需要考虑两个问题:

  1. 秒杀页面的展示
  1. CDN
    我们知道一个html页面还是比较大的,即使做了压缩,http头和内容的大小也可能高达数十K,加上其他的css, js,图片等资源,如果同时有几千万人参与一个商品的抢购,一般机房带宽也就只有1G~10G,网络带宽就极有可能成为瓶颈,所以这个页面上各类静态资源首先应分开存放,然后放到cdn节点上分散压力,由于CDN节点遍布全国各地,能缓冲掉绝大部分的压力,而且还比机房带宽便宜。
  2. 动静分离
    页面彻底动静分离,使得用户秒杀时不需要刷新整个页面,降低刷新请求数(区分了动静数据,就可以将静态数据缓存,提高效率。)。
  1. 倒计时
    出于性能原因这个一般由js调用客户端本地时间,就有可能出现客户端时钟与服务器时钟不一致另外服务器之间也是有可能出现时钟不一致
    web服务器之间时间不同步可以采用统一时间服务器的方式,比如每隔1分钟所有参与秒杀活动的web服务器就与时间服务器做一次时间同步。

就我以前测试的结果来看,一台标准的web服务器2W+QPS不会有问题,如果100W人同时刷,100W QPS也只需要50台web,一台硬件LB就可以了。

  1. 浏览器层请求拦截
  1. 产品层面,用户点击“查询”或者“购买”后,按钮置灰或者做页面跳转,禁止用户重复提交请求;
  2. JS层面,限制用户在x秒之内只能提交一次请求。
  1. 浏览器缓存
    将动态请求的读数据缓存在浏览器本地,过滤无效数据读。

网关设计

前端层的请求拦截,只能拦住小白用户(99%的用户),脚本刷单根本不吃这一套,写个for循环,直接调用你后端的http请求,怎么整?

  1. 同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面
  2. 同一个item的查询,做页面缓存,x秒内到达站点层的请求,均返回同一页面

秒杀场景下可以限制uid。

服务端设计

假设网关层设计好了,如果有高级黑客控制了几万台肉鸡,网关层限制uid的策略解决不了了。
或者 当秒杀的用户量很大时,即使每个用户只有一个请求,到服务层的请求数量还是很大。

比如我们有100W用户同时抢100台手机,服务层并发请求压力至少为100W。这时候,笔者有三种可能的办法:

  1. 采用消息队列缓存请求
    既然服务层知道库存只有100台手机,那完全没有必要把100W个请求都传递到数据库啊,那么可以先把这些请求都写到消息队列缓存一下,数据库层订阅消息减库存,减库存成功的请求返回秒杀成功,失败的返回秒杀结束。
  2. 利用缓存应对读请求:对类似于12306等购票业务,是典型的读多写少业务,大部分请求是查询请求,所以可以利用缓存分担数据库压力。
  3. 利用缓存应对写请求:缓存也是可以应对写请求的,比如我们就可以把数据库中的库存数据转移到Redis缓存中,所有减库存操作都在Redis中进行,然后再通过后台进程把Redis中的用户秒杀请求同步到数据库中。

数据库层设计

数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担“能力范围内”的访问请求。所以,上面通过在服务层引入MQ队列和缓存,让最底层的数据库高枕无忧。

数据库可以实现分片和主从,将压力分散到各个节点上。

  1. 分片

范围:range
优点:简单,容易扩展
缺点:各库压力不均(新号段更活跃)

哈希:hash 【大部分互联网公司采用的方案二:哈希分库,哈希路由】
优点:简单,数据均衡,负载均匀
缺点:迁移麻烦(2库扩3库数据要迁移)

路由服务:router-config-server
优点:灵活性强,业务与路由算法解耦
缺点:每次访问数据库前多一次查询

  1. 主从一致性

中间件: 如果某一个key有写操作,在不一致时间窗口内,中间件会将这个key的读操作也路由到主库上。这个方案的缺点是,数据库中间件的门槛较高(百度,腾讯,阿里,360等一些公司有)。

java 秒杀生产方案 java秒杀系统设计_数据库_02


缓存+缓存双淘汰:

写操作时序升级为:

(1)淘汰cache;

(2)写数据库;

(3)在经验“主从同步延时窗口时间”后,再次发起一个异步淘汰cache的请求;

这样,即使有脏数据如cache,一个小的时间窗口之后,脏数据还是会被淘汰。带来的代价是,多引入一次读miss(成本可以忽略)。

服务重启与服务降级

如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。

秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回。

其他优化点

1.发现热点数据:

  • 发现静态热点数据:强制让卖家通过报名方式提前把热点数据筛选出来缓存,但是增加了卖家的工作量,也不够实时。也可以根据每日访问数进行统计,然后缓存TOP N的商品。
  • 发现动态热点数据:抽离出一个中间件用于收集搜索、商品详情、购物车等关键热点业务的点击数据,然后异步记录到日志,然后根据规则判断是否热点数据后缓存在队列中(因为热点数据一般是临时的,所以可采用LRU算法淘汰)。

2.CPU及线程数量:

不是线程数越多,QPS越高,因为线程上下文切换有消耗,所以需要合理的设置线程数。
一般的计算公式为:线程数 = [(线程等待时间 + 线程CPU时间) / 线程CPU时间] * CPU数量,当然最好的方式是性能测试来确认。
秒杀系统的大部分瓶颈在CPU,但不一定是CPU,有可能是其他部分,比如QPS达到极限时,CPU使用率是否超过95%,如果不是则可能是锁限制或过多本地I/O等待发生

3.系统优化:

  • 减少编码:java编码速度慢,涉及字符串操作(输入输出操作、I/O操作)比较消耗CPU资源。原因是磁盘、网络IO都需要将字符串转为字节,这个转换必须查表编码。可通过(OutputStream()直接进行流输出),可提高30%.
  • 减少序列化:序列化与编码同时发生,所以需要减少。尽量减少RPC,将关联性强的应用服务合并。
  • Java极致优化:对大流量Web系统做静态化改造;直接使用Servlet,绕过框架多余处理逻辑;直接输出流数据。
  • 并发读优化:秒杀系统单机缓存。不要求读一致性,但是写数据的时候要求强一致性

4.秒杀系统独立部署:

将秒杀系统服务做独立部署,甚至使用独立域名,使其与网站完全隔离。

案例:利用消息中间件和缓存实现简单的秒杀系统

Redis是一个分布式缓存系统,支持多种数据结构,我们可以利用Redis轻松实现一个强大的秒杀系统。

我们可以采用Redis 最简单的key-value数据结构,用一个原子类型的变量值(AtomicInteger)作为key,把用户id作为value,库存数量便是原子变量的最大值对于每个用户的秒杀,我们使用 RPUSH key value 插入秒杀请求, 当插入的秒杀请求数达到上限时,停止所有后续插入。

然后我们可以在台启动多个工作线程,使用LPOP key读取秒杀成功者的用户id,然后再操作数据库做最终的下订单减库存操作。也可以将缓存和消息中间件 组合起来,缓存系统负责接收记录用户请求,消息中间件负责将缓存中的请求同步到数据库