如何设计一个秒杀系统?

​ 1这个项目主要是针对处理高并发问题。主要考虑的问题有以下几个方面:

一、正确性。核心问题就是防止超卖,和重复下单。

二、高并发。主要是采用Redis进行缓存常用查询、消息队列异步下单、页面资源静态化等方面减去数据库压力。

三、安全性。主要有动态地址生成和接口放刷,双重MD5加密密码。

四、高可用性。一方面使用Redis集群的主从复制和主从切换保证redis的高可用性,另一方面,为防止redsi服务器宕 机,使用限流来防止mysql承受过多的请求。

一如何防止超卖:

​ 1、利用数据库自带排他锁,当减库存的时候,进位where判断,只有库存余量大于0的时候才进行进库存; update goods set num = num - 1 WHERE id = 1001 and num > 0; 2、也可以可用乐观锁CAS版本号机制。select version from goods WHERE id= 1001;update goods set num = num - 1, version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version);

二、服务器抗压思路:

​ 一、使用消息队列、异步生成订单;

​ 二、redis库存量预缓存。只将少量的请求流入到服务器。如果全部卖完,拦截请求。

​ 三、生成订单前,进行一系列的检验:是否还有库存,是否重复下单,这些数据都可以缓存。

三、前端设计

静态资源缓存:将活动页面上的所有可以静态的元素全部静态化,尽量减少动态元素;通过CDN缓存静态资源,来抗峰值。在url后面加上?即可。

禁止重复提交:前端:用户提交之后按钮置灰,禁止重复提交;后端:在进入页面时,服务器生成token并存到缓存或者session中,form表单使用隐藏域来存储这个token,提交之后带有token.后端收到这个token,看是否与服务器生成的token一致,如果不一致就是重复提交。如果一致,处理完之后清除token.

服务器返回表单页面时,会先生成一个subToken保存于session,并把该subToen传给表单页面。当表单提交时会带上subToken,服务器拦截器Interceptor会拦截该请求,拦截器判断session保存的subToken和表单提交subToken是否一致。若不一致或session的subToken为空或表单未携带subToken则不通过。

首次提交表单时session的subToken与表单携带的subToken一致走正常流程,然后拦截器内会删除session保存的subToken。当再次提交表单时由于session的subToken为空则不通过。从而实现了防止表单重复提交。

用户限流:某一时间段内只允许用户提交少数次请求,IP限流(Nginx设置IP地址限流)。

​ 中间代理层:**

​ 利用负载均衡(例如反响代理Nginx等)使用多个服务器并发处理请求,减小服务器压力。

​ (正向代理代理客户端VPN,反向代理代理服务器。NGINX)

​ 横向增加服务器数量,然后将请求分发到各个服务器上,将原先请求集中到单个服务器上的情况改为将请求分发到多个服务器上,将负载分发到不同的服务器,也就是我们所说的负载均衡。普通轮询算法、比例加权轮询、ip路由负载、基于服务器响应时间负载分配、根据域名负载。

轮询(默认):每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。

指定权重:指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。

IP绑定ip_hash:每个请求按ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。

url_hash:按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。

fair:按后端服务器的响应时间来分配请求,响应时间短的优先分配。

服务层:

业务分离**:**将秒杀业务系统和其他业务分离,单独放在高配服务器上。

​ 采用消息队列缓存请求:将大流量请求写到消息队列缓存,利用服务器根据自己的处理能力主动到消息缓存队列中抓取任务处理请求。

利用缓存应对读请求:对于读多写少业务,大部分请求是查询请求,所以可以读写分离,利用缓存分担数据库压力。

数据库层:

​ 上游就需要把请求拦截掉,数据库层只承担“能力范围内”的访问请求。所以,上面通过在服务层引入队列和缓存,让最底层的数据库高枕无忧。可以对数据库进行优化,减少数据库压力。

​ 如果redis挂掉的话,如果提高数据库的并发能力:

业务拆分:将不同功能的模块拆分,使用不同的数据库。

​ MySQL主从复制,读写分离

分表分库:

其他策略:为请求分配成功状态或者分配秒杀资格,将没有资格的请求全部过滤,只有有资格的才能参与秒杀。说到底的秒杀这个高并发,并不是真正的处理高并发请求,而是如何应对高并发。将大量请求拦截然后放小量请求到数据库执行抢单是完全可以的,不用担心请求丢失的问题。

四、怎么保证**redis**缓存和数据库的一致性

延时双删;

