缓存,基本上都是在内存空间里,所以读缓存比读磁盘更快。
缓存能减少数据库服务器的压力,并提升用户请求的响应速度。其实,不管是前端还是后端,缓存都是一种必不可少的优化方式之一。
但也可能出现缓存穿透、缓存击穿、缓存雪崩、热点数据失效等问题。
1. 缓存的工作流程
不管缓存的位置在哪,可能在浏览器、代理服务器、服务器本地、分布式缓存系统或者数据库中,作用机理都基本一致。
假定我们现在讨论服务器本地缓存。
客户端(用户)请求数据
服务器先查缓存,如果缓存中有相关的响应数据,直接返回
缓存中没有相应数据,则服务器将从数据库获取数据,然后返回给客户端,同时将这份数据存入缓存
服务器根据客户端的请求查找缓存,如何做到相同请求对应相同的缓存数据?
客户端请求的URL可以映射为一个Key,服务器根据这个Key查找对应的缓存Value。
2. 缓存穿透
客户端请求数据库中不存在的数据时发生。
因为数据库中都不存在该数据,缓存中自然也没有。服务器查遍缓存也没有,然后客户端请求就会去冲击数据库,请求成功的话返回空。
但并发量一大,所以客户端请求全都冲击数据库,容易导致数据库服务器压力过大甚至宕机。
注:请求 id = -1 的数据。
解决方案:
- 接口校验(过滤无效请求)
- 缓存空值(key-null),并设置较短的缓存过期时间
- 布隆过滤器(BloomFilter)
布隆过滤器本质上是一种数据结构,巧妙的概率型数据结构。特点是高效地插入和查询,节约内存空间,时间复杂度低。
布隆过滤器能够告诉你,“某样东西一定不存在或者可能存在”。这里的应用场景,就是“请求的key在缓存中一定不存在或者可能存在”。通过“一定不存在”能过滤掉大量无效请求。
相比于传统的List、Map、Set等数据结构,布隆过滤器这种bitMap是一个bit数组或者说bit向量,更高效、占用空间更小。
布隆过滤器的应用关键是如何选择Hash函数个数和布隆过滤器的长度。
注:虽然理论上布隆过滤器应对缓存穿透十分有效,但实际上可能并不常用。它也有缺点,一是准确率有误(本质是因为hash碰撞),二是不能删除元素(如果一个元素被删除,却不能从布隆过滤器中删除)。
3. 缓存击穿
大量请求同时查询Key,但这个Key正好失效,于是并发的请求同时冲击数据库。这就是缓存击穿。
注:热点Key缓存过期。
缓存击穿与缓存穿透不一样。缓存击穿查询的是数据库中有但缓存没有的数据,缓存穿透查询的是数据库中没有且缓存也没有的数据。
解决方案:
- 设置热点数据永不过期
- 加互斥锁
注:热点数据的更新通过异步线程实现,保证高可用性;互斥锁能保证数据的一致性,大量的线程阻塞降低吞吐量,失去了性能。
4. 缓存雪崩
缓存雪崩是缓存中大量数据同时过期,导致大量的请求到达数据库。
缓存雪崩和缓存击穿不一样。缓存击穿查询的是同一条数据,缓存雪崩是不同的数据都过期了。
缓存服务器宕机,或者(初始化阶段)数据未加载到缓存中,都可能导致缓存雪崩。
解决方案:
- 缓存数据设置不同的过期时间
- 缓存预热(缓存服务器刚启动不久,大量数据未被缓存的背景下)
注:给不同key过期时间加上随机值,使缓存失效的时间点尽量均匀。其实缓存击穿的解决方案完全适用于缓存雪崩。在考虑使用互斥锁时,可以加上主备缓存策略(二级缓存)。
缓存预热是在系统上线后,先将相关的数据构建到缓存中,这样可以避免用户请求直接到达数据库。预热的数据主要取决于访问量和数据量大小。
4. 热点Key重建
使用“缓存+过期时间”的策略能够加速读写,同时能保证数据的定期更新,能满足绝大部分需求。
如果当前Key是热点Key,并且该Key的重建缓存不能在短时间完成(可能需要复杂计算)。
那么这个Key的缓存失效就得不偿失了。
解决方案:
- 热点数据永不过期
- 互斥锁(数据库服务端)
- 后端限流
- 二级缓存(A级缓存正常,B级缓存永不过期,A/B级缓存适时同步更新)
前两种这些方案都建立在Hot Key是已知的背景下。在不知道Hot Key的前提下,还是考虑后端限流、二级缓存等解决方案。
5. 缓存降级
缓存降级是指缓存失效或缓存服务器宕机的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。值得注意的是,缓存降级是有损的。
所谓的有损,是因为用户享受到的服务不是完整的(页面部分数据丢失)或者获取到的不是最新数据(一致性不能保证)。限流其实就是服务降级的一种,部分用户可能会请求失败。
注:降级和熔断相似但有区别。降级出现在整体负荷超出整体负载承受能力时,延迟或暂停非重要服务,例如日志收集服务。熔断针对于单个服务,当某一服务出现了过载现象,为防止造成整个系统故障,直接关闭该服务(比较暴力)或者保证部分请求成功。
6. 小结
数据库服务器很脆弱。
缓存解决的请求类型都是查询请求,查询就可能带来一致性问题。
缓存很重要,是数据库服务器的盾牌(削减伤害)。
解决缓存的问题,似乎加锁是通用方法。但加锁需要付出性能代价。
限流应该是数据库的贴身保镖,请求密集可能响应慢,但服务还不至于挂掉。
高并发系统的三大法宝:缓存、降级(熔断)和限流。