浅谈高并发——以秒杀场景为例
导读
高并发指的是同一时刻有大量的用户请求到达服务器,服务器需要对请求进行处理,并及时返回响应信息。通过有限的服务器资源,尽可能快速地处理尽可能多的网络请求,是一个值得深入研究与探讨的话题。
现如今,互联网服务内容越来越丰富,用户越来越多,服务器压力也变得越来越大。在流量不大的情况下,对于小型网站来说,部署简单的动态页面,通过数据库进行信息存取,即可满足大部分应用场景。但随着用户单位时间访问量越来越大,并发越来越高,传统服务器的简单架构已经难以应对大流量的冲击,服务会被冲垮,造成宕机。
要想优雅地处理高并发问题,需要进行全面的考虑,包括但不限于:网络请求、服务器性能、IO瓶颈、带宽等。做好相应的方案设计,才能在资源有限的前提下,提高服务器的承载能力。高并发系统常见的应对措施包括缓存、限流和降级。使用缓存可以有效缓解服务器的压力、增大系统处理能力、加快请求响应速度,几乎是高并发服务器系统的标配;限流是通过减少请求频率来减轻服务器压力;降级的意思是把不重要的服务暂时关闭,节省服务器资源,从而保证核心服务的正常运行,降级首要考虑的问题是区分核心服务与非核心服务,分辨哪些服务可以降级,哪些不能降级。核心服务如写库、下单、支付等是不能降级的,在高并发的情形下需要考虑应对措施保证其可用。
今天我们以生活中常见的秒杀场景为例,结合具体项目—搜狐焦点在线开盘系统,来说明高并发场景的应对措施。秒杀场景有以下几个特点:大量用户同时进行抢购操作,系统流量激增,服务器瞬时压力很大;请求数量远大于商品库存量,只有少数客户可以成功抢购;业务流程不复杂,核心功能是下订单。秒杀场景的应对,一般要从以下几个方面进行处理,如下:
限流:从客户端层面考虑,限制单个客户抢购频率;服务端层面,加强校验,识别请求是否来源于真实的客户端,并限制请求频率,防止恶意刷单;应用层面,可以使用漏桶算法或令牌桶算法实现应用级限流。
缓存:热点数据都从缓存获得,尽可能减小数据库的访问压力;
异步:客户抢购成功后立即返回响应,之后通过消息队列,异步处理后续步骤,如发短信、更新数据库等,从而缓解服务器峰值压力。
分流:单台服务器肯定无法应对抢购期间大量请求造成的压力,需要集群部署服务器,通过负载均衡共同处理客户端请求,分散压力。
除此之外,设计一个秒杀系统还需要考虑系统的可扩展性,客户量级进一步增大后,能够方便地进行容量扩展。
房产在线开盘是一种典型的高并发秒杀场景,买房的人往往多于房源数量,几千人同时抢购并不是什么稀罕事,一些热门楼盘甚至出现万人抢房的空前盛况。搜狐焦点在线开盘系统为这种场景提供了可靠的解决方案,能够从容应对万人级别的并发抢购场景,抗并发能力在实际开盘中得到检验。该系统整体架构如图1所示。
下面具体介绍一下在线开盘系统的技术细节:
总体方案如下:从前端开始,在各层级模块进行限流处理,对单个用户的请求频率作限制,进行多级校验,及时丢弃⽆效请求;前端和后端联合进行请求数据校验,防止请求作弊,减小服务器负载。具体如下:
客户登录时,在nginx层经过会lua脚本的防刷处理,对登录接口访问频率作限制,防止登录接口请求频率过高导致的数据库崩溃;同时,前端读取当前时间戳,与用户ID进行拼接后进行Hash运算,生成token,存储在Redis缓存以及Cookie中,并返回Cookie给客户端浏览器;该token作为用户的身份校验凭证,每次发起抢购请求时,后端都会校验该token,判断前后端是否匹配,若不匹配则拒绝客户端请求,并提示用户重新登录;从图2可以看出,当用户从其他设备重新登录时,会生成最新的token覆盖旧token,原设备Cookie中存储的token则失效。这样保证了C端用户只能在单台设备上进行登录,减少了用户通过多台设备同时参与秒杀的可能。
图2 前端限制单台设备登录的示意图
客户抢购过程中,从多个层级模块对认购请求进行限流削峰处理,整体流程图如图3所示,包括如下几个步骤:
1.前端控制用户点击抢购按钮的频率,每一次点击之后都出现一个蒙层或倒计时,对抢购按钮禁用一段时间,之后才允许C端用户再次点击按钮发起抢购请求;
图3 抢购流程图
2.服务器端对来自于客户端的请求做合法性校验,判断用户token是否有效,判断请求参数中的签名值是否正确(即图4中后端模块第二步),判断活动是否已经开始,从缓存获取客户信息,校验用户是否具有抢购资格,对不合法的请求全部拒绝并返回失败结果;
3.获取用户频率锁,若成功获取,则进行后续判断,否则认为用户访问频率超限,使缓存中存储的用户token失效,给客户返回失败结果,并提醒用户重新登录;
4.获取对应房源的分布式锁,若获取失败,则认为该房源已售,返回失败结果;若获取商品锁成功,则认为本次抢购成功,返回成功结果;
5.客户抢购成功后,请求数据会推送给Kafka消息队列,并进行后续处理;
6.消费Kafka队列中的记录,发送抢购成功的通知短信给用户,将成交数据持久化到数据库,通过WebSocket实时传递成交记录给B端投屏页面。
在这个过程中,用户频率锁和商品锁都是通过Redis分布式非阻塞锁实现,只尝试一次,不阻塞等待。前端所使用的静态资源文件,通过CDN获取,避免对业务服务器的直接访问;服务器热点数据,如活动描述、活动须知等数据,存储在进程本地缓存中,以此提高程序响应速度。
图4 前后端校验防作弊方法流程图
抢购成功之后使用Kafka消息队列异步进行后续处理,包括写入订单到数据库、发送抢购成功的提醒短信、推送实时消息给B端前台投屏页等操作,既减少了客户的等待时间,也减轻了服务器和数据库的平均压力。
简单总结一下上述过程中涉及的防刷及防作弊策略。前端会对客户点击抢购按钮的频率作限制,正常情况下不会出现某客户请求频率过高的情况;客户的登录请求在nginx进行反向代理时会经过lua脚本的处理,对于请求过于频繁的可疑ip进行一段时间的封禁处理,拒绝其请求;抢购过程中,对于频率过高的用户请求,会被认为是作弊,返回失败结果,强制其下线。同时,对于请求是否来源于真实客户端,进行前后端联合检验:校验逻辑包括前端模块和后端模块。其中,前端模块通过页面直出数据获取到用户的初始校验值,用户进行抢购操作时,对某些特征数据以及用户初始校验值进行加密运算,加密后的字符串作为签名传递给后端接口,之后前端按照某种方法对校验值作变换;后端模块收到请求后,首先对校验值按照与前端相同的方法进行变换,存入缓存,等待下一次的前端请求,然后对请求特征数据以及原先的校验值进行与前端相同规则的加密运算,判断前端签名的值与后端计算的值是否匹配,匹配则继续进行后续处理,不匹配则返回失败结果。这种联合校验的方式减少了使用工具作弊的可能性,在增加作弊难度的同时保持了良好的用户体验。
从分流角度来说,项目后端服务集群部署,客户端请求经过多层负载均衡,均匀地分散到各个应用服务器,各服务器共同分担计算力;IO层面,数据库进行读写分离,读请求和写请求分散到不同数据库,增加数据库承载能力。某些服务进行单独部署,如获取验证码、发送短信、发起支付等,这样一来,压力分摊在不同的微服务上,避免压力过于集中导致的服务器崩溃。
综上所述,通过多层级模块的防刷限流削峰处理,以及后端服务负载均衡,有效降低了服务器和数据库的负载压力,使得项目能够在资源有限的情况下,在短时间内承受大量的并发请求,极大地增加了服务器的吞吐量。同时通过对用户访问次数的前后端校验,能够快速识别作弊请求,保证了活动稳定、公平地进行。
展望
目前的技术方案在众多开盘实践中已经证明了其可行性与可靠性。如果参与秒杀的客户规模进一步增加,则需要对技术方案进一步升级。服务器计算能力的扩展可以靠增加数量来解决,但单纯增加机器是不能完全解决问题的,服务器IO能力也需要考虑。数据库层面,可以进行数据分片,根据客户的主键哈希值将客户信息存储在不同的数据库;缓存层面,单机版redis足够支撑十万级QPS,如果不够的话,可以将数据分散存储到几个不同的redis实例上,通过redis客户端分片或服务端集群的方式,增强服务器IO能力。另外,进行页面静态化,将较少改动的页面缓存起来,避免不必要的计算和IO,减轻服务器和数据库压力。这样的技术方案足以应对一般性的秒杀抢购场景。假如参与抢购的客户均匀分布在全国或全世界各地,还需要考虑不同距离的网络延迟带来的公平性问题,考虑进行多机房部署。总之,在进行秒杀系统设计时一定要综合考虑业务的实际情况,具体问题具体分析,选择最适合的技术方案。