缓存/限流/服务雪崩
1. 在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
- 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
- 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行,服务降级:
- 服务接口拒绝服务
- 页面拒绝服务
- 延迟持久化
- 随机拒绝服务
- 限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
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())); }
3.服务雪崩
https://blog.csdn.net/qq_38149225/article/details/109454418
4.服务熔断和降级
https://blog.csdn.net/qq_38149225/article/details/109456482
5.读写锁
6.缓存更新策略:先更新数据库,再更新缓存。
先更新数据库再删缓存的思路是目前使用得最多的,即使删除失败或者其他并发进程读等问题,也有缓存过期来兜底。先删缓存再更新数据库因为出问题概率太大并没有什么用,因为读的并发量比较高,删除后被读并发更新的概率比较大。
缓存代理:更新数据库的操作由缓存进行
写回:只更新缓存,异步批量更新数据库。
US中采用主动更新策略,访问时发现缓存中的数据超过最大更新间隔,在返回结果的同时,还是会继续查询数据库用于更新缓存。