Redis缓存

做过项目的同学应该知道,Redis经常作为MySQL的缓存来使用;因为MySQL操作数据需要访问磁盘,性能不高,通常就几千QPS,而Redis基于内存并做了很多优化,性能高达10W QPS,一些热点数据就可以缓存到Redis,查询时先查Redis,不存在才查MySQL,这种Redis+MySQL结合的方式可以有效提高系统QPS。

一般而言,缓存分为服务器端缓存,和客户端缓存,服务器端缓存即服务端将数据存Redis,可以在访问DB之后,将从DB得到的数据缓存起来。

客户端缓存就是对服务端远程调用之后,将结果存储在客户端,这样下次请求相同数据时就能直接拿到结果,不会再远程调用,提高性能节省网络带宽。用服务端还是客户端呢?

其实是需要分析具体瓶颈在哪里,当然,如果按通常的经验,从服务角度来看,在目前的微服务架构下,每个服务其实都应该缓存一些热点数据,以减轻热点数据频繁请求给自己带来的压力,毕竟微服务也要有一定的互不信任原则。至于客户端缓存,这个就更看场景了,频繁请求的数据,就有必要做缓存。

下面我们以服务端缓存的视角介绍Redis常用的旁路缓存模式(Cache-Aside Pattern)。

一.旁路缓存模式

Redis中旁路缓存模式算是最常用到的了,而Redis设置缓存通常使用SET命令,SET key value, 大多数情况是使用string数据结构,一般都是用来给读操作加速,比较习惯于不管什么结构,都json序列化之后当string扔进缓存。有业务需要可以考虑其它结构。

应用服务把缓存当作数据库的旁路,直接和缓存进行交互。什么是旁路呢?可看下表:

数据流向
缓存作为主路(如 Write-Through) 应用 → 缓存 → 数据库,所有读写必须经过缓存
缓存作为旁路(Cache Aside) 应用自己决定什么时候走缓存,什么时候走数据库

读操作的流程如下:

应用服务收到查询请求后,先查询数据是否在缓存上,如果在,就用缓存数据直接打包返回,如果不存在,就去访问数据库,从数据库查询,并放到缓存中,除了查库后加载这种模式,如果业务有需要,还可以预加载数据到缓存。

写操作的流程如下:

在更新操作的时候,CacheAside模式是一般是先更新数据库,然后直接删除缓存,为什么不直接更新呢?因为更新相比删除会更容易造成时序性问题。举个例子:

thread1更新mysql为5 ->thread2更新mysql为3 ->thread2更新缓存为3->thread1更新缓存为5,最终正确的数据因为时序性被覆盖了。

CacheAside适用于读多写少的场景,比如用户信息、新闻报道等,一旦写入缓存,几乎不会进行修改。该模式的缺点是可能会出现缓存和数据库不一致的情况。总结一下:

二.缓存异常场景

2.1 缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求就会被打到数据库,使得缓存失去了意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

常见的解决方案有两种,分别是缓存空对象以及布隆过滤

缓存空对象:

  • 即将key-value设置为key-null;
  • 这种解决方案的优点是实现简单,维护也方便
  • 但是这个缺点在于过多的null会导致额外的内存消耗,为了解决此问题,可以设置TTL;但是仍无法解决短期的不一致(比如我同一时间插入和查询id=5的数据,此时有数据了,但是缓存中id为5的数据仍为null,只有当TTL过期后才能获取到真实的)

布隆过滤:

bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,它可以告诉我们**"某样东西一定不存在或者可能存在"**

  • 布隆过滤的优点是内存占用比较少,没有多余key
  • 缺点是实现复杂,存在误判现象,还有不支持一个关键字的删除,因为一个关键字的删除会牵连其他的关键字。改进方法就是counting Bloomfilter,用一个counter数组代替位数组,就可以支持删除了。(在布隆过滤器初始化时,分别对key1和key2进行哈希并取模处理。假如它们发生了哈希冲突,两次将索引6位置置为了1。如果在之后,想将关键字key1从布隆过滤器删除,那就只能将它哈希取模后的索引位置1、4、6的值设置为0。但如果这么操作了,之后还能使用布隆过滤器来判断Key2是否存在吗?很明显,不能,会产生误判,因为索引位置6不为1,导致布隆过滤器认为key2不存在。也就是说,如果支持关键字从布隆过滤器删除,会导致所有与被删除关键字发生了哈希冲突的其它存在关键字,都被布隆过滤器认为不存在。)

布隆过滤器原理:

布隆过滤器底层是一个bit数组,将字符串用多个Hash函数(让结果更分散,出现hash冲突的概率更低)映射不同的二进制位置,将对应位置设置为1。

在查询的时候,如果一个字符串所有Hash函数映射的值都存在,那么数据可能存在。为什么说可能呢,就是因为其他字符可能占据该值,提前点亮。(即出现哈希冲突)

