
作者 | Mark_MMXI
缓存的存在是为了在高并发情形下,缓解DB压力,提高业务系统体验。业务系统访问数据,先去缓存中进行查询,假如缓存存在数据直接返回缓存数据,否则就去查询数据库再返回值。
Redis是一种缓存工具,是一种缓存解决方案,但是引入Redis又有可能出现缓存穿透、缓存击穿、缓存雪崩等问题。本文就对缓存雪崩问题进行较深入剖析,并通过场景模型加深理解,基于场景使用对应的解决方案尝试解决。
缓存原理及Redis解决方案
首先,我们来看一下缓存的工作原理图:

Redis 本质上是一个 Key-Value 类型的内存数据库。因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10、万次读写操作。Redis 还有一个优势就是是支持保存多种数据结构,例如 String、List、Set、Sorted Set、hash等。
缓存雪崩
2.1 缓存雪崩解释
缓存雪崩的情况是说,当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,DB直接负载大量请求压力导致挂掉。
2.2 模拟缓存雪崩
按照缓存雪崩的解释,其实我们要模拟,只需要达到以下几个点:
- 同一时刻大规模缓存失效。
- 失效的时刻有大量的查询请求冲击DB
@Test
public void testQuery{
ExecutorService es = Executors.newFixedThreadPool(10);
int loop= 1000;
int init=2000;
//查询1k个key放进缓存
for (int i = init; i < loop+init; i++) {
userService.queryById(i);
}
//缓存过期时间为1s,等待1s同时过期
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace;
}
//开始了使用多线程疯狂查询
for (int i = 0; i < 100; i++) {
es.execute( -> {
for (int k = init; k < loop+init; k++) {
userService.queryById(k);
}
});
}
}为了加快崩坏的速度,把数据库的最大连接数调整成5,同时增大数据库表的数据量达到百万级别。

然后执行测试程序,很快程序就报错并停止,详细错误如下:
Exception in thread "pool-1-thread-12" org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is io.lettuce.core.RedisException: Connection is closed
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:74)
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)
at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:270)
at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.convertLettuceAccessException(LettuceStringCommands.java:799)
at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.get(LettuceStringCommands.java:68)
at org.springframework.data.redis.connection.DefaultedRedisConnection.get(DefaultedRedisConnection.java:260)
at org.springframework.data.redis.cache.DefaultRedisCacheWriter.lambda$get$1(DefaultRedisCacheWriter.java:109)
at org.springframework.data.redis.cache.DefaultRedisCacheWriter.execute(DefaultRedisCacheWriter.java:242)
at org.springframework.data.redis.cache.DefaultRedisCacheWriter.get(DefaultRedisCacheWriter.java:109)
at org.springframework.data.redis.cache.RedisCache.lookup(RedisCache.java:88)
at org.springframework.cache.support.AbstractValueAdaptingCache.get(AbstractValueAdaptingCache.java:58)
at org.springframework.cache.interceptor.AbstractCacheInvoker.doGet(AbstractCacheInvoker.java:73)
at org.springframework.cache.interceptor.CacheAspectSupport.findInCaches(CacheAspectSupport.java:554)
at org.springframework.cache.interceptor.CacheAspectSupport.findCachedItem(CacheAspectSupport.java:519)
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:401)
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:345)
at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:61)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
at com.example.demo.user.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$ba6638d2.queryById()
at com.example.demo.DemoApplicationTests$1.run(DemoApplicationTests.java:55)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: io.lettuce.core.RedisException: Connection is closed
at io.lettuce.core.protocol.DefaultEndpoint.validateWrite(DefaultEndpoint.java:195)
at io.lettuce.core.protocol.DefaultEndpoint.write(DefaultEndpoint.java:137)
at io.lettuce.core.protocol.CommandExpiryWriter.write(CommandExpiryWriter.java:112)
2020-03-08 22:31:14.432 ERROR 37892 --- [eate-1895102622] com.alibaba.druid.pool.DruidDataSource : create connection SQLException, url: jdbc:mysql://localhost:3306/redis_demo?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC, errorCode 1040, state 08004
java.sql.SQLNonTransientConnectionException: Data source rejected establishment of connection, message from server: "Too many connections"主要问题出在数据库连接已经满了,无法获取数据库连接进行查询,这个现象是就是缓存雪崩的效果。
‘
2.3 解决缓存雪崩
2.3.1 分析雪崩场景
用图来说,实际上就是没有了redis这层担着上层流量压力

其实从这张图来看,对于我们一般的应用,客户端去访问应用到数据库的整个链路过程,其实在面临大流量的时候,我们一般是以"倒三角"模型进行流量缓冲,什么是“倒三角”模型

通过"倒三角"模型,按照并发需要优化系统,在面临雪崩这种情形,可以按照“倒三角”模型进行优化,注意雪崩是理论上没办法彻底解决的,可能到最终得提高硬件配置。
2.3.1 雪崩优化方案
经过分析得解决雪崩方案:
1.随机缓存过期时间,能一定程度缓解雪崩
2.使用锁或队列、设置过期标志更新缓存
3.添加本地缓存实现多级缓存
4.添加熔断降级限流,缓冲压力2.3.1.1 随机缓存时间
随机缓存时间意在避免大量热点key同时失效。
接下来,我们基于Redis+SpringBoot+SpringCache基础项目搭建这个项目继续进行实践。
由于是使用了SpringCache,我们最优的方案就是直接在@Cacheable等注解上面加参数,比如像表达式之类的,让数据放进缓存的时候按照表达式/参数值定义过期时间。
因此我们先查看原有的RedisCache是怎么样的put逻辑
RedisCacheManager创建Cache
protected RedisCache createRedisCache(String name, @able RedisCacheConfiguration cacheConfig) {
return new RedisCache(name, this.cacheWriter, cacheConfig != ? cacheConfig : this.defaultCacheConfig);
}打开RedisCache.class,查看put 方法如下:
public void put(Object key, @able Object value) {
Object cacheValue = this.preProcessCacheValue(value);
if (!this.isAllowValues && cacheValue == ) {
throw new IllegalArgumentException(String.format("Cache '%s' does not allow '' values. Avoid storing via '@Cacheable(unless="#result == ")' or configure RedisCache to allow '' via RedisCacheConfiguration.
















