本文将从业务场景、设计思路、实现细节三大方面记录实现一个自己的秒杀系统原型的思考与实践
业务场景
提起秒杀的场景,首先脑海里浮现的是大流量、高并发量的特征。现在让我们脱离固有的简单印象,深入思考理解类似于秒杀这样的场景。无论是秒杀、抢红包,还是抢票抢优惠券,本质上这样的场景可以抽象成多数人去争夺有限的资源,最终少部分人成功获取资源的问题。在这样的场景中,有用户对资源信息的获取,例如获取待抢购的商品的列表、详情;有用户触发争抢资源的操作,即点击抢单、开启红包;对于服务提供方则需要在用户抢单时分配资源 (简单理解为先到先得)、在资源所有权确定后进行后续的结算,例如库存变更、账户操作等;还有用户的支付、超时结束等行为
沿着这些动作的路径,来分析这些动作发生的阶段资源的特征、并发量的预测与动作本身的特征。获取资源的动作会发生在用户实际下单之前,会拥有最多的访问量,这时基本可以认定为操作为读操作。同时这一阶段也会获取最多的信息,包括占更大比重的相对不易改变的信息 (商品图片、描述…) 和少部分的动态信息 (时间、地域、用户信息)。下单的操作需要从两方面来看,用户端下单会发生大量用户争夺相同资源的情况,同时路径相似、入口相同,自然会使得请求集中到一起,带来最大的并发压力。在服务端,下单的操作意味着需要检查可用资源、分配资源。在一般的电商场景中,用户下单还会意味着一条新订单的创建操作。但是对于秒杀的场景来说,用户是在争夺资源,用户更关心的是有没有抢到的结果,订单的优先级稍低。因而这一阶段的操作都是围绕着剩余可用资源展开的,具体一点来说即可用库存量。可用资源量作为热点数据,有并发的读写问题,且与后续的库存变更存在一致性问题。下单后的结算行为,由于获得到资源的用户量有相应的限制,此时的请求数量是可控的,操作也会集中在写操作,但是需要操作的资源是分散的,有可能是用户信息。账户信息、库存出库记录… 最后关于订单后续的支付、关闭操作则是整个秒杀场景后续部分,这时会有相应的支付流程、订单自动关闭流程,这时流量峰值有可能已经过去,同时处置的允许时间窗口相对较长,可以延后从容处置
总结来说,秒杀的场景中,请求量从前向后逐步缩小,留给服务处理的时间窗口逐渐增大;读操作基本集中在前置环节中,写操作集中在后置环节;资源的数量和内容可以进行提前的预测;对于可用资源的操作需要保证并发读写的性能与正确性、与实际库存的一致性;与一般涉及订单的业务相比较,用户更关注是否抢到的结果
设计思路
本文会着重于设计秒杀系统中的服务端功能,会提供一个暴露Rest API的Web Servcer,对于静态资源部分仅会在设计思路这一部分进行阐述。对于服务的划分,将会按照功能来进行细分,将提供不同功能的服务进行隔离,便于之后进行分别部署优化:
- Dashboard Service
抽象出来对秒杀活动进行管理的服务,提供创建秒杀活动、检查活动统计结果的功能 - Product Service
商品信息服务,包含商品列表、详情功能 - Seckill Activity Service
秒杀活动服务,包含活动信息、动态信息的获取功能 - Order Service
订单服务,包含关键的下单功能,以及之后对订单信息的获取功能 - Inventory Service
库存服务,实际记录库存变更,限于内部触发,不会暴露外部接口
资源动静分离
首先先对秒杀系统中的各种数据进行处理,之前提到,秒杀场景中数据的种类和内容基本是固定的,有一部分相对不易改变的热点数据,而这部分数据也基本会在实际下单之前被用户大量请求获取。那么首先我们可以把这部分数据分离开,对其进行静态化的处理。对于秒杀系统而言,分离这部分数据相对容易,由于这些静态资源基本上会集中在商品信息、活动信息中,而这些资源又很容易可以通过一个唯一标识而确定,同时在请求时也可以很容易的通过其请求路径附加标识id来唯一确定。静态化处理后的资源,可以理解为不需要再进行格式化处理的数据,可以直接返回展示给用户,同时减少用户访问这部分数据的路径,无需再进入缓存层甚至存储层进行获取。
静态化的方式也有很多种,一种是对资源静态化后放置在系统边缘的部分直接返回,例如理由Nginx来作为静态资源服务器;也可以将这部分资源主动推送到client端进行提前缓存,例如浏览器、APP的缓存中;另外也可以借助CDN对这部分资源进行缓存,client端将会先经过CDN获取资源,在未获取到有效资源的情况下才会到达主站。对比三种方案,第一种服务端进行静态资源缓存会增加服务器压力与连接数,同时对于距离主站较远的用户,网络的影响会更加显著。但是可以提供更自由的资源的更新、过期机制。第二种的用户端提前缓存在提供更快的获取速度的同时,也会存在资源难以更新的问题,因为一旦向用户端输入了资源,那么就很难可以确认用户是否在线、客户端是否具有在背后执行更新服务的权限。最后使用CDN可以减少用户访问资源的跳转路径,提供相对高效的资源获取效率,减少主站的压力,同时也可以方便地通知CDN节点资源的更新、过期,再配合客户端进行处理。因而使用CDN是一种简单高效,具有更好性价比的静态资源的部署方式
最终用户对于静态资源的请求路径可能会是经过本地缓存、CDN节点、Nginx静态服务,最终才会到达服务层。由此将大量的读操作转移到别处,保证服务端专门应对关键请求路径的大流量冲击
第一步中将静态资源分离后,仍然会有一些动态的信息也需要提前获取,同时在大并发的情况下也会造成较大的读压力。例如用户信息、系统时间、地域信息等,都需要根据用户情况来每次进行实时获取。为了优化用户体验,这部分的请求可以通过异步方式来获取。这里有两种方式来进行异步处理,一种是服务端胶水层获取信息并渲染页面返回,用户体验较好但是对服务端又增加了新的压力;另一种是在客户端获取静态资源的同时,使用ajax获取信息。另外,对于这一部分,由于获取的信息量不大,服务端处理可以通过缓存进行快速获取,个人理解可以合并请求,减少请求数
多级缓存
对资源动静分离后,需要构建一套缓存体系,一方面包含对资源静态化缓存的层级,同时也要对一些的动态资源信息进行缓存以提升性能。使用缓存层需要关注几个关键问题,避免缓存穿透、缓存雪崩,确定缓存信息 (发现热点信息) 以及缓存数据的更新。在秒杀场景下,相对于其他场景,热点数据可以进行提前预测,同时除去库存等不可避免会发生改变的信息,大部分信息是不易发生改变的。此时需要重点关注缓存穿透、雪崩的问题,同时,在秒杀场景下,还会有一个比较特殊的问题,即由于大量用户会集中访问一些特定的资源,此时很可能是同一个资源,那么大量的请求会经过hash计算后命中同一个缓存节点。在一般场景下,单个缓存节点已经可以承受很大压力,但是在秒杀场景中由于压力很可能超出阈值,就会引起单个缓存节点的不可用,进而导致系统中大量压力转移到兜底的数据访问层,引起更严重的后果
- CDN、Nginx
静态资源缓存层 - LocalCache
对每个服务实例都构建自己的本地缓存层,将缓存信息存放在内存中,使得每个实例都可以很快命中缓存并返回,避免大量的请求集中到缓存服务中的单个节点上。由于秒杀业务中一般不会改变的信息,可以设置缓存有效期至活动结束之后;对于会频繁变动的数据,例如库存数据,可以每几秒更新一次 - Redis Cluster
缓存集群作为主要的缓存层,在本地缓存没有命中时从缓存集群中获取 - Database
没有命中缓存,最终请求进入数据库存储层
todo