高并发系统 限流 缓存 降级 设计方案

高并发系统设计方案

高并发系统设计一般会考虑三个方面:限流、缓存、降级

限流:控制在一定时间内的访问量,比如秒杀,这种场景下访问量过于庞大,使用缓存或者降级根本无法解决访问量巨大的问题,那么只能选择限流

缓存:缓存设计是我们常用的减轻服务器压力的方案,常用的缓存有 redis(分布式)、 memcache(分布式)、google guava cache(本地缓存)等

降级:高并发高负载情况下,选择动态的关闭一些不重要的服务拒绝访问等,为重要的服务节省资源,比如双11当天淘宝关闭了退款等功能

限流

常见的限流算法:令牌桶、漏桶、计数器

接入层限流:指请求流量的入口,该层的主要目的有 负载均衡、非法请求过滤、请求聚合、服务质量监控等等

Nginx接入层限流:使用Nginx自带了两个模块,连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module

应用层限流:比如TPS/QPS超过一定范围后进行控制,比如tomcat可配置可接受的等待连接数、最大连接数、最大线程数等

令牌桶:Guava框架提供了令牌桶算法实现,可直接拿来使用,Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现,代码如下:


   
   
  1. import java.util.concurrent.atomic.AtomicInteger;
  2. import org.junit.Test;
  3. import com.google.common.util.concurrent.RateLimiter;
  4. /**
  5. * 固定速率请求,每200ms允许一个请求通过
  6. *
  7. * @date 2019-10-14 16:54
  8. **/
  9. public class FixedRequestLimitDemo {
  10. @Test
  11. public void fixedRequestTest () {
  12. // 表示1秒内产生多少个令牌,即1秒内产生5个令牌,控制每200ms一个请求
  13. RateLimiter rateLimiter = RateLimiter.create( 5);
  14. AtomicInteger counter = new AtomicInteger( 0);
  15. for ( int i = 0; i < 15; i++) {
  16. // 同时开启15个线程访问
  17. new Thread(() -> fixedRequest(rateLimiter, counter)).start();
  18. }
  19. try {
  20. Thread.sleep( 3000);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. private void fixedRequest (RateLimiter rateLimiter, AtomicInteger counter) {
  26. double time = rateLimiter.acquire();
  27. if (time >= 0) {
  28. System.out.println( "时间:" + time + " ,第 " + counter.incrementAndGet() + " 个业务处理");
  29. }
  30. }
  31. }

计数器:使用计数器方案简单粗暴的实现限流,使用 google guava cache(本地缓存),代码如下:


   
   
  1. import java.util.concurrent.TimeUnit;
  2. import java.util.concurrent.atomic.AtomicLong;
  3. import org.junit.Test;
  4. import com.google.common.cache.CacheBuilder;
  5. import com.google.common.cache.CacheLoader;
  6. import com.google.common.cache.LoadingCache;
  7. /**
  8. * 限流demo
  9. *
  10. * @date 2019-10-12 17:26
  11. **/
  12. public class LimitDemo {
  13. @Test
  14. public void limitTest () {
  15. // 本地缓存、key (Long)表示当前时间秒、value (AtomicLong)表示请求计数器
  16. LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder().
  17. expireAfterAccess( 2, TimeUnit.SECONDS).build(
  18. new CacheLoader<Long, AtomicLong>() {
  19. @Override
  20. public AtomicLong load (Long aLong) throws Exception {
  21. return new AtomicLong( 0);
  22. }
  23. }
  24. );
  25. for ( int i = 0; i < 15; i++) {
  26. // 同时开启15个线程访问
  27. new Thread(() -> requestLimit(counter)).start();
  28. }
  29. try {
  30. // 这里休眠是为了多线程全部执行完输出结果
  31. Thread.sleep( 2000);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. }
  36. /**
  37. * 请求限制,本方法加锁是为了控制缓存LoadingCache get数据时候的并发
  38. *
  39. * @param counter
  40. */
  41. private synchronized void requestLimit (LoadingCache<Long, AtomicLong> counter) {
  42. try {
  43. // 流量限制数量
  44. long limit = 10;
  45. // 当前秒数
  46. long currentSecond = System.currentTimeMillis() / 1000;
  47. if (counter.get(currentSecond).incrementAndGet() > limit) {
  48. // 超出每秒内允许访问10个的限制
  49. System.out.println( "第 " + counter.get(currentSecond) + " 个请求超出上限,限流了");
  50. return;
  51. }
  52. System.out.println( "第 " + counter.get(currentSecond) + " 个业务处理");
  53. } catch (Exception e) {
  54. e.printStackTrace();
  55. }
  56. }
  57. }

缓存

缓存方案可以有效地减轻服务器压力,但是它的设计也有一些必须考虑的问题,例如缓存雪崩、缓存击穿、缓存穿透、缓存预热、缓存降级、资源隔离

缓存雪崩

设置缓存时使用了相同的过期时间,导致大量的缓存在同一时刻同时失效,请求全部访问了DB(数据层),DB瞬间压力过大而宕机,从而引起一系列的严重后果

解决方案

1、使用锁或者队列的方式控制多线程同时对DB的读写,即避免失效时所有请求一下子全部访问到DB

2、缓存失效时间分散开,即设置缓存时设置不同的过期时间(原有的缓存时间上增加一个随机数),避免同一时刻大量缓存失效

3、缓存数据增加缓存失效标记,如果缓存标记失效,则更新数据缓存

使用加锁一般适用于并发量不是特别大的场景,伪代码如下:


   
   
  1. //伪代码
  2. public object getProductList () {
  3. int cacheTime = 30;
  4. String cacheKey = "product_list";
  5. String lockKey = cacheKey;
  6. String cacheValue = CacheHelper.get(cacheKey);
  7. if (cacheValue != null) {
  8. return cacheValue;
  9. } else {
  10. // 对lockKey加锁
  11. synchronized(lockKey) {
  12. cacheValue = CacheHelper.get(cacheKey);
  13. if (cacheValue != null) {
  14. return cacheValue;
  15. } else {
  16. //这里一般是sql查询数据
  17. cacheValue = getProductListFromDB();
  18. CacheHelper.Add(cacheKey, cacheValue, cacheTime);
  19. }
  20. }
  21. return cacheValue;
  22. }
  23. }

注意:加锁排队仅仅是减轻了数据库的压力,但是并没有提高系统吞吐量,它不仅要解决分布式锁的问题,还会产生线程阻塞问题,所以用户体验比较差!因此,在真正的高并发场景下很少使用!

缓存数据增加缓存失效标记,伪代码如下:


   
   
  1. //伪代码
  2. public object getProductList () {
  3. int cacheTime = 30;
  4. String cacheKey = "product_list";
  5. // 缓存标记
  6. String cacheSign = cacheKey + "_sign";
  7. String sign = CacheHelper.Get(cacheSign);
  8. // 获取缓存值
  9. String cacheValue = CacheHelper.Get(cacheKey);
  10. if (sign != null) {
  11. //未过期,直接返回
  12. return cacheValue;
  13. } else {
  14. CacheHelper.Add(cacheSign, "1", cacheTime);
  15. // 开启后台线程来更新缓存
  16. ThreadPool.QueueUserWorkItem((arg) -> {
  17. // sql查询数据
  18. cacheValue = getProductListFromDB();
  19. //日期设缓存时间的2倍,用于脏读
  20. CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);
  21. });
  22. return cacheValue;
  23. }
  24. }

注意:缓存标记的失效时间设置为缓存数据失效时间的一半,这样根据缓存标记后台线程提前更新缓存数据,在这之前还可以返回旧的缓存数据,这种方案对内存要求高,每个缓存都要设置一个对应的缓存标记

缓存击穿

正好要过期的key在某一个时刻被高并发的访问,即某时刻的热点数据,key正好过期了需要请求DB回写到缓存中去,此时大量的请求都访问到DB,DB瞬间压力过大也崩掉了。这里和缓存雪崩不同的是缓存击穿针对的是某一个key,而缓存雪崩针对的是多个key

解决方案

1、使用互斥锁

2、缓存不过期,使用value内部的过期时间来控制过期更新缓存值

使用互斥锁,伪代码如下:


   
   
  1. // 1、redis setnx 实现
  2. public String getValue (key) {
  3. String value = redis.get(key);
  4. if(value != null){
  5. return value;
  6. }
  7. // 缓存值过期,获取锁,设置3min的超时,防止del操作失败的时候产生死锁
  8. if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {
  9. // 代表获取锁成功
  10. value = db.get(key);
  11. redis.set(key, value, expire_secs);
  12. redis.del(key_mutex);
  13. return value;
  14. }
  15. // 没有获取到锁表示其他线程已经重新设置缓存了,此时重试获取缓存即可
  16. try {
  17. Thread.sleep( 50);
  18. //重试
  19. getValue(key);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. // 2、memcache 实现
  25. public String getValue (String key) {
  26. String value = memcache.get(key);
  27. if (value != null) {
  28. return value;
  29. }
  30. // 加锁
  31. if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
  32. value = db.get(key);
  33. memcache.set(key, value);
  34. memcache.delete(key_mutex);
  35. return value;
  36. }
  37. // 没有获取到锁表示其他线程已经重新设置缓存了,此时重试获取缓存即可
  38. try {
  39. Thread.sleep( 50);
  40. //重试
  41. getValue(key);
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. }

缓存不过期,使用value内部过期值更新缓存,伪代码如下:


   
   
  1. /**
  2. * redis 实现
  3. * <p>
  4. * V 对象中有两个属性,value 是缓存值,timeout 是缓存过期更新时间
  5. *
  6. * @param key
  7. * @return
  8. */
  9. public String getValue (String key) {
  10. V v = redis.get(key);
  11. String value = v.getValue();
  12. long timeout = v.getTimeout();
  13. if (timeout > System.currentTimeMillis()) {
  14. // 缓存值没有到缓存过期时间,直接返回
  15. return value;
  16. }
  17. // 缓存值过期,异步更新后台执行
  18. threadPool.execute( new Runnable() {
  19. public void run () {
  20. String keyMutex = "mutex:" + key;
  21. if (redis.setnx(keyMutex, "1", 3 * 60) == 1) {
  22. String newValue = db.get(key);
  23. redis.set(key, newValue);
  24. redis.delete(keyMutex);
  25. }
  26. }
  27. });
  28. // 此时直接返回旧value值
  29. return value;
  30. }

此方法的优点是并发性能好,缺点是不能及时的获取到最新的缓存值,有点延迟

缓存穿透

查询一个数据库中不存在的数据,此时缓存中也不会设置缓存值,那么所有请求都会直接查询到DB,将好像是缓存穿透了一样,请求过大将会导致DB宕机引起严重后果。黑客可以利用这种不存在的key来频繁请求我们的应用,拖垮应用服务器

解决方案

1、布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

2、查询到数据为空的数据也设置到缓存系统中,缓存时间可以设置的相对短一些,这样子的话缓存就可以起作用,挡掉了直接访问DB的压力

查询到数据为空的数据也设置到缓存系统,伪代码如下:


   
   
  1. public String getValue (key) {
  2. String value = redis.get(key);
  3. if (value != null) {
  4. return value;
  5. }
  6. // 缓存值过期,获取锁,设置3min的超时,防止del操作失败的时候产生死锁
  7. if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {
  8. // 代表获取锁成功
  9. value = db.get(key);
  10. if (value == null) {
  11. // value 为空时也缓存起来
  12. value = String.empty;
  13. }
  14. redis.set(key, value, expire_secs);
  15. redis.del(key_mutex);
  16. return value;
  17. }
  18. // 没有获取到锁表示其他线程已经重新设置缓存了,此时重试获取缓存即可
  19. try {
  20. Thread.sleep( 50);
  21. //重试
  22. getValue(key);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. }

缓存预热

缓存预热就是系统上线后,将需要用的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候再去查询DB设置数据缓存,用户直接查询事先被预热的缓存数据即可

解决方案

1、数据量不大的时候直接在项目启动的时候加载缓存数据

2、定时刷新缓存数据

3、页面按钮手动操作刷新缓存数据

缓存降级

当访问量剧增,缓存服务响应慢时,需要对某些数据自动缓存降级,也可以配置开关人工降级,例如redis缓存访问不到的时候降级访问二级缓存、本地缓存等

在进行降级之前要对系统进行梳理,那些缓存可以降级,那些不能降级,然后设置预案:

1、一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

2、警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

3、错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;

4、严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级

使用Hystrix对Redis进行资源隔离

对redis的访问加上保护措施,全都用hystrix的command进行封装,做资源隔离,确保redis的访问只能在固定的线程池内的资源来进行访问,哪怕是redis访问的很慢,有等待和超时,也不要紧,只有少量额线程资源用来访问,缓存服务不会被拖垮

解决方案

引入Hystrix 保护redis

1、引入Hystrix依赖


   
   
  1. <dependency>
  2. <groupId>com.netflix.hystrix</groupId>
  3. <artifactId>hystrix-core</artifactId>
  4. <version> 1.5 .18</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.netflix.hystrix</groupId>
  8. <artifactId>hystrix-metrics-event-stream</artifactId>
  9. <version> 1.5 .18</version>
  10. </dependency>

2、具体示例代码如下:


   
   
  1. /**
  2. * 保存商品信息到redis缓存中
  3. *
  4. * @date 2018/06/12
  5. */
  6. public class SaveProductInfo2RedisCacheCommand extends HystrixCommand<Boolean> {
  7. private ProductInfo productInfo;
  8. public SaveProductInfo2RedisCacheCommand (ProductInfo productInfo) {
  9. super(HystrixCommandGroupKey.Factory.asKey( "RedisGroup"));
  10. this.productInfo = productInfo;
  11. }
  12. @Override
  13. protected Boolean run () {
  14. StringRedisTemplate redisTemplate = SpringContext.getApplicationContext().getBean(StringRedisTemplate.class);
  15. String key = "product_info_" + productInfo.getId();
  16. redisTemplate.opsForValue().set(key, JSON.toJSONString(productInfo));
  17. return true;
  18. }
  19. }
  20. /**
  21. * 将商品信息保存到redis中
  22. *
  23. * @param productInfo
  24. */
  25. public void saveProductInfo2RedisCache (ProductInfo productInfo) {
  26. SaveProductInfo2RedisCacheCommand command = new SaveProductInfo2RedisCacheCommand(productInfo);
  27. command.execute();
  28. }
  29. public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {
  30. private Long productId;
  31. public GetProductInfoCommand (Long productId) {
  32. super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey( "ProductInfoService"))
  33. .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey( "GetProductInfoPool"))
  34. .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
  35. .withCoreSize( 10)
  36. .withMaxQueueSize( 12)
  37. .withQueueSizeRejectionThreshold( 8)
  38. .withMaximumSize( 30)
  39. .withAllowMaximumSizeToDivergeFromCoreSize( true)
  40. .withKeepAliveTimeMinutes( 1)
  41. .withMaxQueueSize( 50)
  42. .withQueueSizeRejectionThreshold( 100))
  43. .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
  44. // 多少个请求以上才会判断断路器是否需要开启。
  45. .withCircuitBreakerRequestVolumeThreshold( 30)
  46. // 错误的请求达到40%的时候就开始断路。
  47. .withCircuitBreakerErrorThresholdPercentage( 40)
  48. // 3秒以后尝试恢复
  49. .withCircuitBreakerSleepWindowInMilliseconds( 4000))
  50. );
  51. this.productId = productId;
  52. }
  53. @Override
  54. protected ProductInfo run () throws Exception {
  55. String productInfoJSON = "{\"id\": " + productId + ", \"name\": \"iphone7手机\", \"price\": 5599, \"pictureList\":\"a.jpg,b.jpg\", \"specification\": \"iphone7的规格\", \"service\": \"iphone7的售后服务\", \"color\": \"红色,白色,黑色\", \"size\": \"5.5\", \"shopId\": 1, \"modifiedTime\": \"2017-01-01 12:01:00\"}";
  56. return JSONObject.parseObject(productInfoJSON, ProductInfo.class);
  57. }
  58. }

服务降级

高并发高负载情况下,选择动态的关闭一些不重要的服务拒绝访问,比如推荐、留言等不太重要的服务

总结

上面是常用的高并发系统设计考虑的方面,尤其是缓存中的解决方案,没有哪一个是最优的,适合自己的业务场景才是最好的

 

参考文章

https://blog.csdn.net/kevin_love_it/article/details/88095271

https://blog.csdn.net/zeb_perfect/article/details/54135506

https://blog.csdn.net/xlgen157387/article/details/79530877

http://www.saily.top/2018/06/12/cache06/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值