随着互联网的发展,各项软件的客户量日益增多,当客户量达到一定峰值时,当数以万计的流量来临时,程序的顺利运行以及即时响应则显得尤为重要,就像双11那天的淘宝一样。那么,如何设计架构才能够抗住这千万级的流量。
老板让你抗住千万级流量,如何做架构设计?
首先,要在我们架构设计的时候建立一些原则。
1. 实现高并发
服务拆分:将整个项目拆分成多个子项目或者模块,分而治之,将项目进行水平扩展。
服务化:解决服务调用复杂之后的服务的注册发现问题。
消息队列:解耦,异步处理
缓存:各种缓存带来的并发
2. 实现高可用
集群、限流、降级
3. 业务设计
幂等:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用,就像数学里的数字1,多少次幂的结果都是1。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。
防重:防止同样的数据同时提交
除了在业务方向判断和按钮点击之后不能继续点击的限制以外,在服务器端也可以做到防重:
在服务器端生成一个唯一的随机标识号(Token<令牌>)同事在当前用户的Session域中保存这个令牌,然后将令牌发送到客户端的form表单中,在form表单中使用隐藏域来存储这个Token,表单提交的时候联通这个Token一起提交到服务器,然后在服务器端判断客户提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就重复提交了,此时服务器端就可以不处理重复提交的表单,如果相同则处理表单,处理完后清楚当前用户的Session域中存储的标识号。高可用高并发架构参考:高可用高并发的 9 种技术架构。
在下列情况中,服务器程序将拒绝处理用户提交的表单请求:
1)存储Session域中的Token与表单提交的Token不一致
2)当前用户的Session中不存在Token
3)用户提交的表单数据中没有Token。
状态机
软件设计中的状态机概念,一般是指有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
这里着重讲一下限流的概念和例子
限流的目的
限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统的可用性,一旦达到限制速率就可以拒绝服务。就像手机预售一样,假如要卖出3万台,只需要接收3万用户的请求就可以,其他的用户请求可以选择过滤,可以提示"当前服务器过忙,请稍后再试"的提示。推荐大家看这篇文章:接口限流算法:漏桶算法&令牌桶算法。
限流方式:
1. 限制瞬时并发数 : 比如在入口层(nginx添加nginx_http_limit_conn_module)来限制同一个ip来源的连接数,防止恶意***访问的情况。
2. 限制总并发数:通过配置数据库连接池、线程池大小来约束总并发数
3. 限制时间窗口内的平均速率:在接口层面,通过限制访问速率来控制接口的并发请求。
4. 其他方式:限制远程接口的调用速率、限制MQ的消费速率。
常用限流算法
1. 滑动窗口协议:一种常见的流量控制技术,用来改善吞吐量的技术。
滑动窗口协议的由来:
滑动窗口(sliding window)是一种流量控制技术。早期的网络通讯中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发送不了数据,所以就有了滑动窗口机制来解决此问题。 发送和接收方都会维护一个数据帧的序列,这个序列被称为窗口。
定义:滑动窗口协议(Sliding Window Protocol),属于TCP协议的一种应用,用于网络数据传输时的流量控制,以避免拥塞的发生。该协议允许发送方在停止并等待确认前发送多个数据分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输,提高网络吞吐量。
发送窗口:就是发送端允许连续发送的帧的序号表。发送端可以不等待应答而连续发送数据(可以通过设置窗口的尺寸来控制)
接收窗口:接收方允许接收的帧的序列表,凡是落在接收窗口内的帧,接收方都必须处理,落在接收窗口外的帧将被丢弃。接收方每次允许接收的帧数称为接收窗口的尺寸
演示地址:
https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/index.html
2. 漏桶:漏桶算法能强行限制数据的传输速率。
漏桶算法思路很简单,请求先进入到漏桶里,漏桶以一定的速度出水。当水请求过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。进入端无需考虑出水端的速率,就像mq消息队列一样,provider只需要将消息传入队列中,而不需要关心Consumer是否接收到了消息。
对于溢出的水,就是被过滤的数据,可以直接被丢弃,也可以通过某种方式暂时保存,如加入队列之中,像线程池里对溢出数据的4种处理机制一样
3. 令牌桶:属于控制速率类型的限流算法。
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
设置 Rate = 2 :每秒放入令牌的个数
桶的大小:100
这里用一个小demo来实现一下令牌桶
public class TokenDemo { //qps:每秒钟处理完请求的次数;tps:每秒钟处理完的事务次数 //代表qps是10; RateLimiter rateLimiter = RateLimiter.create(10); public void doSomething(){ if (rateLimiter.tryAcquire()){ //尝试获得令牌.为true则获取令牌成功 System.out.println("正常处理"); }else{ System.out.println("处理失败"); } } public static void main(String args[]) throws IOException{ /* * CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量,此值是线程将要等待的操作数(线程的数量)。 * 当某个线程为了想要执行这些操作而等待时, 它要使用 await()方法。 * 此方法让线程进入休眠直到操作完成。 * 当某个操作结束,它使用countDown() 方法来减少CountDownLatch类的内部计数器,计数器的值就会减1。 * 当计数器到达0时,它表示所有的线程已经完成了任务,这个类会唤醒全部使用await() 方法休眠的线程们恢复执行任务。 * * */ CountDownLatch latch = new CountDownLatch(1); Random random = new Random(10); TokenDemo tokenDemo = new TokenDemo(); for (int i=0;i<20;i++){ new Thread(()->{ try { latch.await(); Thread.sleep(random.nextInt(1000)); tokenDemo.doSomething(); }catch (InterruptedException e){ e.printStackTrace(); } }).start(); } latch.countDown(); System.in.read(); }}
执行结果:
正常处理正常处理正常处理正常处理正常处理处理失败正常处理处理失败处理失败处理失败正常处理处理失败正常处理处理失败正常处理正常处理正常处理正常处理处理失败处理失败
由此可见,当令牌不足时,会获取令牌失败,达到限流的效果。
4. 计数器:最简单的一种。通过控制时间段内的请求次数。