一、概述
在用户请求的整个链路上处处都能见到缓存的身影。客户端浏览器有缓存,前端CDN静态资源有缓存,后台服务有本地缓存和分布式缓存,数据库有查询缓存,操作系统对磁盘数据有页缓存,CPU对内存数据有高速缓存。如果从源头去获取一个东西代价比较高,那么把它搬到一个更容易获取的地方,之后从这个地方获取更高效,这是缓存解决问题的本质。
二、数据一致性
缓存有像反范式化数据库设计一样的问题:重复数据,有多个地方需要更新数据,所以要保证数据一致性。然而这里的一致性也只是最终一致性,并不是强一致性,从数据更新到缓存更新,这期间查询的缓存数据就是过期的数据。对于强一致性的数据而言,不适合用缓存,如果DB有压力,那么分区是更为合适的选择。
写时更新
在更新原始数据的同时更新缓存,下次访问的缓存数据就是最新的。并发写可能导致脏数据。比如两个写请求,正常情况下DB1 -> Cache1 -> DB2 -> Cache2。并发时可能 DB1 -> DB2 -> Cache2 -> Cache1。DB存在最新的数据,但是缓存里的是旧数据。
读时更新
写时令缓存失效,读数据时将数据加载到缓存。读写并发时可能导致脏数据。getN -> update N to M -> put cache N。库里是M,缓存是N。当变化了某个数据时,需要引起大量缓存的更新,这样写时更新会消耗很多时间,用读时更新就相对合适。
定时更新
在固定时间或者每隔一段时间,将数据重新载入缓存。这适用于数据一致性要求不高的场景。比如Redis会定期检查过期的数据,并清除。(Redis在实现过期的时候同时采用定时更新和读时更新)
三、缓存失效的问题
缓存穿透
对一个根本不存在的数据发起查询,比如id = -1,这类查询会透过缓存落在DB,大量的查询会给DB带来压力。第一、做基本的参数判断,不合法的参数直接快速失败。第二、对该非法key同样做缓存,设置过期时间。防止用非法key重复攻击。第三、布隆过滤器,对于不存在的数据,直接快速失败。布隆过滤器表示不存在的数据一定不存在,而布隆过滤器表示存在的数据也许不存在。这样能过滤掉大部分无效请求,详情参考Redis的布隆过滤器。
缓存击穿
一个拥有大量访问量的key在缓存失效时,会导致请求落到DB。第一、热点key可以不设置过期时间。第二、在失效去DB获取数据时使用双重检查锁来维护缓存数据。这样至少能保证同一时间点,一台机器上最多一个线程去DB获取数据。
缓存雪崩
同一时间段内有大量的缓存失效,导致请求落到DB。第一、热点key可以不设置过期时间。第二、过期时间可以加上一个随机值,使得过期时间分散开。第三、可以将不同的热点数据分布到不同的缓存上,这样也减少缓存的压力。
四、其他注意事项
1.更新缓存失败
缓存更新失败,不做其他处理,势必会导致读取到过期的数据。如果更新失败,可以用异步任务重推,可以设置定时任务定时做数据对其。多次失败后告警,人工介入。
2.缓存维护成本
缓存维护成本包括检查缓存的开销,维护缓存的时间成本和空间成本。如果成本很高,缓存命中率低,带来的收益不佳,需要重新考虑缓存的必要性了。
3.缓存命中率
如果缓存命中率很低,那么需要结合缓存维护成本来考虑是否有必要加这个缓存。应当尽量提高缓存的命中率,充分利用缓存。
4.内存与网络开销
在使用分布式缓存时,需要考虑网络开销的成本。因为缓存本来就很快,相比而言,网络开销的占比就增大了。特别是多次从缓存中获取数据,需要考虑叠加的网络开销。尽量通过更少的请求数量,获取一批所需的缓存数据。分布式缓存系统主要关注的就是内存和带宽两方面。