目录
1. 最简单的redis缓存设计
以下是我们常见的最简单的redis缓存的设计,新增、更改数据时,更新缓存;查询时,先查询缓存,缓存不存在,查询数据库,且更新缓存数据。在数据少,压力小,并发小的情况下,这个设计似乎没有什么大问题,但是在数据量大,高并发的情况下,却会有不少问题,接下来,我们会基于这套架构来分析高并发下redis缓存设计的问题以及解决方案。
2. 以上设计下的问题
2.1 大量冷数据占据内存问题
2.1.1 问题描述
当我们系统有大量数据,比如上亿数据,但是用户经常访问的数据,只有几百万数据,那么大量冷数据常驻redis内存,造成redis内存浪费。
2.2.2 解决方案
针对这种不常用的数据常驻缓存的问题,我们可以在缓存数据时设置一个超时时间,在一定时间内,让缓存自动失效。
那么设置超时时间后,又会有新的问题,常用的数据,在这个超时时间内也会失效。那么我们为了让常用的数据,常驻内存,我们可以在每次读缓存时,给该缓存延长在内存的时间,读一次,延长一次,这样就热点数据就会常驻内存了。
2.2 缓存击穿问题
2.2.1 问题描述
基于上述优化后的架构,如果我们批量导入入数据或者批量新增数据,那么这些数据建立缓存时缓存失效的时间会差不多一样,那么就会出现大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大 甚至挂掉。
2.2.2 解决方案
对于这种同一时间大量缓存失效的问题,那么我们在增加缓存时我们将缓存过期时间设置为不同的时间,是不是就可以解决了这个问题。我们在设计缓存时在设定时间上再加上一个随机时间。
2.3 缓存穿透问题
2.3.1 问题描述
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储 层查不到数据则不写入缓存层。 缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
造成缓存穿透的基本原因有两个:
- 自身业务代码或者数据出现问题。
- 一些恶意攻击、 爬虫等造成大量空命中。
2.3.2 解决方案
针对这种缓存穿透的问题,我们可以缓存一个空对象,并在一定时间内让其自动过期,且争对对同一条不存在数据多次请求问题,设置读延长机制。
2.4 冷数据突然变热数据导致数据库压力暴增问题
2.4.1 问题描述
比如大V直播间对冷产品的推广,导致大量用户同一时间抢购同一冷商品(缓存已经过期),导致大量请求直达数据库,造成数据库压力突然暴增。
2.4.2 解决方案
双重检测;当多个请求过来时,查询缓存没有数据时,要去查数据库前,获取一把锁,且再次查缓存,这样当第一个获取锁的请求,查完数据库重建缓存后,其他请求获取锁进来时,会先查缓存,这样就请求不会都打到数据库,造成数据库压力过大。
2.5 缓存与数据库双写不一致问题
2.5.1 问题描述
如下图所示,在高并发情况下,上述方案中,线程3进来读缓存,没有数据,此时线程1进行了数据库数据更新,更新为10,那么线程3读的数据则为10, 线程3要更新缓存时,线程2进行了数据库写,以及更新了缓存,都为5,之后线程3才执行到了缓存更新,将数据跟新为了10,那么这个数据最终数据库为5,缓存中为10,出现了缓存与数据库双写不一致的问题。
2.5.2 解决方案
1、对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生 缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
2、就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期 时间依然可以解决大部分业务对于缓存的要求。
3、如果不能容忍缓存数据不一致,可以通过加分布式读写锁保证并发读写或写写的时候按顺序排好队,读读的 时候相当于无锁。
4、也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加 了系统的复杂度。
分布式锁解决双写不一致的问题:
既然是在读数据库,然后更新缓存时出现的并发问题,那么我们在都数据库,更新缓存时,给加一个分布式锁。
当然,在新建和更新数据,更新缓存时,也需要加锁
上述中,我们直接加了一个分布式锁,那么读写时,都会阻塞,降低了我们的性能。出些双写不一致的主要原因是读写,写写,写读。那读读场景是不会出现并发问题,所以读读场景可以并行,那么这种读多写少的场景,我们可以再用读写锁来优化一下这个分布式锁。
我们在读的地方加读锁,大量请求过来读数据时,都会加锁成功,是重入锁。
在写数据的地方加写锁,只有一个线程会加锁成功,其他的会等待。
2.6 热点数据导致系统奔溃问题
2.6.1 问题描述
热点数据突然访问过大,同一时刻又几十万、上百万的请求过来,redis单节点也就能扛10万的并发,这种超大压力,都可能打垮我们的redis,导致系统崩溃。
2.6.2 解决方案
使用多级缓存架构,比如下述中使用JVM级别的缓存,先查JVM中的缓存,不存在,再查redis;当然在集群架构下,这个还需要订阅,通知其他JVM更新缓存;多级缓存架构下还有短暂数据不一致的问题。
2.7 缓存雪崩问题
2.7.1 问题描述
缓存雪崩指的是缓存层支撑不住或宕掉后, 流量会像奔逃的野牛一样, 打向后端存储层。 由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下 降), 于是大量请求都会打到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。
2.7.2 解决方案
预防和解决缓存雪崩问题, 可以从以下三个方面进行着手。
- 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。
- 依赖隔离组件为后端限流熔断并降级。比如使用Sentinel或Hystrix限流降级组件。 比如服务降级,我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商 品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是 错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失, 也可以继续通过数据库读取。
- 提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基 础上做一些预案设定。