整理自己秒杀项目的问答点。

设计原则

单一责任制,秒杀模块不依赖其他模块,即使挂了正常下单也能走。

秒杀场景是一种读多写少的场景。

  1. 尽量将请求拦截在上游,在秒杀前将下单按钮置灰,秒杀开始后只能下单一次。

  2. 尽量使用缓存来缓解数据库的压力。

  3. 使用消息队列去做削峰填谷,同时异步去写数据库,降低数据库的压力。

数据库设计

item 商品信息表

item_stock 库存表

order_info 下单信息表

订单号有16位

前8位为时间信息,年月日

中间6位为自增序列

最后两位分库分表

promo 秒杀商品信息表

sequence_info

order_info表根据里面的step去存

stock_log

user_info 用户信息表

user_password 用户密码表

页面静态化

cdn的核心原理并将静态页面部署到cdn上,之后使用了phantomjs的无头浏览器方案实现了将静态请求和动态请求合并一同部署到cdn上,更进一步的将商品详情页的流量能力提升到极致;

页面静态化,其实就是将动态生成的jsp页面,变成静态的HTML页面,让用户直接访问。这样的好处就是:

  • 大大提升访问速度,不需要去访问数据库,或者缓存来获取哪些数据,浏览器直接加载渲染html页即可;

  • 搜索引擎更喜欢静的,更便于抓取,搜索引擎SEO排名更容易提高;

  • 从安全角度讲,静态网页不宜遭到黑客攻击,如果黑客不知道你网站的后台、网站采用程序、数据库的地址,静态网页, 更不容易受到黑客的攻击;

  • 从网站稳定性来讲,如果程序、数据库出了问题,会直接影响网站的访问,而静态网页就避免了如此情况,不会因为程序等,而损失网站数据,影响正常打开,损失用户体验,影响网站信任度;

服务降级

服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。

实际:

不实时显示库存。

例如用户抢购需要填写二维码。

不能及时反馈抢购成功消息,显示排队中。

接口防刷

Nginx

校验恶意请求

Nginx黑名单:过滤日志访问API接口的IP,统计每10分钟调用超过100次的IP,直接丢进nginx的访问黑名单

在 Nginx 上限制单 IP 单位时间的请求数,以及单 IP 的并发连接数。

隐藏接口

每次点击秒杀按钮,先从服务器获取一个秒杀验证值(接口内判断是否到秒杀时间),使用md5加密userID和秒杀ID然后再加盐。

Redis以缓存用户ID和商品ID为Key,秒杀地址为Value缓存验证值,过期时间设置1分钟左右的随机值。

用户请求秒杀商品的时候,要带上秒杀验证值进行校验。

单用户限制频率

每当用户访问一次下单接口就将用户的访问数+1,到达一定阈值就拒绝访问。

采用验证码

加入验证码来防止机器刷接口。

怎么解决超卖

使用redis服务器,将并发的请求转化成串行的请求,逐一处理每一个请求,原子操作减库存。
减库存sql,set库存=库存-1 where 库存>0。

  1. 在系统初始化时,或者定时任务,调用接口时将商品的库存数量加载到Redis缓存中
  2. 在Redis中设置一个key表示是否该秒杀商品有库存
  3. 接收到秒杀请求时,先判断key是否为空,如果为空在Redis中进行预减库存,当Redis中的库存不足时,直接返回秒杀失败,并将key设置为true;
  4. 将请求放入异步队列中,返回正在排队中;
  5. 服务端异步队列将请求出队,出队成功的请求可以生成秒杀订单,减少数据库库存,返回秒杀订单详情。

异步队列开启事务,如果失败在catch中将库存加回去,同时去掉标志位。

如果Redis超卖了就使用lua脚本

可以使用JVM缓存去做flag,但是需要用zookeeper。

分布式Session

对每个登录的用户分发Token,将Token放入Redis中,确保分布式的访问。

限流算法

主要使用Guava的RateLimiter进行限流,采用的是令牌桶机制。

为什么用令牌桶:

漏桶算法能够强行限制数据的传输速率,而令牌桶算法在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在令牌桶算法中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量

计数器算法

通过一个计数器 counter 来统计一段时间内请求的数量,并且在指定的时间之后重置计数器。

