本文主要是介绍大流量和高并发应对手段总结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
大流量和高并发的常规应对手段
扩容、动静分离、缓存、服务降级和限流。
限流的常用算法和实践思路
- 目前主流的算法主要有三种:令牌桶算法、漏桶算法和计数器算法。
- 令牌桶算法:主要限制流量的流入速率,允许出现一定程度的突发流量。Nginx的限流模块就是使用的这种算法实现的。
- 每秒会有r个令牌按照固定速率放入桶中。
- 桶的容量是固定不变的,如果桶满了再放入令牌,则溢出。
- 若桶中的可用令牌不足,则改请求会被进行限流处理(被抛弃或缓存)。
- 漏桶算法:主要限制流量的流出速率,并且流出速率是固定不变的
- 可以以任意速率向桶中流入水滴。
- 桶的容量是固定不变的,如果桶满了则溢出。
- 按照固定的速率从桶中流出水滴。
- Google的Guava也实现了基于令牌桶算法那样的平均速率限流,RateLimiter抽象类。
- Nginx可以使用限流模块在接入层实现令桶牌算法限流,
- limit_zone 定义每个IP的session空间大小。
- limit_zeq_zone定义每个IP每秒允许发起的请求数。
- limit_conn 定义每个IP能够发起的并发连接数。
- limit_req 等待处理的请求队列数量。
- 生产环境中的商品抢购可以使用计数器算法,具体不同的sku限流规则配置在配置中心内,支持动态更改。可抢购次数的扣减操作,既可以用redis,也可以用JVM。如果是集群并且选择用JVM,则要根据总并发数量除以集群数量,得出单台机器的并发数。(比如总并发数5000,集群机器10台,则每台机器的并发为5000/10=500)。
抢购商品高并发读需求
对于一件抢购商品的流量来说,因为key是同一个,所以流量必然会都引入到同一个redis缓存节点中,这时就容易出现单点故障。因此有下面两种解决方式:
1. 在每个master节点都挂slave从节点,当主节点挂了可以自动顶上。
2. 多级Cache方案,多用LocalCache来过滤掉一部分流量。
- 本地缓存一般只缓存一些热点商品数据,缓存内容一般是商品详情和商品库存。
- 本地缓存跟分布式缓存的同步一般有两种方式:一种是定时主动拉取更新策略。这种会存在一定时间的不一致,要视业务情况而定,例如库存,暂时的不一致导致超卖,单到真正下单的时候还会再进行库存的判断,所以影响较小,可以接受。这种方式要注意关掉缓存的定时失效,防止当用户流量突然过大,都到分布式缓存中拉取数据;第二种方式是每次商品更新,都发布一个消息,订阅此消息的节点监听到后再更新本地缓存的内容。
实时热点自动发现方案
可以将交易系统产生的相关数据,以及在上游系统中埋点上报的相关数据异步写入日志系统中,然后通过实时热点自动发现平台对收集到的日志数据做调用次数统计和热点分析。数据符合热点条件后,就立即通知交易系统做好热点保护。
redis使用watch命令实现高并发抢购需求
- 一般高并发这里,不用悲观锁,会迅速增加系统资源;而使用队列,容易造成请求堆积,内存效果过快。所以一般使用乐观锁,可以用redis的watch命令实现。
- watch命令会监视给定的key,当exec时,如果监视的key从调用watch后发生过变化,则事务会失败。注意watch的可以是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然exec,discard,unwatch命令都会清除连接中的所有监视。
缓存雪崩,缓存穿透,缓存并发,缓存预热,缓存算法
- 缓存雪崩:可能是因为数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。解决思路:
- 加锁计数(即限制并发的数量,可以用semphore)或者起一定数量的队列来避免缓存失效时大量请求并发到数据库。但这种方式会降低吞吐量。
- 分析用户行为,然后失效时间均匀分布。或者在失效时间的基础上再加1~5分钟的随机数。
- 如果是某台缓存服务器宕机,则考虑做主备。
- 缓存穿透:指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库中查询。解决思路:
- 如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。设置一个过期时间或者当有值的时候将缓存中的值替换掉即可。
- 可以给key设置一些格式规则,然后查询之前先过滤掉不符合规则的Key。
- 缓存并发:如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。解决思路:
- 对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。
- 缓存预热:目的就是在系统上线前,将数据加载到缓存中。解决思路:
- 数据量不大的话,在系统启动的时候直接加载。
- 自己写个简单的缓存预热程序。
- 缓存算法:
- FIFO算法:First in First out,先进先出。原则:一个数据最先进入缓存中,则应该最早淘汰掉。也就是说,当缓存满的时候,应当把最先进入缓存的数据给淘汰掉。
- LFU算法:Least Frequently Used,最不经常使用算法。
- LRU算法:Least Recently Used,近期最少使用算法。
- LRU和LFU的区别。LFU算法是根据在一段时间里数据项被使用的次数选择出最少使用的数据项,即根据使用次数的差异来决定。而LRU是根据使用时间的差异来决定的。
协程(纤程)Fiber
- 协程概念:一种用户态的轻量级线程,其实就是单线程,指定执行整个函数中到一部分然后就先出去执行别的,等条件满足时,协程下次更新帧到了再继续往下执行。优点是无需线程上下文切换的开销,充分开发了单CPU的能力,资源占用低,适合高并发I/O。缺点也很明显,就是没办法利用多CPU的优势。
- 框架:Quasar,调度器使用ForkJoinPool来调度这些fiber。Fiber调度器FiberScheduler是一个高效的、work-stealing、多线程的调度器。
- 场景:服务A平时需要调用其他服务,但其他服务在并发高的时候延迟很严重。
- 一开始可以用httpClient连接池+线程池来处理,但如果调用服务的时候延迟太高或者超时,则会导致服务A的吞吐量会特别差。原因主要是一般一个链接由一个线程来处理,是阻塞的,所以在线程池数有限的情况下,吞吐量肯定上不去。并且当所有线程都I/O阻塞的时候,会很浪费CPU资源,并且CPU会一直做无用的上下文切换。
- 这时候可以考虑协程来替换。
- 参考文章:http://blog.csdn.net/qq910894904/article/details/41699541,
http://blog.csdn.net/hj7jay/article/details/51980038,
http://blog.csdn.net/zdy0_2004/article/details/51323583
ForkJoinPool线程池
- Fork/Join框架是Java7提供了的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
- 工作窃取(work-stealing)算法是Fork/Join框架最重要的特性。一般一个线程会对应一个任务队列,当处理较快的线程处理完自己的任务之后,就会窃取另外一个处理比较慢的线程对应的任务,这时候会存在两个线程同时处理一个队列的情况,所以任务队列一般使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。优点是充分利用线程进行并行计算,并减少了线程间的竞争。
这篇关于大流量和高并发应对手段总结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!