​ 存在不一致问题的,基本都是库存量。秒杀系统的设计,最重要的是不能超卖,这个问题我们已经谈过,用mysql排他锁或者乐观CAS版本号机制可以防止。而即使redis库存量比实际mysql库存量大,依然不会超卖。而redis库存量比mysql库存量小,可能发生没少卖的情况。少卖,问题不大。如果不能少卖,可以将redis预库存调大,他主要起到拦截请求降流的作用,一致不一致问题不大。

五、安全性问题

1、动态地址生成

​ 2、接口防刷

六、消息队列

防止重复消费:重复消费在消息队列所存在的问题中,从来都不是一个严重的问题。如果是消息是读,那多消费一次没啥影响。如果是写,例如我们这个订单生成,消费之前,查询一下是否之前已经存在用户ID商品ID构成的订单,我们可以将生成的订单存入缓存,所以查询一次也不费劲。

消息的消费结果如何返回给消息发送方:客户端轮询订单生成结果。

消息丢失:秒杀系统中,本来就是万中选一的,丢失无所谓。如果是重要的信息,我们可以从三个角度来避免。如果是发送者丢失,开启confirm机制,如果队列丢失,开始queue持久化和消息持久化。如果是消费者丢失,关闭自动ACK,当我们消费完之后,调用API给queue发送确认信息。

七、秒杀流程、画架构图

​ 1、登录进入商品列表页面,静态资源缓存

​ 2、点击进入商品详情页面,静态资源缓存,ajax获取验证码(服务器生成三个数的预算,并将结果缓存到redis);

​ 3、点击秒杀, 将验证码结果和商品ID传给后端,如果结果正确。动态生成随机串UUID,结合用户ID和商品ID存入redis,并将path传给前端。前端获取path后,再根据path地址调用秒杀服务;

​ 4、服务端获取请求的path参数,去查缓存是否在;

​ 5、如果存在,预减redis库存,如果还有库存,看是否已经生成订单,没有的话就将请求入消息队列。

​ 6、从消息队列中取消息:获取商品Id和用户ID,判断库存,重复下单;然后下单。

​ 7、下单:减库存,生成订单;

​ 8、前端轮询订单生成结果。50ms继续轮询或者秒杀是否成功和失败;

八、优化策略

​ 多服务器负载均衡、

简单介绍一下Nginx

Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。 Nginx主要提供反向代理、负载均衡、动静分离(静态资源服务)等服务。下面我简单地介绍一下这些名词。

​ 正向代理:某些情况下,代理我们用户去访问服务器,需要用户手动的设置代理服务器的ip和端口号。正向代理比较常见的一个例子就是 VPN了。

​ 反向代理:是用来代理服务器的,代理我们要访问的目标服务器。代理服务器接受请求,然后将请求转发给内

部网络的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。

​ 负载均衡

​ 在高并发情况下需要使用,其原理就是将并发请求分摊到多个服务器执行,减轻每台服务器的压力,多台服务器(集

群)共同完成工作任务,从而提高了数据的吞吐量。Nginx支持的weight轮询(默认)、ip_hash、fair、url_hash这四种负载均衡调度算法,感兴趣的可以自行查阅。负载均衡相比于反向代理更侧重的时将请求分担到多台服务器上去,所以谈论负载均衡只有在提供某服务的服务器大于两台时才有意义。

动静分离

​ 动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以

后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。

22.设计秒杀方案(从高并发、快速响应、高可用三方面回答,高并发(增加网络带宽、DNS域名解析分发多台服务器、使用前置代理服务器ngnix、CDN内容分发、数据库查询优化(读写分离、分库分表)),快速响应(缓存服务器(memcached、redis)、能使用静态页面就用静态页面,减少容器解析、把常访问的图片等内容缓存)、高可用(热备,如数据库服务器的热备、集群监控(如使用zabbix,重点关注IO、内存、带宽和机器load)))

服务器返回表单页面时,会先生成一个subToken保存于session,并把该subToen传给表单页面。当表单提交时会带上subToken,服务器拦截器Interceptor会拦截该请求,拦截器判断session保存的subToken和表单提交subToken是否一致。若不一致或session的subToken为空或表单未携带subToken则不通过。

首次提交表单时session的subToken与表单携带的subToken一致走正常流程,然后拦截器内会删除session保存的subToken。当再次提交表单时由于session的subToken为空则不通过。从而实现了防止表单重复提交。

缓存、降级和限流

在开发高并发系统时,有三把利器用来保护系统:缓存、降级和限流:

​ 缓存:缓存的目的是提升系统访问速度和增大系统处理容量

​ 降级:降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开

