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

高并发系统设计方案

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

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

缓存:缓存设计是我们常用的减轻服务器压力的方案,常用的缓存有 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)实现,代码如下:

import java.util.concurrent.atomic.AtomicInteger;

import org.junit.Test;

import com.google.common.util.concurrent.RateLimiter;

/**
 * 固定速率请求,每200ms允许一个请求通过
 *
 * @date 2019-10-14 16:54
 **/
public class FixedRequestLimitDemo {
    @Test
    public void fixedRequestTest() {
        // 表示1秒内产生多少个令牌,即1秒内产生5个令牌,控制每200ms一个请求
        RateLimiter rateLimiter = RateLimiter.create(5);
        AtomicInteger counter = new AtomicInteger(0);

        for (int i = 0; i < 15; i++) {
            // 同时开启15个线程访问
            new Thread(() -> fixedRequest(rateLimiter, counter)).start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void fixedRequest(RateLimiter rateLimiter, AtomicInteger counter) {
        double time = rateLimiter.acquire();
        if (time >= 0) {
            System.out.println("时间:" + time + " ,第 " + counter.incrementAndGet() + " 个业务处理");
        }
    }
}

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

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import org.junit.Test;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

/**
 * 限流demo
 *
 * @date 2019-10-12 17:26
 **/
public class LimitDemo {
    @Test
    public void limitTest() {
        // 本地缓存、key (Long)表示当前时间秒、value (AtomicLong)表示请求计数器
        LoadingCache counter = CacheBuilder.newBuilder().
                expireAfterAccess(2, TimeUnit.SECONDS).build(
                new CacheLoader() {
                    @Override
                    public AtomicLong load(Long aLong) throws Exception {
                        return new AtomicLong(0);
                    }
                }
        );

        for (int i = 0; i < 15; i++) {
            // 同时开启15个线程访问
            new Thread(() -> requestLimit(counter)).start();
        }

        try {
            // 这里休眠是为了多线程全部执行完输出结果
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

   /**
     * 请求限制,本方法加锁是为了控制缓存LoadingCache get数据时候的并发
     *
     * @param counter
     */
    private synchronized void requestLimit(LoadingCache counter) {
        try {
            // 流量限制数量
            long limit = 10;
            // 当前秒数
            long currentSecond = System.currentTimeMillis() / 1000;
            if (counter.get(currentSecond).incrementAndGet() > limit) {
                // 超出每秒内允许访问10个的限制
                System.out.println("第 " + counter.get(currentSecond) + " 个请求超出上限,限流了");
                return;
            }

            System.out.println("第 " + counter.get(currentSecond) + " 个业务处理");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

缓存

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

缓存雪崩

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

解决方案

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

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

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

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

//伪代码
public object getProductList() {
    int cacheTime = 30;
    String cacheKey = "product_list";
    String lockKey = cacheKey;

    String cacheValue = CacheHelper.get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    } else {
        // 对lockKey加锁
        synchronized(lockKey) {
            cacheValue = CacheHelper.get(cacheKey);
            if (cacheValue != null) {
                return cacheValue;
            } else {
	            //这里一般是sql查询数据
                cacheValue = getProductListFromDB(); 
                CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            }
        }
        return cacheValue;
    }
}

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

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

//伪代码
public object getProductList() {
    int cacheTime = 30;
    String cacheKey = "product_list";
    // 缓存标记
    String cacheSign = cacheKey + "_sign";

    String sign = CacheHelper.Get(cacheSign);
    // 获取缓存值
    String cacheValue = CacheHelper.Get(cacheKey);
    if (sign != null) {
        //未过期,直接返回
        return cacheValue; 
    } else {
        CacheHelper.Add(cacheSign, "1", cacheTime);
        // 开启后台线程来更新缓存
        ThreadPool.QueueUserWorkItem((arg) -> {
			// sql查询数据
            cacheValue = getProductListFromDB(); 
	        //日期设缓存时间的2倍,用于脏读
	        CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);                 
        });
        return cacheValue;
    }
} 

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

缓存击穿

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

解决方案

1、使用互斥锁

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

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

    // 1、redis setnx 实现
    public String getValue(key) {
        String value = redis.get(key);
        if(value != null){
            return value;
        }
        // 缓存值过期,获取锁,设置3min的超时,防止del操作失败的时候产生死锁
        if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {
            // 代表获取锁成功
            value = db.get(key);
            redis.set(key, value, expire_secs);
            redis.del(key_mutex);
            return value;
        }

        // 没有获取到锁表示其他线程已经重新设置缓存了,此时重试获取缓存即可
        try {
            Thread.sleep(50);
            //重试
            getValue(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    // 2、memcache 实现
    public String getValue(String key) {
        String value = memcache.get(key);
        if (value != null) {
            return value;
        }

        // 加锁
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
            value = db.get(key);
            memcache.set(key, value);
            memcache.delete(key_mutex);
            return value;
        }

        // 没有获取到锁表示其他线程已经重新设置缓存了,此时重试获取缓存即可
        try {
            Thread.sleep(50);
            //重试
            getValue(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

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

    /**
     * redis 实现
     * 

* V 对象中有两个属性,value 是缓存值,timeout 是缓存过期更新时间 * * @param key * @return */ public String getValue(String key) { V v = redis.get(key); String value = v.getValue(); long timeout = v.getTimeout(); if (timeout > System.currentTimeMillis()) { // 缓存值没有到缓存过期时间,直接返回 return value; } // 缓存值过期,异步更新后台执行 threadPool.execute(new Runnable() { public void run() { String keyMutex = "mutex:" + key; if (redis.setnx(keyMutex, "1", 3 * 60) == 1) { String newValue = db.get(key); redis.set(key, newValue); redis.delete(keyMutex); } } }); // 此时直接返回旧value值 return value; }

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

缓存穿透

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

解决方案

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

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

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

    public String getValue(key) {
        String value = redis.get(key);
        if (value != null) {
            return value;
        }
        // 缓存值过期,获取锁,设置3min的超时,防止del操作失败的时候产生死锁
        if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {
            // 代表获取锁成功
            value = db.get(key);
            if (value == null) {
                // value 为空时也缓存起来
                value = String.empty;
            }
            redis.set(key, value, expire_secs);
            redis.del(key_mutex);
            return value;
        }

        // 没有获取到锁表示其他线程已经重新设置缓存了,此时重试获取缓存即可
        try {
            Thread.sleep(50);
            //重试
            getValue(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

缓存预热

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

解决方案

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

2、定时刷新缓存数据

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

缓存降级

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

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

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

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

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

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

使用Hystrix对Redis进行资源隔离

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

解决方案

引入Hystrix 保护redis

1、引入Hystrix依赖


    com.netflix.hystrix
    hystrix-core
    1.5.18


    com.netflix.hystrix
    hystrix-metrics-event-stream
    1.5.18

2、具体示例代码如下:

/**
 * 保存商品信息到redis缓存中
 *
 * @date 2018/06/12
 */
public class SaveProductInfo2RedisCacheCommand extends HystrixCommand {

    private ProductInfo productInfo;

    public SaveProductInfo2RedisCacheCommand(ProductInfo productInfo) {

        super(HystrixCommandGroupKey.Factory.asKey("RedisGroup"));
        this.productInfo = productInfo;
    }


    @Override
    protected Boolean run() {
        StringRedisTemplate redisTemplate = SpringContext.getApplicationContext().getBean(StringRedisTemplate.class);

        String key = "product_info_" + productInfo.getId();
        redisTemplate.opsForValue().set(key, JSON.toJSONString(productInfo));

        return true;
    }
}


/**
 * 将商品信息保存到redis中
 *
 * @param productInfo
 */
public void saveProductInfo2RedisCache(ProductInfo productInfo) {
    SaveProductInfo2RedisCacheCommand command = new SaveProductInfo2RedisCacheCommand(productInfo);
    command.execute();
}



public class GetProductInfoCommand extends HystrixCommand {


    private Long productId;

    public GetProductInfoCommand(Long productId) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetProductInfoPool"))
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        .withCoreSize(10)
                        .withMaxQueueSize(12)
                        .withQueueSizeRejectionThreshold(8)
                        .withMaximumSize(30)
                        .withAllowMaximumSizeToDivergeFromCoreSize(true)
                        .withKeepAliveTimeMinutes(1)
                        .withMaxQueueSize(50)
                        .withQueueSizeRejectionThreshold(100))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        // 多少个请求以上才会判断断路器是否需要开启。
                        .withCircuitBreakerRequestVolumeThreshold(30)
                        // 错误的请求达到40%的时候就开始断路。
                        .withCircuitBreakerErrorThresholdPercentage(40)
                        // 3秒以后尝试恢复
                        .withCircuitBreakerSleepWindowInMilliseconds(4000))
        );
        this.productId = productId;
    }

    @Override
    protected ProductInfo run() throws Exception {
        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\"}";
        return JSONObject.parseObject(productInfoJSON, ProductInfo.class);
    }
}

服务降级

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

总结

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

 

参考文章

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/

你可能感兴趣的:(高并发)