img

该方法实现简单,但是有临界问题。例如,允许一分钟内通过的请求数为 N,如果在重置计数器的前后一小段时间内分别请求 N 次,那么在这一小段时间内总共请求了 2N 次,超出了规定的 N 次。

img

滑动窗口算法

是计数器算法的一种改进,将原来的一个时间窗口划分成多个时间窗口,并且不断向右滑动该窗口。

img

在临界位置的突发请求都会被算到时间窗口内,因此可以解决计数器算法的临界问题。

img

漏桶算法

能够以恒定速率处理请求。

请求需要先放入缓存中,当缓存满了时,请求会被丢弃。

img

令牌桶算法

和漏桶算法的区别在于它是以恒定速率添加令牌,当一个请求到来时,先从令牌桶取出一个令牌,如果能取到令牌那么就可以处理该请求。

令牌桶的大小有限,超过一定的令牌之后再添加进来的令牌会被丢弃。

令牌桶算法允许突发请求,因为令牌桶存放了很多令牌,那么大量的突发请求会被执行。但是它不会出现临界问题,在令牌用完之后,令牌是以一个恒定的速率添加到令牌桶中的,因此不能再次发送大量突发请求。

img

Nginx

将静态网页部署在Nginx上,在静态网页方面,Nginx是比Tomcat快的。

使用反向代理动态请求到Tomcat上,Nginx可以管理后端的一个tomcat集群,然后以一个统一的域名供用户去访问

nginx动静分离服务器

  • location节点path特定resources:静态资源路径;
  • location节点其它路径:动态资源用;

如何使用动态资源?
nginx做反向代理服务器

  • 设置upstream server
  • 设置动态请求location为proxy pass路径;
  • 开启tomcat access log(访问日志)验证

命令

  • sbin/nginx -c conf/nginx.conf启动;
  • 修改配置后直接sbin/nginx -s reload无缝重启;

Nginx负载均衡算法

1. 轮询(默认)
每个web请求按照时间顺序逐一分配到不同的后端服务器,如果后端服务器over了,就自动剔除;

2. weight权重
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况,weight默认是1;

3. 最少链接
web请求会被转发到连接数最少的服务器上;

4. ip_hash
每个请求按访问ip的hash值分配,这样同一客户端连续的Web请求都会被分发到同一服务器进行处理,可以解决session的问题。当后台服务器宕机时,会自动跳转到其它服务器;

5. url_hash(第三方)
nginx按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存服务器、文件服务器、静态服务器时比较有效。缺点是当后端服务器宕机的时候,url_hash不会自动跳转的其他缓存服务器,而是返回给用户一个503错误;

6. fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配;

Nginx高性能的原因

Nginx多进程模型

Nginx高性能主要分为以下三点:

  • epoll多路复用完成非阻塞式的IO操作
  • master worker进程模型,允许其进行平滑重启和配置,不会断开和客户端连接
  • 协程机制,完成单进程单线程模型,并支持并发编程调用接口

管理员理解为root操作用户,用于启动管理nginx进程;
Master进程的主要功能:

  • 接收来自外界的信号;

  • 向各个worker进程发送信号;

  • 监控worker进程的运行状态;

  • 当worker进程在异常情况下退出后,会自动重启新的worker进程;

nginx会启动一个master进程,然后根据配置文件内的worker进程的数量去启动相应的数量的worker进程,master进程和worker进程是一个父子关系;master进程用来管理worker进程,worker进程才是用来管理客户端连接的。

Master进程会先创建好对应的socke去监听对应的端口,然后再fork出多个worker进程,master会启动一个epoll的多路复用模型;当client想要在socket端口建立经典的TCP三次握手建立连接的时候,对应的epoll多路复用会产生一个回调,通知所有的可以accept的worker进程,但只有一个worker进程会成功,其它的都会失败。

Nginx提供了一把共享锁accept_mutex来保证同一时刻只有一个work进程在accept连接,从而解决集群问题;当一个worker进程accept这个连接后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接;

协程机制

一个线程可以有多个协程,协程是线程的内存模型

  • 依附于线程的内存模型,切换开销小;

  • 遇阻塞及归还执行权,代码同步,调用新的不阻塞的协程;

  • 无需加锁;

