Redis 应用--缓存
在生产环境中,Redis 最常见的应用场景之一就是作为缓存使用。通过将频繁访问的数据存储在内存中,可以显著提高应用程序的响应速度,减轻数据库的负载,从而提升整体系统的性能。
什么是缓存
缓存(cache)在计算机科学中,是一种用于存储数据的高效存储层,旨在加速数据的访问速度。缓存通常位于主存储器(如 RAM)和处理器之间,或者作为应用程序的一部分,用于临时存储频繁访问的数据,以减少对较慢存储介质(如硬盘或远程数据库)的访问次数。
而在 Web 开发中,缓存通常指的是在服务器端或客户端存储一些数据,以便在后续请求中能够更快地获取这些数据,说白了,就是将一些常用的数据放到触手可及的地方。常见的缓存类型包括:
- 内存缓存:将数据存储在内存中,以便快速访问。Redis 就是一个典型的内存缓存解决方案。
- 磁盘缓存:将数据存储在磁盘上,虽然访问速度比内存慢,但可以存储更多的数据。
- 浏览器缓存:浏览器会缓存一些静态资源(如图片、CSS、JavaScript 文件等),以减少对服务器的请求。
使用缓存的主要目的是提高数据访问速度,减少延迟,并减轻后端系统(如数据库)的负载。通过缓存,应用程序可以更快地响应用户请求,从而提升用户体验。
使用 Redis 作为缓存
在生产环境当中,我们通常会使用关系型数据库(如 MySQL)来存储数据。关系型数据库的功能非常强大,可以处理一些及其复杂的数据关系和查询操作。但是,关系型数据库的查询速度相对较慢,尤其是在面对大量并发请求时,数据库的性能可能成为系统的瓶颈。
为什么说关系型数据库性能不高?
- 数据库把数据存储在硬盘上,硬盘的IO速度并不快,尤其是随机访问
- 如果查询不能命中索引,就需要进行表的遍历,这就会大大增加硬盘的IO次数
- 关系型数据库对于 SQL 的执行会做一系列的解析,校验以及优化工作
- 如果是一些复杂查询,比如联合查询,需要进行笛卡尔积操作,效率会指数级下降
为了提升系统的性能,我们可以使用 Redis 作为缓存层,将一些频繁访问的数据存储在 Redis 中。当应用程序需要访问这些数据时,首先会查询 Redis,如果数据存在(命中缓存),则直接从 Redis 获取数据,避免了对关系型数据库的访问;如果数据不存在(未命中缓存),则从关系型数据库中查询数据,并将查询结果存储到 Redis 中,以便下次访问时能够更快地获取。
缓存可以被理解为一个防护罩,保护我们的数据库不被频繁访问,从而提升整体系统的性能。在计算机科学中,数据的访问有一个“80/20 法则”,即 80% 的访问请求集中在 20% 的数据上。因此通过将这些热点数据存储在缓存中,可以显著提高系统的响应速度。
我们在 MySQL 前方让 Redis 作为盾牌,而 MySQL 同样可以利用主从复制、分库分表等机制来牢固自己的根基。
缓存更新策略
我们会将热点数据存储到缓存当中,这点没错,但是,哪些数据属于是热点数据呢?
定期生成
每隔一定的周期,对于访问数据的频次进行统计,挑选出访问频次最高的前 N %的数据,存储到缓存当中。
这种方式的优点是实现简单,缺点是不能及时反映数据的变化,可能会导致缓存中的数据过时,其在搜索引擎中应用较多:
用户在搜索引擎中会输入一个查询词,有些词属于是高频的,但有一些就属于是低频词了,大家很少搜。
搜索引擎的服务器会把哪个用户什么时候搜索了什么词都通过日志记录下来,然后每隔一定周期(比如每天),对这些日志进行分析,统计出哪些词是高频词,然后把这些高频词的搜索结果预先计算好,存储到缓存当中。且日志的数量可能非常巨大,这个统计的过程可能需要使用 Hadoop 之类的分布式计算框架来完成。
这种做法的实时性较低,对于一些突然情况应对的并不好,比如说,在春节期间,大家都在搜索“春晚”,但平时这个词的搜索频次并不高,这种突发情况就不能及时反映到缓存当中,当然,这些都可以预判到,比如说,春节期间,哪些词可能会成为高频词,可以提前把这些词的搜索结果计算好,存储到缓存当中,不过我们难免会遇到一些突发情况,比如说,某个明星突然爆冷了,大家都在搜索这个明星的名字,这种情况是无法预判到的。
实时生成
先给缓存设定容量上限,可以通过 Redis 配置文件的 maxmemory 参数来设置,当缓存达到这个上限时,就需要淘汰一些数据,给新的数据腾出空间。
接下来把用户每次查询:
- 如果在 Redis 中查询到了,就直接返回
- 如果 Redis 中不存在,就去数据库中查询,并把查询结果存储到 Redis 中
如果缓存已经满了,就触发缓存淘汰策略,把一些相对不那么热门的数据给淘汰掉,按照上述流程,服务器运行一段时间后 Redis 内部自然就是实时的热门数据了。
通常来说,淘汰策略主要有以下几种:
- LRU(Least Recently Used,最久未被使用):淘汰那些最近最久未被访问的数据。这种策略假设最近被访问的数据在未来也更有可能被访问。
- LFU(Least Frequently Used,最不经常使用):淘汰那些访问频率最低的数据。这种策略假设访问频率低的数据在未来也不太可能被访问。
- FIFO(First In First Out,先进先出):淘汰那些最早进入缓存的数据。这种策略简单易实现,但可能会淘汰掉仍然有用的数据。
- 随机淘汰:随机选择一些数据进行淘汰。这种策略实现简单,但可能会导致一些热门数据被误删。
如果说你问我该采用哪一种策略,那么我只能说“具体情况具体分析”了,因为每一种策略都有其适用的场景和优缺点。
在 Redis 中,内置了许多的淘汰策略,可以通过配置文件中的 maxmemory-policy 参数来设置,常见的策略包括:
noeviction:当内存达到上限时,不进行任何淘汰操作,所有写操作都会报错。allkeys-lru:从所有键中淘汰最近最少使用的键。volatile-lru:从设置了过期时间的键中淘汰最近最少使用的键。allkeys-random:从所有键中随机淘汰键。volatile-random:从设置了过期时间的键中随机淘汰键。volatile-ttl:从设置了过期时间的键中淘汰即将过期的键。volatile-lfu:从设置了过期时间的键中淘汰最不经常使用的键。allkeys-lfu:从所有键中淘汰最不经常使用的键。
整体来说,Redis 提供的策略和我们上述介绍的策略是基本一致的,只不过 Redis 在针对于“过期key”和“全部key”做了分别处理。
缓存(预热、穿透、雪崩、击穿)–重中之重
缓存预热(Cache Preheating)
使用 Redis 作为 MySQL 的缓存时,当 Redis 刚刚启动,或者 Redis 大批 key 失效之后,此时由于 Redis 自身相当于是空着的,没有缓存数据,那么 MySQL 就可能直接被访问到,从而造成较大的压力。因此就需要提前把热点数据准备好,直接写入到 Redis 中,使 Redis 可以尽快为 MySQL 撑起保护伞。
热点数据可以基于之前介绍的统计的方式生成即可。这份热点数据不一定非得那么“准确”,只要能帮助 MySQL 抵挡大部分请求即可。随着程序运行的推移,缓存的热点数据会逐渐自动调整(自我修正),来更适应当前情况。
缓存穿透(Cache Penetration)
缓存穿透是指查询一个根本不存在的数据,由于缓存中没有,数据库中也没有,所以每次请求都会直接打到数据库上,造成数据库压力过大,甚至可能导致数据库崩溃。
当你在看到这一部分的时候,一定会想到,这不合理啊,数据既不在缓存中,也不在数据库中,那用户为什么还会去查询这个数据呢?这不符合常理啊。
没错,这本身就是由于一方的失误造成的,比如说,用户输入了一个错误的 ID,或者用户恶意攻击,频繁查询一些不存在的数据,当然,也有可能是因为运维那边睡着了,把一些不该删的数据给删掉了。
不过,总归还是程序员的错,因为程序员没有做好数据校验工作,导致用户可以输入一些错误的数据,安全工作没有做好,导致用户可以恶意攻击。
如何解决这些问题呢?
- 针对要查询的参数进行严格的校验,比如说,ID 必须是数字,不能是字符串,不能是负数,不能是小数,不能是特殊字符等等。
- 对于一些不存在的数据,可以将其结果也缓存起来,比如说,查询 ID=12345 的数据不存在,那么就将这个结果缓存起来,设置一个具有标志性意义的
value,下次再查询 ID=12345 的时候,就直接返回缓存中的结果,而不是去查询数据库,而服务器这边根据value定期清理这些不存在的数据的缓存即可。 - 对于恶意攻击,可以通过一些防火墙,或者说,限流的方式来进行防御。
- 对于运维失误,可以通过一些监控,告警的方式来进行防御。
- 使用布隆过滤器(Bloom Filter)来快速判断某个数据是否存在,从而避免对数据库的无效查询。
缓存雪崩(Cache Avalanche)
什么是缓存雪崩呢?缓存雪崩是指大量的缓存数据在同一时间失效,导致大量的请求直接打到数据库上,造成数据库压力过大,甚至可能导致数据库崩溃。
为什么会出现这种情况呢?主要有以下几种原因:
- 缓存服务器宕机,导致所有的缓存数据都无法访问。
- 缓存数据设置了相同的过期时间,导致在同一时间大量的缓存数据失效。
当然,也有可能是程序员或运维人员干的,不过这些都不算是主要原因了,这些都是小概率事件。
如何解决这些问题呢?
- 针对于缓存服务器宕机的问题,可以通过集群的方式来进行解决,比如说,使用 Redis Sentinel,或者说,使用 Redis Cluster,来保证缓存服务器的高可用性。而且,还有可能是服务器磁盘损坏,或者说,网络故障等等,我们可以让当前集群工作一段时间,然后将其中一部分机器换成新的机器,来避免出现大批量的机器同时宕机。
- 针对于缓存数据设置了相同的过期时间的问题,其实答案已经在问题当中了,为什么会同时过期,那可不就是你同时给他们设置了相同的过期时间嘛,我们可以通过设置不同的过期时间,来避免大量的缓存数据在同一时间失效。比如说,可以将过期时间设置为一个随机值,在一个范围内随机生成一个过期时间,这样就可以避免大量的缓存数据在同一时间失效。
缓存击穿(Cache Breakdown)
说句实在话,这个名词定义在中文当中很容易和“缓存穿透”搞混淆,英文是Cache Breakdown,其实就是指某一个热点数据在缓存失效的瞬间,有大量的请求直接打到数据库上,造成数据库压力过大,甚至可能导致数据库崩溃。
我们可以将缓存击穿理解为缓存雪崩的一个特例,缓存雪崩是指大量的缓存数据在同一时间失效,而缓存击穿是指热点数据突然过期,导致大量的数据同时被查询,而因为 Redis 中并没有该数据,从而导致大量的请求直接打到数据库上。
针对于这种情形,我们可以通过以下几种方式来进行解决:
- 设置热点数据永不过期,或者说,设置一个非常长的过期时间,比如说,一年,甚至更长时间,这样就可以避免热点数据在短时间内失效。
- 对服务器进行必要的降级策略,比如说,采用分布式锁策略,来保证同一时间只有一个请求可以查询数据库,并将查询结果写入到缓存中,其他请求则等待缓存更新完成后,再从缓存中获取数据。