​ 限流:限流的目的是通过对并发访问请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。

​ 计数器法:设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter。缺点:统计的精度太低,无法处理临界问题。如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为突刺现象

public boolean grant() {

​ long now = getNowTime();

​ if (now < timeStamp + interval) {

​ // 在时间窗口内

​ reqCount++;

​ // 判断当前时间窗口内是否超过最大请求控制数

​ return reqCount <= limit;

​ } else {

​ timeStamp = now;

​ // 超时后重置

​ reqCount = 1;

​ return true;

​ }

}

滑动窗口算法:将窗口更加细分,每个窗口都有自己的计数器,当总计算达到限定时,限流。这个滑动窗口只是将计算法变得更平滑而已。本质一样。

漏斗法:将容器比作一个漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。

在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。通过Google开源的guava包,我们可以很轻松的创建一个令牌桶算法的限流器。

Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用。RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率,它支持两种获取permits接口,一种是如果拿不到立刻返回fals(tryAcquire()),一种会阻塞等待一段时间看能不能拿到(tryAcquire(long timeout, TimeUnit unit))。

缺点:传统的方式整合RateLimiter有很大的缺点:代码重复量特别大,而且本身不支持注解方式。

ES:

Elasticsearch是一个近乎实时的搜索平台。这意味着从索引文档到可以搜索的时间只有轻微的延迟(通常是1秒).

集群是一个或多个节点(服务器)的集合. 节点是一个单独的服务器,它是集群的一部分,存储数据,并参与集群的索引和搜索功能。

索引是具有某种相似特征的文档的集合。例如,你可以有一个顾客数据索引,产品目录索引和订单数据索引。文档是可以被索引的基本信息单元。文档用JSON表示,有多个field,如年龄,性别,地址。

Elasticsearch提供了将你的索引细分为多个碎片(或者叫分片)的能力。在创建索引时,可以简单地定义所需的分片数量。每个分片本身就是一个功能完全独立的“索引”,可以驻留在集群中的任何节点上。Shards & Replicas.每个分片又有副本。

正向索引是通过ke找value,反向索引则是通过value找key

首先将文本分割成一系列被称为语汇单元(token)的独立原子元素,此过程即为文档分析,然后建立倒排索引,也就是每个term关键词出现在哪些文档之中。ID TERM DOCUMENT List.,Elasticsearch分别为每个field都建立了一个倒排索引。

Elasticsearch为了能快速找到某个term,将所有的term排个序,二分法查找term,logN的查找效率,就像通过字典查找一样,这就是Term Dictionary。又有一个Term Index,就像字典里的索引页一样,A开头的有哪些term,分别在哪页,可以理解term index是一颗树这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。再结合FST(Finite State Transducers的压缩技术,可以使term index缓存到内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term大大减少了磁盘随机读的次数

用FST压缩term index外,对posting list也有压缩技巧,如bitmap

联合索引直接利用跳表(Skip list)的数据结构快速做“与”运算,或者利用上面提到的bitset按位“与”。

shard = hash(document_id) % (num_of_primary_shards)

字典树:

根节点不包含字符,除根节点外每一个节点都只包含一个字符。

从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。

每个节点的所有子节点包含的字符都不相同。

系统设计问题:

​ 1、让你系统的设计一个高并发的架构,你会从哪几个方面考虑?

​ 2、一个千万级的APP,你要搞定关注和粉丝列表,你用什么来做。要求最后一个关注的在最前面。新增和取关都要比较快的反馈你怎么做?如果一个人关注了之后,服务器宕机了怎么办?

​ 3设计一个榨汁机类,面向对象怎么设计

​ 4、OOD design:计费停车场

​ 5、多个服务器间共享session的解决方案

​ 问了new一个对象的加载顺序, 答了从父类到子类的加载过程 静态变量和静态块, 哪个先加载, 答了静态变量。

​ 假设有这么一个场景,有一条新闻,新闻的评论量可能很大,如何设计评论的读和写

​ 你如果写用InnoDB,读用Myisam的话,主从同步怎么做

​ 假设如果有同一时间海量数据入库,你怎么做(期间扯到了鹿晗关晓彤,这种微博大 V给他安排上,还提了消息队列做削峰)

​ 你对Elasticsearch有什么了解

然后就问开放题了,问12306怎么处理大量请求。

问12306怎么处理大量的读请求。

问12306为什么有的时候会有看的时候有票,但是买的时候没票的情况,问我有可能会是什么原因。

问12306可能存在第三方软件帮忙抢票,怎么防止。