缓存
缓存(Cache),就是数据交换的缓冲区 ,俗称的缓存就是缓冲区内的数据 ,一般从数据库中获取,存储于高速存储媒介上。
缓存的本质就是用空间换时间,牺牲数据的实时性,以服务器内存中的数据暂时代替从数据库读取最新的数据,减少数据库IO,减轻服务器压力,减少网络延迟,加快页面打开速度。
缓存的优点及作用
降低后端负载,提高读写效率,降低响应时间。
缓存的分类
浏览器缓存
主要是存在于浏览器端的缓存
应用层缓存
使用在代码层面的Map、List、Set等进行存储,实现对数据、页面、图片等资源的缓存
数据库缓存
早期的数据库,如Oracle、MySQL、SQL server等,数据都是存放在磁盘。虽然数据库层也有对应的缓存(例如,MySQL增改查数据都会先加载到数据库中的一片空间 buffer pool),但这种缓存一般针对的是查询内容,而且粒度太小,一般只有表中数据没有变更的时候,数据库对应的缓存才发挥作用。但这并不能减少业务系统对数据库产生的增、删、查、改的庞大IO压力。
redis、mamcached、mongodb是比较常见的缓存数据库,把经常需要从数据库查询的数据、或经常更新的数据放入到缓存中,这样下次查询时,直接从缓存直接返回,减轻数据库压力。但这些缓存数据库大多在系统关机后,数据就会丢失,所以大量的数据仍需存在早期的数据库中。
CPU缓存
当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存,介于CPU和内存之间。
服务器缓存
包括代理服务器缓存和CDN缓存
代理服务器缓存 :代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。
代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大。可以把它理解为一个共享缓存,不只为一个用户服务,一般为大量用户提供服务,因此在减少响应时间和带宽使用方面很有效,同一个副本会被重用多次。
CDN缓存 :也叫网关缓存、反向代理缓存。CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。
浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。
虽然这种架构负载均衡源服务器之间的缓存没法共享,但却拥有更好的处扩展性。从浏览器角度来看,整个CDN就是一个源服务器。
缓存的思路
基于Redis的缓存
标准的操作方式就是查询数据库之前先查询缓存,如果缓存数据存在,则直接从缓存中返回,如果缓存数据不存在,再查询数据库,然后将数据存入redis。
更新策略
缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以需要淘汰掉部分过期的数据。
内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
超时剔除 :当我们给redis设置了过期时间ttl
之后,redis会将超时的数据进行删除,方便咱们继续使用缓存
主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题
选择哪种更新策略可以依据业务的需求来抉择。对于一致性需求低的业务,我们可以采取内存淘汰机制;对于一致性需求高的数据,应当采取主动更新的策略,并以超时剔除作为兜底。
数据库缓存不一致
缓存的数据源来自于数据库 ,而数据库的数据是会发生变化的 ,因此,如果当数据库中数据发生变化,而缓存却没有同步 ,此时就会有一致性问题存在。
解决方案
-
Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
-
Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理
-
Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致
对于方案一:
- 删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存
- 如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用TCC等分布式事务方案
缓存穿透
缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
- 缓存空对象
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的不一致
- 布隆过滤
- 优点:内存占用较少,没有多余key
- 缺点:
- 实现复杂
- 存在误判可能
缓存空对象思路分析:当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了
布隆过滤:布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,假设布隆过滤器判断这个数据不存在,则直接返回。
这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突。
缓存雪崩
缓存雪崩是指在同一时段大量的缓存失效或者Redis等缓存服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值,避免了因为采用相同的过期时间导致的缓存雪崩
- 利用Redis集群提高服务的可用性,可以使用主从架构的集群提高服务的可用性,万一出现宕机可以使用从节点顶上,避免因为一台Redis缓存服务宕机影响整个业务,提高Redis的容灾性
- 给缓存业务添加降级限流策略,当redis发生故障的时候可以直接拒绝服务而不是继续访问数据库
- 给业务添加多级缓存,在浏览器、nginx、redis、jvm、数据库等一层层的添加缓存
- 使用熔断机制。当流量到达一定的阈值时,就直接返回"系统拥挤"之类的提示,防止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
- 互斥锁
- 逻辑过期
互斥锁
如果发生缓存击穿后,第一个请求查询数据库中该数据的时候,使用一个锁锁住,后续的所有请求在锁未放开之前访问这个数据就让它休眠一会重新查询缓存。缺点就是在第一个线程写缓存期间,其他访问该数据的线程拿不到锁就只能处于等待状态,影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。
优点:没有额外的内存消耗;保证了缓存数据库的一致性;实现比较简单
缺点:线程需要等待,性能受影响;可能有死锁风险。
逻辑过期
我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。
假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个新开的线程2去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回过期的数据,只有等到新开的线程2把重建数据构建完后,其他线程才能返回更新的数据。
优点:线程无需等待
缺点:不能保证缓存数据库一致性;有额外的内存消耗;实现比较复杂。