随着系统访问量的提高,复杂性的提升,响应性能成为一个重点的关注点。其中,缓存的使用成为了一个重点。Redis作为缓存中间件的一个佼佼者,很有必要了解Redis相关的一些重要知识点。
什么是缓存雪崩?
如果缓存挂掉了,就意味着全部的请求都跑到数据库去了,这就是缓存雪崩。
我们都知道,Redis不可能把所有的数据都缓存起来,因为内存昂贵且有限。所以,Redis需要对数据设置过期时间,并且采用的是惰性删除+定期删除两种策略对过期的键进行删除。如果缓存数据设置的过期时间是相同的,并且Redis恰好将这部分数据全部删光了,这样就会导致在这段时间内这些缓存同时失效,导致全部请求都直接到达数据库。
一旦缓存雪崩了,就很有可能会把数据库搞垮(大量请求并发),导致整个服务瘫痪。
如何解决缓存雪崩?
在缓存的时候,给过期时间加上一个随机值,就能大幅度减少缓存在同一时间过期的问题。
对于【Redis挂掉了,请求全部达到数据库】这种情况,可以有以下解决问题的思路:
1.事发前预防。实现Redis的高可用(主从架构+Sentinel或Redis Cluster),尽量避免Redis挂掉这种情况。
2.事发中控制。万一Redis真的挂了,我们可以设置本地缓存(ehcache)+限流(hystrix),尽量避免我们的数据库被干掉,起码能保证服务的正常工作。
3.事发后补救。使用Redis持久化,挂掉重启后能自动从磁盘上加载数据,快速恢复缓存数据。
什么是缓存击穿?
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发访问,即一些热点的key,一旦这些热点key因为过期或因为Redis抵挡不住这些高并发的请求等原因,而导致缓存失效进而请求全部走向数据库,则可能导致数据库宕机而造成整个服务挂掉的情况,这种情况就叫做缓存击穿。
相比于缓存雪崩是大量key在同一时间过期引发的问题,缓存击穿强调的是某一热点key过期的瞬间引发的问题。
如何解决缓存击穿?
解决缓存击穿问题有两个方案。
1.设置热点数据永不过期。
2.加互斥锁(mutex key)。业界比较常用的做法,是使用mutex。简单来说,就是在缓存失效的时候(判断拿出来的值是空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的setnx或memcache的add)去set一个mutex key,当操作返回成功时,再进行load db操作并回设缓存,否则就重试整个get缓存的方法。加锁会使其他并行线程进入等待状态,等到当前线程释放锁之后,下一个线程才能获取锁并执行操作。这样就能防止所有线程都去数据库重复取数据、重复往缓存中更新数据的情况出现,只是这样会降低系统的吞吐量。
public String get(key) { String value = redis.get(key); if (value == null) { // 代表缓存值过期 // 设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db String keynx = key.concat(":nx"); if (redis.setnx(keynx, 1, 3 * 60) == 1) { // 代表设置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(keynx); } else { // 这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可 sleep(50); get(key); //重试 } } else { return value; } }
什么是缓存穿透?
缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据,则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。
这样,如果请求的数据在缓存大量不命中,导致大量的请求走向数据库,就很可能将数据库搞垮,导致整个服务瘫痪。
如何解决缓存穿透?
解决缓存穿透问题有两种方案。
1.由于请求的参数是不合法的(每次都请求不存在的参数),我们就可以使用布隆过滤器(Bloom Filter)或压缩Filter提前拦截,拦截到不合法的参数就不让这个请求到达数据库层。
2.当我们从数据库中找不到这个数据的情况下,我们也将这个空对象设置到缓存中去,下次请求的时候就可以从缓存里面取了,虽然取的也是空对象,但是有效防止了请求再次到达数据库层。
什么是缓存预热?
缓存预热是一个比较常见的概念,就是指在系统上线后,先将相关的缓存数据直接加载到缓存系统。这样,用户请求的时候就不需要先去查询数据库,再将数据放入缓存了,用户可以直接拿到实现被预热的缓存数据。
什么是缓存降级?
当访问量剧增,服务出现问题(比如响应慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,及时是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的,比如加入购物车、结算等服务。
在进行降级之前,要先对系统进行梳理,看看系统是不是可以弃帅保车,进而梳理出哪些是核心服务(不可降级),哪些是非核心服务(可降级)。
拿日志级别设置预案作为参考:
1.一般级别。比如某些服务偶尔因为网络抖动或者服务正在上线而超时,就可以自动降级。
2.警告级别。有些服务在一段时间内成功率有波动(比如在95~100%之间),就可以自动降级或人工降级,并发送警告。
3.错误级别。比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级。
4.严重错误级别。比如因为特殊原因数据错误了,此时就需要紧急人工降级。
什么是缓存与数据库双写一致性问题?
对于读操作,流程是这样的:如果我们的数据在缓存里面有,那就直接读取缓存的数据;如果缓存里面没有,则先去查询数据库,然后将数据库查出来的数据写入到缓存中,最后再将数据返回给请求。
如果仅仅只是查询的话,缓存的数据和数据库的数据都是没问题的。但是,当我们要更新的时候,有一些情况就很可能造成数据库和缓存的数据不一致了。举个例子,数据库的库存值是999,但是缓存的库存值是1000,那么很可能在一段时间内,页面拿到的是缓存1000的值,尽管实际上的库存是999(数据库的值)。
从理论上来说,只要我们设置了键的过期时间,我们就能够保证缓存和数据库的数据最终一致性。因为只要缓存数据过期了,就会被删除,下次读的时候因为缓存里面没有,就会从数据库中查询并更新到缓存中。但是,在缓存数据没过期的时间内,缓存数据和数据库数据是不同步的。怎样保证在写入数据库的同时,同步更新缓存中的数据,就是缓存与数据库双写一致性问题。
怎样解决缓存与数据库双写一致性问题?
一般来说,如果你的系统不是严格要求缓存数据和数据库数据必须保证一致性的话,缓存可以稍微和数据库偶尔有不一致的情况。为什么建议不要做双写一致性的方案,是因为这种方案会使读请求和写请求串行化,串行化到一个内存队列中去,才能保证一定不会出现不一致的情况。而串行化会导致系统的吞吐量大幅度降低,需要用比正常情况下多几倍的机器去置成线上的一个请求。
解决思路基本上都是删除缓存。因为这样的话,下一次读就会到数据库中读到缓存中,保证缓存的一致性。就算数据库更新操作失败了,也不会有缓存数据与数据库数据不一致的问题,即使缓存数据和数据库数据都是旧数据。只是删除缓存的时机不同会引发不同的问题。
解决思路1:先更新数据库,再删除缓存。这样,一旦删除缓存失败了,就会导致数据库中是新数据,缓存中是旧数据,保证不了数据一致性。
解决思路2:先删除缓存,再更新数据库。这样,如果有一个读请求在更新数据库之前发生,就会导致脏读的问题。因为这个请求首先去读缓存,发现读不到,就会去数据库中读,因为数据库还没更新数据,就查到了修改前的旧数据放到了缓存中,随后数据库才完成数据更新操作,导致数据库和缓存中的数据不一致。
解决思路3:写请求先将缓存修改为指定值,再更新数据库,再更新缓存。读请求过来之后,先读缓存,判断是指定值,则进入等待状态,等待写请求更新缓存之后再读缓存。如果等待超时,则直接到数据库中读取数据,更新缓存。这种方案可以保证读写的一致性,但是因为读请求需要等待写请求的完成,降低了吞吐量。
"爱我们爱的人都怕来不及,哪里还有时间去憎恨呢。"