别让HR再质问我:我费劲招的人,你用缓存问废了,不能简单点?
概念
缓存穿透
在高并发下,查询一个不存在的值时,缓存不会被命中,导致大量请求直接落到数据库上,如活动系统里面查询一个不存在的活动。
缓存击穿
在高并发下,对一个特定的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,导致大量请求直接落到数据库上,如活动系统里面查询活动信息,但是在活动进行过程中活动缓存突然过期了。
缓存雪崩
在高并发下,大量的缓存key在同一时间失效,导致大量的请求落到数据库上,如活动系统里面同时进行着非常多的活动,但是在某个时间点所有的活动缓存全部过期。
常见解决方案
- 直接缓存NULL值
- 限流
- 缓存预热
- 分级缓存
- 缓存永远不过期
layering-cache实践
在layering-cache里面结合了缓存NULL值,缓存预热,限流、分级缓存和间接的实现"永不过期"等几种方案来应对缓存穿透、击穿和雪崩问题。
直接缓存NULL值
应对缓存穿透最有效的方法是直接缓存NULL值,但是缓存NULL的时间不能太长,否则NULL数据长时间得不到更新,也不能太短,否则达不到防止缓存击穿的效果。
我在layering-cache对NULL值进行了特殊处理,一级缓存不允许存NULL值,二级缓存可以配置缓存是否允许存NULL值,如果配置可以允许存NULL值,框架还支持配置缓存非空值和NULL值之间的过期时间倍率,这使得我们能精准的控制每一个缓存的NULL值过期时间,控制粒度非常细。当NULL缓存过期我还可以使用限流,缓存预热等手段来防止穿透。
示例:
@Cacheable(value = "people", key = "#person.id", depict = "用户信息缓存",
firstCache = @FirstCache(expireTime = 10, timeUnit = TimeUnit.MINUTES),
secondaryCache = @SecondaryCache(expireTime = 10, timeUnit = TimeUnit.HOURS,
isAllowNullValue = true, magnification = 10))
public Person findOne(Person person) {
Person p = personRepository.findOne(Example.of(person));
logger.info("为id、key为:" + p.getId() + "数据做了缓存");
return p;
}
在这个例子里面isAllowNullValue = true表示允许缓存NULL值,magnification = 10表示NULL值和非NULL值之间的时间倍率是10,也就是说当缓存值为NULL时,二级缓存的有效时间将是1个小时。
限流
应对缓存穿透的常用方法之一是限流,常见的限流算法有滑动窗口,令牌桶算法和漏桶算法,或者直接使用队列、加锁等,在layering-cache里面我主要使用分布式锁来做限流。
layering-cache数据读取流程:
下面是读取数据的核心代码:
private <T> T executeCacheMethod(RedisCacheKey redisCacheKey, Callable<T> valueLoader) {
Lock redisLock = new Lock(redisTemplate, redisCacheKey.getKey() + "_sync_lock");
// 同一个线程循环20次查询缓存,每次等待20毫秒,如果还是没有数据直接去执行被缓存的方法
for (int i = 0; i < RETRY_COUNT; i++) {
try {
// 先取缓存,如果有直接返回,没有再去做拿锁操作
Object result = redisTemplate.opsForValue().get(redisCacheKey.getKey());
if (result != null) {
logger.debug("redis缓存 key= {} 获取到锁后查询查询缓存命中,不需要执行被缓存的方法", redisCacheKey.getKey());
return (T) fromStoreValue(result);
}// 获取分布式锁去后台查询数据
if (redisLock.lock()) {
T t = loaderAndPutValue(redisCacheKey, valueLoader, true);
logger.debug("redis缓存 key= {} 从数据库获取数据完毕,唤醒所有等待线程", redisCacheKey.getKey());
// 唤醒线程
container.signalAll(redisCacheKey.getKey());
return t;
}
// 线程等待
logger.debug("redis缓存 key= {} 从数据库获取数据未获取到锁,进入等待状态,等待{}毫秒", redisCacheKey.getKey(), WAIT_TIME);
container.await(redisCacheKey.getKey(), WAIT_TIME);
} catch (Exception e) {
container.signalAll(redisCacheKey.getKey());
throw new LoaderCacheValueException(redisCacheKey.getKey(), e);
} finally {
redisLock.unlock();
}
}
logger.debug("redis缓存 key={} 等待{}次,共{}毫秒,任未获取到缓存,直接去执行被缓存的方法", redisCacheKey.getKey(), RETRY_COUNT, RETRY_COUNT * WAIT_TIME, WAIT_TIME);
return loaderAndPutValue(redisCacheKey, valueLoader, true);
}
当需要加载缓存的时候,需要获取到锁才有权限到后台去加载缓存数据,否则就会等待(同一个线程循环20次查询缓存,每次等待20毫秒,如果还是没有数据直接去执行被缓存的方法,这个主要是为了防止获取到锁并且去加载缓存的线程出问题,没有返回而导致死锁)。当获取到锁的线程执行完成会将获取到的数据放到缓存中,并且唤醒所有等待线程。
这里需要注意一下让线程等待一定不能用Thread.sleep(),我在使用Spring Redis Cache的时候,我发现当并发达到300左右,缓存一旦过期就会引起死锁,原因是使用的是sleep方法来让没有获取到锁的线程等待,当等待的线程很多的时候会产生大量上下文切换,导致获取到锁的线程一直获取不到cpu的执行权,导致死锁。在layering-cache里面,我们使用的是LockSupport.parkNanos方法,它会释放cpu资源, 因为我们使用的是redis分布式锁,所以也不能使用wait-notify机制。
缓存预热
有效应对缓存的击穿和雪崩的方式之一是缓存预加载。
@Cacheable(value = "people", key = "#person.id", depict = "用户信息缓存",
firstCache = @FirstCache(expireTime = 10, timeUnit = TimeUnit.MINUTES),
secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 2,timeUnit = TimeUnit.HOURS,))
public Person findOne(Person person) {
Person p = personRepository.findOne(Example.of(person));
logger.info("为id、key为:" + p.getId() + "数据做了缓存");
return p;
}
在 layering-cache里面二级缓存会配置两个时间,expireTime是缓存的过期时间,preloadTime 是缓存的刷新时间(预加载时间)。每次二级缓存被命中都会去检查缓存的过去时间是否小于刷新时间,如果小于就会开启一个异步线程预先去更新缓存,并将新的值放到缓存中,有效的保证了热点数据**"永不过期"**。这里预先更新缓存也是需要加锁的,并不是所有的线程都会落到库上刷新缓存,如果没有获取到锁就直接结束当前线程。
/**
* 刷新缓存数据
*/
private <T> void refreshCache(RedisCacheKey redisCacheKey, Callable<T> valueLoader, Object result) {
Long ttl = redisTemplate.getExpire(redisCacheKey.getKey());
Long preload = preloadTime;
// 允许缓存NULL值,则自动刷新时间也要除以倍数
boolean flag = isAllowNullValues() && (result instanceof NullValue || result == null);
if (flag) {
preload = preload / getMagnification();
}
if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preload) {
// 判断是否需要强制刷新在开启刷新线程
if (!getForceRefresh()) {
logger.debug("redis缓存 key={} 软刷新缓存模式", redisCacheKey.getKey());
softRefresh(redisCacheKey);
} else {
logger.debug("redis缓存 key={} 强刷新缓存模式", redisCacheKey.getKey());
forceRefresh(redisCacheKey, valueLoader);
}
}
/**
* 硬刷新(执行被缓存的方法)
*
* @param redisCacheKey {@link RedisCacheKey}
* @param valueLoader 数据加载器
*/
private <T> void forceRefresh(RedisCacheKey redisCacheKey, Callable<T> valueLoader) {
// 尽量少的去开启线程,因为线程池是有限的
ThreadTaskUtils.run(() -> {
// 加一个分布式锁,只放一个请求去刷新缓存
Lock redisLock = new Lock(redisTemplate, redisCacheKey.getKey() + "_lock");
try {
if (redisLock.lock()) {
// 获取锁之后再判断一下过期时间,看是否需要加载数据
Long ttl = redisTemplate.getExpire(redisCacheKey.getKey());
if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preloadTime) {
// 加载数据并放到缓存
loaderAndPutValue(redisCacheKey, valueLoader, false);
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
redisLock.unlock();
}
});
}
在缓存总量和并发量都很大的时候,这个时候缓存如果同时失效,缓存预热将是一个非常漫长的过程,就比如说服务重启或新上线一个新的缓存。这个时候我们可以采用切流的方式,让缓存慢慢预热,如开始切10%流量,观察没有异常后,再切30%流量,观察没有异常后,再切60%流量,然后全量。这种方式虽然有点繁琐,但是一旦遇到异常我们可以快速的切回流量,让风险可控。
总结
总体来说layering-cache在缓存穿透、击穿和雪崩上是以预防为主,补救为辅。而在应对缓存的这些问题上其实也没有一个完全完美的方案,只有最适合自己业务系统的方案。目前如果直接使用layering-cache缓存框架已经基本能应对大部分的缓存问题了。
这是关于缓存的问题,但是相信每一个程序员都清楚,当你在面试的过程中,只要涉及到缓存了,必定和一个技术脱不开关系,对,就是Redis,
关于Redis我之前整理过一篇文章:第一次见到这么齐全的redis知识图谱,老大再也不用担心我的技术
有兴趣的老铁,可以看一下
而对于Redis的学习感兴趣的老铁,或者今天的文章没怎么看明白的老铁,没关系,等更新太慢了,视频+文档资料来了,需要这份资料的,关注+转发,私信“资料”即可
redis学习面试集锦
zookeeper解决方案
别让HR再质问我:我费劲招的人,你用缓存问废了,不能简单点?的更多相关文章
- java解答:有17个人围成一圈(编号0~16),从第0号的人开始从1报数,凡报到3的倍数的人离开圈子,然后再数下去,直到最后只剩下一个人为止,问此人原来的位置是多少号?
package ttt; import java.util.HashMap; import java.util.Map.Entry; /** * 有17个人围成一圈(编号0~16),从第0号的人开始从 ...
- react入门(1)
这篇文章也不能算教程咯,就算是自己学习整理的笔记把. 关于react一些相关的简介.优势之类的,随便百度一下一大堆,我就不多说了,可以去官网(http://reactjs.cn/)看一下. 这片主要讲 ...
- 教务处sso设计缺陷
前言 刚学习python,觉得比较枯燥总不知道从哪里入手,偶然一次,同学让我帮忙看看选课,发给我的是学校统一的默认格式的密码,突然就想试试有多少人还是默认密码,从QQ群里找了一份学生信息尝试了一下,发 ...
- 亿级日PV的魅族云同步的核心协议与架构实践(转)
云同步的业务场景 这是魅族云同步的演进,第一张是M8.M9,然后到后面的是MX系统,M9再往后发展,我们的界面可以看到基本上是没有什么变化的,但本质发生了很大的变化,我们经过了一些协议优化,发展到今天 ...
- 亿级日PV的魅族云同步的核心协议与架构实践
声明:本文根据msup和魅族联合举办的<第三期魅族技术开放日-架构设计与优化>录音整理原创首发,转载或节选内容前需获授权. 嘉宾:沈辉煌,魅族高级架构师,魅族云同步负责人.2010年加入魅 ...
- DApp是什么,DApp是必然趋势
DApp是什么,DApp是必然趋势 https://www.jianshu.com/p/dfe3098de0de Thehrdertheluck关注 12018.04.23 11:54:00字数 2 ...
- 五分钟学后端技术:如何学习Redis、memcache等常用缓存技术
原创声明 本文作者:黄小斜 转载请务必在文章开头注明出处和作者. 本文思维导图 什么是缓存 计算机中的缓存 做后端开发的同学,想必对缓存都不会陌生了,平时我们可能会使用Redis,MemCache这类 ...
- 第13讲 | 套接字Socket:Talk is cheap, show me the code
第13讲 | 套接字Socket:Talk is cheap, show me the code 基于 TCP 和 UDP 协议的 Socket 编程.在讲 TCP 和 UDP 协议的时候,我们分客户 ...
- 网络协议学习笔记(五)套接字Socket
概述 前面学习网络知识的时候写过一篇关于套接字的随笔见<JAVA SOCKET 详解>,现在本人正在系统的学习网络知识,现在除了温故知新之外,在详细的学习记录一下套接字的知识. Socke ...
随机推荐
- 关于idea的一次踩坑记录-Auto build completed with errors
maven项目添加pom依赖后,一直不能正常导入所依赖的jar包,并且报错“ Auto build completed with errors”
- 「雕爷学编程」Arduino动手做(17)---人体感应模块
37款传感器和模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器与模块,依照实践出真知(动手试试)的理念,以学习和交流为目的,这里准备 ...
- mysql小白系列_04 datablock
1.为什么创建一个InnoDB表只分配了96K而不是1M? 2.解析第2行记录格式?(用下面的表定义和数据做测试) mysql> create table gyj_t3 (),name2 var ...
- nginx: [error] invalid PID number "" in ...
1.查看进程 ps -ef|grep nginx 2.进入nginx安装目录sbin下,执行命令: ./nginx -t 如下显示: syntax is ok test is successful 3 ...
- [Python进阶]001.不定参数
不定参数 介绍 元组参数 字典参数 混合 介绍 不定参数用 * 和 ** 定义 不定参数必须在其他所有参数之后 例子:os.path.join 方法就可以写入不定数量的参数 元组参数 定义:*args ...
- Excel表格中无法中间插入新行列! 提示:在当前工作表的最后一行或列中,存在非空单元格,解决方案
excel中新增行列时报错: 提示:在当前工作表的最后一行或列中,存在非空单元格,所以无法插入新行或新列.
- xxshenqi分析报告
背景 今年七夕爆发了一场大规模手机病毒传播,apk的名字叫做xxshenqi.中了这个病毒的用户会群发手机所有联系人一条信息,内容是包含这个apk下载的链接,同时用户的联系人信息和短信会被窃取,造成隐 ...
- 01 . HAProxy原理使用和配置
HaProxy简介 HaProxy是什么? HAProxy是一个免费的负载均衡软件,可以运行于大部分主流的Linux操作系统上. HAProxy提供了L4(TCP)和L7(HTTP)两种负载均衡能力, ...
- Java Word中的文本、图片替换功能
Word中的替换功能以查找指定文本然后替换为新的文本,可单个替换或全部替换.以下将要介绍的内容,除常见的以文本替换文本外,还将介绍使用不同对象进行替换的方法,具体可包括: 1. 指定字符串内容替换文本 ...
- Spring AOP学习笔记01:AOP概述
1. AOP概述 软件开发一直在寻求更加高效.更易维护甚至更易扩展的方式.为了提高开发效率,我们对开发使用的语言进行抽象,走过了从汇编时代到现在各种高级语言繁盛之时期:为了便于维护和扩展,我们对某些相 ...