一些基础芝士
将MySQL的热点数据存储在Redis中,通常业务都满足二八原则,80%的流量在20%的热点数据之上,所以缓存是可以很大程度提升系统的吞吐量。
一般而言, 缓存分为服务器端缓存,和客户端缓存
服务器端缓存即服务端将数据存入Redis,可以在访问DB之后,将数据缓存,或者在回包时将回包内容以请求参数为Key缓存.
(啊 那就每次查询数据之前 看看这个请求有没有加入过 如果有就直接取它的value 发过去)
客户端缓存就是对服务端远程调用之后,将结果存储在客户端,这样下次请求相同数据时就能直接拿到结果,不会再远程调用,提高性能节省网络带宽。
缓存的几种模式
缓存一般有如下几种模式:
●Cache-Aside Pattern:旁路缓存模式
Read Through Cache Pattern:读穿透模式
●Write Through Cache Pattern: 写穿透模式
●Write Behind Pattern:又叫Write Back,异步缓存写入模式
旁路缓存模式
Cache Aside,即旁路缓存模式,是最常见的模式,应用服务把缓存当作数据库的旁路,直接和缓存进行交互。
读操作的流程如下:
应用服务收到查询请求后,先查询数据是否在缓存上,如果在,就用缓存数据直接打包返回,如果不存在,就去访问数据库,从数据库查询,并放到缓存中,除了查库后加载这种模式,如果业务有需要,还可以预加载数据到缓存
那它怎么写呢?
这里选择的方式是 直接删除 然后下次查询到该数据的时候 再把新的数据缓存了
为啥不直接更新呢
因为更新相比删除会更容易造成时序性问题举个例子:
thread1更新mysq|为5 -> thread2更 新mysql为3 -> thread2更新缓存为3 -> thread1更新缓存为5,最终正确的数据因为时序性被覆盖了。
Cache Aside适用于读多写少的场景,比如用户信息、新闻报道等,一旦写入缓存,几乎不会进行修改。该模式的缺点是可能会出现缓存和数据库不一致的情况。
Read Through
Read-Through,读穿透模式,和Cache Aside模式的区别主要在于应用服务不再和缓存直接交互,而是直接访问数据服务,这个数据服务可以理解为一个代理,即单独起这么一个服务,由它来访问数据库和缓存,作为使用者来看,不知道里面到底有没有缓存,数据服务会自己来根据情况查询缓存或者数据库。
查询的时候,和Cache Aside一样,也是缓存中有,就用从缓存中获得的数据,没有就查DB,只不过这些由数据服务托管保存,而对应用服务是透明的。
相比Cache Aside, Read Through的优势是缓存对业务透明,业务代码更简洁。缺点是缓存命中时性能不如CacheAside,相比直接访问缓存,还会多一次服务间调用。
套了层壳子罢
Write Through
在Cache Aside中, 应用程序需要维护两个数据存储:一个缓存,一个数据库。这对于应用程序来说,更新操作比较麻烦,还要先更新数据库, 再去删除缓存。
WriteThrough模式相当于做了一层封装:
由这个存储服务先写入MySQL,再同步写入Redis,这样及时加载或更新了缓存数据。可以理解为,应用程序只有一个单独的访问源,而存储服务自己维护访问逻辑。
这个不是之前的读穿透模式那样了 读穿透模式是旁路缓存纯套个壳子 这个写穿透就有改动了
Write Behind
Write-Behind和Write-Through相同点都是写入时候会更新数据库、也会更新缓存。
不同点在于Write-Through会把数据立即写入数据库中,然后写缓存,安全性很高。而Write-Behind是先写缓存,然后异步把数据一起写入数据库,这个异步写操作是Write-Behind的最大特点。
数据库写操作可以用不同的方式完成:
一种是时间上的灵活性,其中一个方式就是收集写操作并在某一时间点(比如数据库负载低的时候)慢慢写入。另一种方式就是合并几个写操作成为一个批量操作,一起批量写入。
两者是可以根据业务情况结合的。异步写操作极大地降低了请求延迟并减轻了数据库的负担,但是代价是安全性不够,比如先写入了Redis,更新操作先放在存储服务内存中,但是还没异步写入MySQL之前,存储服务崩溃了,那么数据也就丢失了。
就是一个同步一个异步的区别被
各有优势,但是Cache-Aside Pattern,旁路缓存模式是最常见,最易用的,在业务开发中,其他模式很少会用到
缓存异常场景
缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。谁正常人反复查这个数据 就是恶意让你穿透的
1.接口层增加校验,如用户鉴权校验,id做基础校验,id< =0的直接拦截;
2.从缓存取不到的数据, 在数据库中也没有取到,这时也可以将key-value对写为key-null, 缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用 如果后来这个真的key加入了 那么也无法使用即使旁路缓存也无法访问)。这样可以防止攻击用户反复用同一个id暴力攻击
3.布隆过滤器。bloomfilter就类似于一 个hash set, 用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,
缓存击穿
问题背景
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
缓存击穿,一般指是指热键在过期失效的一瞬间,还没来得及重新产生,就有海量数据,直达数据库
●解决方案
1.热点数据支持续期,持续访问的数据可以不断续期,避免因为过期失效而被击穿
2.发现缓存失效,重建缓存加互斥锁,当线程查询缓存发现缓存不存在就会尝试加锁,线程争抢锁,拿到锁的线程就会进行查询数据库,然后重建缓存,争抢锁失败的线程,你可以加一个睡眠然后循环重试
缓存雪崩
●问题背景
缓存雪崩顾名思义,是指大量的应用请求因为异常无法在Redis缓存中进行处理,像雪崩一样,直接打到数据库。
这里异常的原因,也可以说雪崩的原因,主要是:缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机其实在一些资料里,会把Redis宕机算进来,原因是Redis宕机了也就无法处理缓存请求,但这里会觉得有些牵强,如果这里能算,缓存击穿不也可以算?所以这里建议是不把宕机考虑到雪崩里去。
●解决方案
1.缓存数据的过期时间设置随机,防止同- -时间大量数据过期现象发生。
2.重建缓存加互斥锁,当线程拿到缓存发现缓存不存在就会尝试加锁,线程争抢锁,拿到锁的线程就会进行查询
数据库,然后重建缓存,争拾锁失败的线程,你可以加一个睡眠然后循环重试
区分一下 这个击穿是 一条热点数据过期 然后大量请求去查询这个热点数据 然后雪崩是大量的数据过期 这个大量的请求去请求这些数据
缓存一致性问题
缓存,都知道是持久化数据的冗余存储,但如果缓存加载了数据源的数据,但对应数据要发生变化,怎么办呢?
我们以数据源为MySQL,缓存用Redis,前面也说过了,在数据库场景下,更廉价、高效但可靠性稍低的redis可以给更昂贵、较慢、可靠性强的mysq|做缓存。
前面介绍了几种缓存模式,这里我们以常见、最实用的旁路缓存模式为基础,来进行分析。
大的方向有三:
1.更新MySQL即可,不管Redis,以过期时间兜底
2.更新MySQL之后,操作Redis
3.异步将MySQL的更新同步到Redis
方向一
使用redis的过期时间,mysqI更新时,redis不做处理,等待缓存过期失效,再从mysq|拉取缓存。
这种方式实现简单,但不一致的时间会比较明显,具体由你的业务来配置。如果读请求非常频繁,且过期时间设置较长,则会产生很多脏数据。
优点:
redis原生接口,开发成本低,易于实现;
管理成本低,出问题的概率会比较小。
不足:
完全依赖过期时间,时间太短容易造成缓存频繁失效,太长容易有较长时间不一 致
方向二
不光通过key的过期时间兜底,还需要在更新mysql时,同时尝试操作redis,这里的操作分两种方式,1是更新,直接将结果写入Redis,但实际上很少用更新,而是用删除,等待下次访问再加载回来,为什么呢?因为更新容易带来时序性问题。
举个例子:假设a的初始值为2,两台业务服务器在同一时间发出两条请 求:
第一条,给a的值加1
第二条,设置a的值为5
若mysqI中先执行第一条, 再执行第二条,则mysq|中a的值先变成3,最终为5;
但由于网络传输本身有延迟,所以无法保证两条Redis更新操作谁先执行,如果第二条对应的更新先执行,Redis的数据就先变成了5,然后在加1变成了6。
这就出现数据对不上的问题,相比于数据延迟而言,这更让人疑惑和不能接受。所以一般都选择 删除。
上面有提到,这里是尝试删除,这样说是这一步操作是可能失败了,失败就我们可以忽略,也就是不能让删除成为一个关键路径,影响核心流程。
因为我们有key本身的过期时间作为保障,所以最终一致性是一 定达成的, 主动删除redis数据只是为了减少不一致的时间。
优点:
相对方案一,达成最终一致性的延迟更 小;
实现成本较低, 只是在方案-的基础上, 增加了删除逻辑。
不足:
如果更新mysql成功,删除redis却失败,就退化到了方案- - ;
在更新时候需要额外操作Redis,带来了损耗。
方向三
把我们搭建的消费服务作为mysqI的一个slave,订阅mysqI的binlog日志,解析日志内容,再更新到redis.此方案和业务完全解耦,redis的更新对业务方透明,可以减少心智成本。
优点:
和业务完全解耦,在更新mysq|时,不需要做额外操作;
无时序性问题,可靠性强。
缺点:
引入了消息队列这种算比较重的组件,还要单独搭建一个同步服务.维护他们是非常大的额外成本
同步服务如果压力比较大,或者崩溃了,那么在较长时间内,redis中都是老旧数据