缓存/限流

Posted 刘伟伟920647590

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了缓存/限流相关的知识,希望对你有一定的参考价值。

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流

  • 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
  • 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行
  • 限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

1.缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

方案:

接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

布隆过滤器,将所有可能存在的数据存到一个bitMap中,不存在的数据就会进行拦截

2、缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

解决方案:
设置热点数据永远不过期。

服务限流和接口限流。如果服务和接口都有限流机制,就算缓存全部失效了,但是请求的总量是有限制的,可以在承受范围之内,这样短时间内系统响应慢点,但不至于挂掉,影响整个系统。

从数据库获取缓存需要的数据时加锁控制,本地锁或者分布式锁都可以。当所有请求都不能命中缓存,这就是我们之前讲的缓存穿透,这时候要去数据库中查询,如果同时并发的量大,也是会导致雪崩的发生,我们可以在对数据库查询的地方进行加锁控制,不要让所有请求都过去,这样可以保证存储服务不挂掉。

锁会影响性能,可以使用信号量来做,就是限制并发而已

3、缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,        缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
设置热点数据永远不过期。

缓存过期标记+异步刷新:

public Object getCacheValue(String key, int expiredTime) {
    final String signKey = "sign:" + key;
    Object cacheValue = cache.get(key);
    if (!isExpired(signKey, false)) { // 缓存标记未过期
        return cacheValue;
    } else {
        // 缓存标记signKey已过期,异步更新缓存key
        THREAD_POOL.execute(() -> {
            try {
                if (DistributeLock.lock(key)) {
                    if (isExpired(signKey, true)) { // double-check
                        Object cacheValue = GetValueFromDB(); // 读数据库
                        if (cacheValue != null) {
                            cache.set(key, cacheValue); // 设置缓存
                            setSign(signKey, expiredTime); // 设置缓存标记
                        }
                    }
                }
            } catch (Exception ex) {
                logger.error(ex.getMessage(), ex);
            } finally {
                DistributeLock.unlock(key);
            }

        });
        return cacheValue;
    }

}

// 判断缓存标记是否过期
private boolean isExpired(String signKey, boolean prolongTime) {

    Object time = cache.get(signKey);
        if (null == time || Long.valueOf(time) < System.currentTimeMillis()) {
            if (prolongTime) {
                // 将过期时间后延一分钟,防止同一时间过期多次而出现多次重载
                this.setSign(signKey, 1 * 60);
            }
            return true;
        }
        return false;
}

// 设置signKey的过期时间
private void setSign(String key, int expiredSeconds) {
    DateTime dateTime = new DateTime();
    dateTime = dateTime.plusSeconds(expiredSeconds);// 当前时间延后expiredSeconds秒
    cache.set(key, String.valueOf(dateTime.getMillis()));
}

 

以上是关于缓存/限流的主要内容,如果未能解决你的问题,请参考以下文章

并发限流缓存

Nginx分片限流代理缓存

谈谈高并发系统的限流

谈谈高并发系统的限流

谈谈高并发系统的限流

Nginx配置之负载均衡限流缓存黑名单和灰度发布