2.2 缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无效的请求就会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种,分别是互斥锁逻辑过期

2.3 缓存雪崩

缓存雪崩是指同一时间段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力;

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

和缓存击穿不同的是,缓存击穿指一条热点数据在Redis没得到及时重建,缓存雪崩是一大批数据在Redis同时失效

三.缓存一致性怎么保证

Redis作为MySQL的缓存,如果数据源(MySQL)更新了,Redis的数据该怎么保持最终一致,这就是缓存一致性问题。

接下来,我们一旁路缓存为基础,来分析缓存一致性;大的方向有三个,下属文本将会一一介绍。但总的来说都是先更新mysql,再更新redis,是因为怕反过来的话,数据库还未更新的时候宕机,这种就是数据丢失了,先更新到MySQL中,就算挂了,Redis数据丢了就丢了,本来也不过是缓存而已。

方向一:更新MySQL即可,不管Redis,完全以过期时间兜底;

使用redis的过期时间,mysql更新时,redis不做处理,等待缓存过期失效,再从mysql拉取缓存。

这种方式实现简单,但不一致的时间会比较明显,具体由你的业务来配置。如果读请求非常频繁,且过期时间设置较长,则会产生很多脏数据,就看业务是否能接受了。

优点:

  • redis原生接口,开发成本低,易于实现;
  • 管理成本低,出问题的概率会比较小。

不足:

  • 完全依赖过期时间,时间太短容易造成缓存频繁失效,太长容易有较长时间不一致,对编程者的业务能力,有一定要求。

此种操作适用于内容展示页,比如微博热榜:读操作会很多,但是写操作便不会很多;

方向二: 更新MySQL之后,操作Redis,当然要考虑到Redis更新操作可能会因为网络、进程重启等各种原因失败,所以过期时间兜底还是少不了

不光通过key的过期时间兜底,还需要在更新mysql时,同时尝试操作redis,这里的操作分两种方式,1是更新,直接将结果写入Redis,但实际上很少用更新,而是用删除,等待下次访问再加载回来。

但是尝试删除这一步操作是可能失败了,失败就我们可以忽略,也就是不能让删除成为一个关键路径,影响核心流程。

因为我们有key本身的过期时间作为保障,所以最终一致性是一定达成的,主动删除redis数据只是为了减少不一致的时间。

优点:

  • 相对方案一,达成最终一致性的延迟更小;(主动立即删除和等待过期时间删除的两种时间对比)
  • 实现成本较低,只是在方案一的基础上,增加了删除逻辑。

不足:

  • 如果更新mysql成功,删除redis却失败,就退化到了方案一;
  • 在更新时候需要额外操作Redis,带来了损耗。

方向三:异步将MySQL的更新刷入到Redis,比如先更新mysql,通过订阅mysql的binlog记录来异步执行更新redis的操作

Canel是阿里的一款开源框架,当Canal监听到binlog变化时,会通知Canal客户端;

把我们搭建的消费服务当作mysql的一个跟随者slave,什么是跟随者?比如MySQL修改的数据,这个slave都能收到。订阅mysql的binlog日志,解析日志内容,再更新到redis。此方案(阿里巴巴开源的canal)和业务完全解耦。解耦是说,操作redis的,完全由这个slave来完成,不是我们用业务代码去操作redis。

优点:

  • 和业务完全解耦,在更新mysql时,不需要做额外操作;

缺点:

  • 引入了消息队列这种算比较重的组件,还要单独搭建一个同步服务,维护他们是非常大的额外成本
  • 同步服务如果压力比较大,或者崩溃了,那么在较长时间内,redis中都是老旧数据

从解耦层面来看,可以使用订阅binlog的模式来更新,缺点就是重,比较适。合的场景是数据不过期场景

声明: 本篇笔记仅为学习时整理的笔记以及疑问解决点,无其他任何商业用途,如有侵权联系即删。

相关推荐
小高不会迪斯科5 小时前
CMU 15445学习心得(二) 内存管理及数据移动--数据库系统如何玩转内存
数据库·oracle
e***8905 小时前
MySQL 8.0版本JDBC驱动Jar包
数据库·mysql·jar
l1t5 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
失忆爆表症7 小时前
03_数据库配置指南:PostgreSQL 17 + pgvector 向量存储
数据库·postgresql
AI_56787 小时前
Excel数据透视表提速:Power Query预处理百万数据
数据库·excel
SQL必知必会8 小时前
SQL 窗口帧:ROWS vs RANGE 深度解析
数据库·sql·性能优化
Gauss松鼠会8 小时前
【GaussDB】GaussDB数据库开发设计之JDBC高可用性
数据库·数据库开发·gaussdb
+VX:Fegn08958 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
程序猿阿伟8 小时前
《GraphQL批处理与全局缓存共享的底层逻辑》
后端·缓存·graphql
识君啊9 小时前
SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑
java·数据库·spring boot·后端