Redis

怎么保证Redis缓存和数据库的一致性?

在其他一般读大于写的场景,一般处理的原则是:缓存只做失效,不做更新。

失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

更新:先把数据存到数据库中,成功后,再让缓存失效。

雪崩

更新库存问题

MQ

Producer解决消息生产的问题,Consumer消息的消费端
Broker
相当于一个中间人,由topic和MessageQueue组成,任何一条rocketmq的消息都是隶属于某一个topic,一个topic可以被一个messagebroker管理,也可以被多个messagebroker管理;

Broker向Nameserver发送注册请求,broker的ip和负责的topic,queue;
每一个broker至少有一个queue,producer从Nameserver上发现broker1;

采用负载轮询的方式第一次请求到queue1中,生成一个message1,第二次请求到queue2,生成一个message2,同时consumer会向这两个queue分别建立长连接。
当producer做对应投递时,consumer会被唤醒,拉取对应的message,这种方式被称为长轮询;
Consumer group的作用:以queue为单位作为一个消息的管理,当consumer消费完一个message的时候会回复一个消息给对应的queue,并且将对应的message2变为已经消费成功要被干掉的状态;

若对应的一个queue被多个consumer消费的情况下,势必会造成一个同步的问题,存在一个锁竞争的机制,rocketmq采用的是以queue为单位平均的分配给consumer,所以设计一个好的中间件就是为了保证queue和consumer的数量相等;

当有多个consumer group,一个订单系统就属于一个group,另外一个consumer group就属于商品系统;生产者端并不知道生成的消息对应的消费端是哪个系统,只会无脑的投消息,关注这个消息的人指出来即可。topicA还是以consumer_group为管理的基础单元,一个queue1可以被一个consumer group中的一个consumer所消费,也可以被另一个consumer group中的consumer所消费,以consumer_group去做对应消息的消费和管理

如果对应的broker1产生了任何异常,producer知道broker掉线了,没办法投递对应的消息;

broker2作为broker1的slave,平时不对外进行服务,只做消息的从库,一旦对应的message1被消费;一旦broker1发生异常,nameserver感知到会将broker2变为主库,并且通知producer和consumer端,让其通过broker2去接管对应的消费,slave和master之间的数据可以是同步,也可以是异步。

同步的话,producer生成在broker1中生产message1成功,也要broker2中备份message1也能够成功,性能偏低;

主broker1作为生产成功\消费成功即可,roker2做异步复制即可。只要网络的延迟小,对应的cpu处理速度快,是不会发生消息丢失的情况。但是在分布式的环境下,没有办法同时保证强一致性和可用性。如果选择强可用性肯定会降低强一致性,当发生主备切换的时候可能会发生消息的丢失。

  • 消息队列事务型消息基于 “二阶段” 消息实现;
  • 事务型消息是否投递与消息发布者本地事务状态保持一致;
  • 事务型消息状态回查是保证了 “事务型消息” 的严谨性。
  • 什么叫事务性呢?就是保证数据库的事务提交,只要事务提交了就一定会保证消息发送成功。数据库内事务回滚了,消息必定不发送,事务提交未知,消息也处于一个等待的状态;

Tomcat

server.tomcat.accept-count:等待队列长度。默认100;
server.tomcat.max-connections:最大可被连接数,默认10000
server.tomcat.max-threads:最大工作线程数,默认200
server.tomcat.min-spare-threads:最小线程数,默认10
默认配置下,连接超过10000后出现拒绝连接情况;
默认配置下,触发的请求超过200+100后拒绝处理;
定制化内嵌Tomcat开发

keepAliveTimeOut:多少毫秒后不响应的断开keepalive(设置在服务端上)
maxKeepAliveRequests:多少次请求后keepalive断开失效
使用WebServerFactoryCustomizer定制化内嵌tomcat配置

CAP

分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。

一致性

一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。

对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。

可用性

可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。

在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。

分区容忍性

网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。

在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。

权衡

在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在可用性和一致性之间做权衡。

可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,

  • 为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性;
  • 为了保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致。

BASE

BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。

BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

基本可用

指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。

例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。

软状态

指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时延。

最终一致性

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。

ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。

在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。