对页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面,借此把页面刷新的数据降到最少。
用户看到的数据可以分为:静态数据 和 动态数据。
简单来说,"动态数据"和"静态数据"的主要区别就是看页面中输出的数据是否和URL、浏览者、时间、地域相关,以及是否含有Cookie等私密数据。
比如说:
这里再强调一下,我们所说的静态数据,不能仅仅理解为传统意义上完全存在磁盘上的HTML页面,它也可能是经过Java系统产生的页面,但是它输出的页面本身不包含上面所说的那些因素。
也就是所谓"动态"还是"静态",并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。
静态化改造就是要直接缓存 HTTP 连接。
相较于普通的数据缓存而言,你肯定还听过系统的静态化改造。静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据,如下图所示,Web 代理服务器根据请求 URL,直接取出对应的 HTTP 响应头和响应体然后直接返回,这个响应过程简单得连 HTTP 协议都不用重新组装,甚至连 HTTP 请求头也不需要解析。
图片
高并发时候,商详页面是最先受到冲击的,通过商详静态化,可以帮助服务器挡掉99.9%流量。
分类举例:商品图片、商品详细描述等,所有用户看到的内容都是一样的,这一类数据就可以上静态化。
会员折扣、优惠券等信息具备个体差异性,就需要放在动态接接口中,根据入参信息实时查询。
我们从以下 5 个方面来分离出动态内容:
分离出动态内容之后,如何组织这些内容页就变得非常关键了。
动态内容的处理通常有两种方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。
网站应用,静态资源占流量的多数。系统做了动静分离之后,就可以把静态资源通过CDN加速。
这样,静态资源的请求大部分通过就近部署的CDN服务器提供服务,用户的延迟也会有明显的提升。网站服务器专注于服务动态流量,带宽压力会小很多。
动静分离,部署时静态资源要给一个单独域名,这个域名是个CNAME,CNAME映射到CDN服务厂商提供的DNS服务器,CDN DNS服务器会根据请求的IP地址所在区域和资源内容,返回就近的CDN缓存服务器ip,后续用户对这个DNS的请求都会转到这个IP上来。
Tips:CNAME 简单来讲就是给域名起了个别名。
CDN 工作流程大致如下:
图片
静态资源上 CDN 存在以下几个问题:
失效需要一个失效系统来实现,一般有主动失效和被动失效。
主动失效需要监控数据库数据的变化然后转成消息来发送失效消息,这个实现比较复杂,阿里有个系统叫metaq,可以网上参考下。
被动失效就是只缓存固定时间,然后到期后自动失效
部署方式如下图所示:
图片
你可能会问,存储在浏览器或 CDN 上,有多大区别?我的回答是:区别很大!因为在 CDN 上,我们可以做主动失效,而在用户的浏览器里就更不可控,如果用户不主动刷新的话,你很难主动地把消息推送给用户的浏览器。
比如,1 元卖 iPhone,100 台,于是来了一百万人抢购。
我们把技术挑战放在一边,先从用户或是产品的角度来看一下,秒杀的流程是什么样的。
从技术上来说,这个倒计时按钮上的时间和按钮可以被点击的时间是需要后台服务器来校准的,这意味着:
很明显,要让 100 万用户能够在同一时间打开一个页面,这个时候,我们就需要用到 CDN 了。数据中心肯定是扛不住的,所以,我们要引入 CDN。
在 CDN 上,这 100 万个用户就会被几十个甚至上百个 CDN 的边缘结点给分担了,于是就能够扛得住。然后,我们还需要在这些 CDN 结点上做点小文章。
一方面,我们需要把小服务部署到 CDN 结点上去,这样,当前端页面来问开没开始时,这个小服务除了告诉前端开没开始外,它还可以统计下有多少人在线。每个小服务会把当前在线等待秒杀的人数每隔一段时间就回传给我们的数据中心,于是我们就知道全网总共在线的人数有多少。
假设,我们知道有大约 100 万的人在线等着抢,那么,在我们快要开始的时候,由数据中心向各个部署在 CDN 结点上的小服务上传递一个概率值,比如说是 0.02%。
于是,当秒杀开始的时候,这 100 万用户都在点下单按钮,首先他们请求到的是 CDN 上的这些服务,这些小服务按照 0.02% 的量把用户放到后面的数据中心,也就是 1 万个人放过去两个,剩下的 9998 个都直接返回秒杀已结束。于是,100 万用户被放过了 0.02% 的用户,也就是 200 个左右,而这 200 个人在数据中心抢那 100 个 iPhone,也就是 200 TPS,这个并发量怎么都应该能扛住了。
热点数据亦分 静态热点 和 动态热点。
所谓"静态热点数据",就是能够提前预测的热点数据。
例如,我们可以通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。另外,我们还可以通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点。
所谓"动态热点数据",就是不能被提前预测到的,系统在运行过程中临时产生的热点。例如,卖家在抖音上做了广告,然后商品一下就火了,导致它在短时间内被大量购买。
静态热点比较好处理,所以秒级内自动发现热点商品就成为了热点缓存的关键。
这里我给出一个动态热点发现系统的具体实现:
这里我给出了一个图,其中用户访问商品时经过的路径有很多,我们主要是依赖前面的导购页面(包括首页、搜索页面、商品详情、购物车等)提前识别哪些商品的访问量高,通过这些系统中的中间件来收集热点数据,并记录到日志中。
图片
我们通过部署在每台机器上的Agent把日志汇总到聚合和分析集群中,然后把符合一定规则的热点数据,通过订阅分发系统再推送到相应的系统中。你可以是把热点数据填充到Cache中,或者直接推送到应用服务器的内存中,还可以对这些数据进行拦截,总之下游系统可以订阅这些数据,然后根据自己的需求决定如何处理这些数据。
热点发现要做到接近实时(3s内完成热点数据的发现),因为只有做到接近实时,动态发现才有意义,才能实时地对下游系统提供保护。
对于缓存系统来讲,缓存命中率是最重要的指标,甚至都没有之一。时间拉的越长,不确定性越多,缓存命中率必然越低。比如如果10s内才发送热点就没意义了,因为10s内用户可以进行的操作太多了。时间越长,不可控元素越多,热点缓存命中率越低。
可以参考,京东开源的热点探测 Hot Key。
可以考虑建立实时热点发现系统。
具体步骤如下:
限制更多的是一种保护机制,限制的办法也有很多,例如对被访问商品的 ID 做一致性 Hash,然后根据 Hash 做分桶,每个分桶设置一个处理队列,这样可以把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。
使用Java堆内存来存储缓存对象。使用堆缓存的好处是不需要序列化/反序列化,是最快的缓存。缺点也很明显,当缓存的数据量很大时,GC(垃圾回收)暂停时间会变长,存储容量受限于堆空间大小。
一般通过软引用/弱引用来存储缓存对象,即当堆内存不足时,可以强制回收这部分内存释放堆内存空间。一般使用堆缓存存储较热的数据。可以使用Caffeine Cache实现。
现在应用最多的是多级缓存方案,就好比 CPU 也有 L1,L2,L3。
Nginx缓存 → 分布式Redis缓存(可以使用Lua脚本直接在Nginx里读取Redis)→堆内存。
整体流程如下:
添加秒杀答题。有以下两个目的:
你可能有疑问了,排队和锁竞争不都是要等待吗,有啥区别?
如果熟悉 MySQL 的话,你会知道 InnoDB 内部的死锁检测,以及 MySQL Server 和 InnoDB 的切换会比较消耗性能。
对于分布式限流,目前遇到的场景是业务上的限流,而不是流量入口的限流。流量入口限流应该在接入层完成,而接入层笔者一般使用 Nginx。业务的限流一般用Redis + Lua脚本。
千万不要超卖,这是大前提。超卖直接导致的就是资损。
在正常的电商平台购物场景中,用户的实际购买过程一般分为两步:下单和付款。你想买一台 iPhone 手机,在商品页面点了“立即购买”按钮,核对信息之后点击“提交订单”,这一步称为下单操作。下单之后,你只有真正完成付款操作才能算真正购买,也就是俗话说的“落袋为安”。
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
先说第一种,"下单减库存",可能导致恶意下单。
正常情况下,买家下单后付款的概率会很高,所以不会有太大问题。但是有一种场景例外,就是当卖家参加某个活动时,此时活动的有效时间是商品的黄金售卖时间,如果有竞争对手通过恶意下单的方式将该卖家的商品全部下单(雇几个人下单将你的商品全都锁了),让这款商品的库存减为零,那么这款商品就不能正常售卖了。要知道,这些恶意下单的人是不会真正付款的,这正是"下单减库存"方式的不足之处。
既然,从而影响卖家的商品销售,那么有没有办法解决呢?你可能会想,采用"付款减库存"的方式是不是就可以了?的确可以。但是,"付款减库存"又会导致另外一个问题:库存超卖。
假如有 100 件商品,就可能出现 300 人下单成功的情况,因为下单时不会减库存,所以也就可能出现下单成功数远远超过真正库存数的情况,这尤其会发生在做活动的热门商品上。这样一来,就会导致很多买家下单成功但是付不了款,买家的购物体验自然比较差。
超卖情况可以区别对待:对普通的商品下单数量超过库存数量的情况,可以通过补货来解决;但是有些卖家完全不允许库存为负数的情况,那只能在买家付款时提示库存不足。
预扣库存方案确实可以在一定程度上缓解上面的问题。但没有彻底解决,比如针对恶意下单这种情况,虽然把有效的付款时间设置为 10 分钟,但是恶意买家完全可以在 10 分钟后再次下单,或者采用一次下单很多件的方式把库存减完。针对这种情况,解决办法还是要结合安全和反作弊的措施来制止。
例如,给经常下单不付款的买家进行识别打标(可以在被打标的买家下单时不减库存)、给某些类目设置最大购买件数(例如,参加活动的商品一人最多只能买 3 件),以及对重复下单不付款的操作进行次数限制等。
方案的核心思路:将库存扣减异步化,库存扣减流程调整为下单时只记录扣减明细(DB记录插入),异步进行真正库存扣减(更新)。
大量请求对同一数据行的的竞争更新,会导致数据库的性能急剧下降,甚至发生数据库分片的连接被热点单商品扣减。
前置校验库存,从db更换为redis,库存扣减操作,从更新操作,直接修改为插入操作(性能角度,插入锁比更新锁的性能高)
热点发现系统(中间件)会通过消息队列的方式通知应用,应用对库存进行热点打标。一但库存不再是热点(热点失效),则会进行库存热点重置。
将商品库存分开放,分而治之。例如,原来的秒杀商品的id为10001,库存为1000件,在Redis中的存储为(10001, 1000),我们将原有的库存分割为5份,则每份的库存为200件,此时,我们在Redia中存储的信息为(10001_0, 200),(10001_1, 200),(10001_2, 200),(10001_3, 200),(10001_4, 200)。将key分散到redis的不同槽位中,这就能够提升Redis处理请求的性能和并发量。
单个热点商品会影响整个数据库的性能,导致0.01%的商品影响99.99%的商品的售卖,这是我们不愿意看到的情况。一个解决思路是遵循前面介绍的原则进行隔离,把热点商品放到单独的热点库中。但是这无疑会带来维护上的麻烦,比如要做热点数据的动态迁移以及单独的数据库等。
线程隔离主要是指线程池隔离,在实际使用时,我们会把请求分类,然后交给不同的线程池处理。当一种业务的请求处理发生问题时,不会将故障扩散到其他线程池,从而保证其他服务可用。
图片
随着对系统可用性的要求,会进行多机房部署,每个机房的服务都有自己的服务分组,本机房的服务应该只调用本机房服务,不进行跨机房调用。其中,一个机房服务发生问题时,可以通过DNS/负载均衡将请求全部切到另一个机房,或者考虑服务能自动重试其他机房的服务,从而提升系统可用性。
图片
核心业务以及非核心业务可以放在不同的线程池。
可以使用Hystrix来实现线程池隔离。
所谓“降级”,就是当系统的容量达到一定程度时,是为了保证核心服务的稳定而牺牲非核心服务的做法。
降级方案可以这样设计:当秒杀流量达到 5w/s 时,把成交记录的获取从展示 20 条降级到只展示 5 条。“从 20 改到 5”这个操作由一个开关来实现,也就是设置一个能够从开关系统动态获取的系统参数。
降级无疑是在系统性能和用户体验之间选择了前者,降级后肯定会影响一部分用户的体验,例如在双 11 零点时,如果优惠券系统扛不住,可能会临时降级商品详情的优惠信息展示,把有限的系统资源用在保障交易系统正确展示优惠信息上,即保障用户真正下单时的价格是正确的。所以降级的核心目标是牺牲次要的功能和用户体验来保证核心业务流程的稳定,是一个不得已而为之的举措。
如果限流还不能解决问题,最后一招就是直接拒绝服务了。
当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的系统保护方式。
在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码,在 Java 层同样也可以设计过载保护。
在项目的架构中,我们一般会同时部署 LVS 和 Nginx 来做 HTTP 应用服务的负载均衡。也就是说,在入口处部署 LVS,将流量分发到多个 Nginx 服务器上,再由 Nginx 服务器分发到应用服务器上。
为什么这么做呢?
主要和 LVS 和 Nginx 的特点有关,LVS 是在网络栈的四层做请求包的转发,请求包转发之后,由客户端和后端服务直接建立连接,后续的响应包不会再经过 LVS 服务器,所以相比 Nginx,性能会更高,也能够承担更高的并发。
可 LVS 缺陷是工作在四层,而请求的URL是七层的概念,不能针对URL做更细致地请求分发,而且LVS也没有提供探测后端服务是否存活的机制;而Nginx虽然比LVS的性能差很多,但也可以承担每秒几万次的请求,并且它在配置上更加灵活,还可以感知后端服务是否出现问题。
因此,LVS适合在入口处,承担大流量的请求分发,而Nginx要部在业务服务器之前做更细维度的请求分发。
我给你的建议是,如果你的QPS在十万以内,那么可以考虑不引入 LVS 而直接使用 Nginx 作为唯一的负载均衡服务器,这样少维护一个组件,也会减少系统的维护成本。
但对于Nginx来说,我们要如何保证配置的服务节点是可用的呢?
这就要感谢淘宝开源的 Nginx 模块 nginx_upstream_check_moduule 了,这个模块可以让 Nginx 定期地探测后端服务的一个指定的接口,然后根据返回的状态码,来判断服务是否还存活。当探测不存活的次数达到一定阈值时,就自动将这个后端服务从负载均衡服务器中摘除。
它的配置样例如下:
upstream server { server 192.168.1.1:8080; server 192.168.1.2:8080; check interval=3000 rise=2 fall=5 timeout=1000 type=http default_down=true check_http_send "GET /health_check HTTP/1.0/r/n/n/n/n"; //检测URL check_http_expect_alivehttp_2xx; //检测返回状态码为 200 时认为检测成功}
不过这两个负载均衡服务适用于普通的Web服务,对于微服务多架构来说,它们是不合适的。因为微服务架构中的服务节点存储在注册中心里,使用 LVS 就很难和注册中心交互,获取全量的服务节点列表。
另外,一般微服务架构中,使用的是RPC协议而不是HTTP协议,所以Nginx也不能满足要求。
所以,我们会使用另一类的负载均衡服务,客户端负载均衡服务,也就是把负载均衡的服务内嵌在RPC客户端中。
当我们的应用单实例不能支撑用户请求时,此时就需要扩容,从一台服务器扩容到两台、几十台、几百台。
然而,用户访问时是通过如 http://www.jd.com 的方式访问,在请求时,浏览器首先会查询DNS服务器获取对应的IP,然后通过此 IP 访问对应的服务。
因此,一种方式是 www.jd.com 域名映射多个IP,但是,存在一个最简单的问题,假设某台服务器重启或者出现故障,DNS 会有一定的缓存时间,故障后切换时间长,而且没有对后端服务进行心跳检查和失败重试的机制。
对于一般应用来说,有Nginx就可以了。但Nginx一般用于七层负载均衡,其吞吐量是有一定限制的。为了提升整体吞吐量,会在 DNS 和 Nginx之间引入接入层,如使用LVS(软件负载均衡器)、F5(硬负载均衡器)可以做四层负载均衡,即首先 DNS解析到LVS/F5,然后LVS/F5转发给Nginx,再由Nginx转发给后端Real Server。
图片
对于一般业务开发人员来说,我们只需要关心到Nginx层面就够了,LVS/F5一般由系统/运维工程师来维护。Nginx目前提供了HTTP (ngx_http_upstream_module)七层负载均衡,而1.9.0版本也开始支持TCP(ngx_stream_upstream_module)四层负载均衡。
一致性hash算法最好在 lua脚本里指定。
Nginx商业版还提供了 least_time,即基于最小平均响应时间进行负载均衡。
Nginx的服务检查是惰性的,Nginx只有当有访问时后,才发起对后端节点探测。如果本次请求中,节点正好出现故障,Nginx依然将请求转交给故障的节点,然后再转交给健康的节点处理。所以不会影响到这次请求的正常进行。但是会影响效率,因为多了一次转发,而且自带模块无法做到预警。
比如对于订单库,当对其分库分表后,如果想按照商家维度或者按照用户维度进行查询,那么是非常困难的,因此可以通过异构数据库来解决这个问题。可以采用下图的架构。
图片
异构数据主要存储数据之间的关系,然后通过查询源库查询实际数据。不过,有时可以通过数据冗余存储来减少源库查询量或者提升查询性能。
针对这类场景问题,最常用的是采用“异构索引表”的方式解决,即采用异步机制将原表的每一次创建或更新,都换另一个维度保存一份完整的数据表或索引表,拿空间换时间。
也就是应用在插入或更新一条订单ID为分库分表键的订单数据时,也会再保存一份按照买家ID为分库分表键的订单索引数据,其结果就是同一买家的所有订单索引表都保存在同一数据库中,这就是给订单创建了异构索引表。
本文链接:http://www.28at.com/showinfo-26-94282-0.html我们一起聊聊如何设计一个秒杀系统?
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 探索Word文档导入导出的前端实现方案