缓存/限流
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())); }
以上是关于缓存/限流的主要内容,如果未能解决你的问题,请参考以下文章