为什么使用缓存

在高并发的分布式的系统中,缓存是必不可少的一部分。没有缓存对系统的加速和阻挡大量的请求直接落到系统的底层,系统是很难撑住高并发的冲击,所以分布式系统中缓存的设计是很重要的一环。

收益:

  1. 提高性能,提高并发能力(内存速度>访问硬盘速度)

同等配置单机Redis QPS可轻松上万,MySQL则只有几千。

  1. 降低后端的负载

成本:

  • 数据不一致性:缓存层与存储层的数据存在着一定时间窗口一致,时间窗口与缓存的过期时间更新策略有关。
  • 代码维护成本:加入缓存后,需要同时处理缓存层和存储层的逻辑,增加了开发者维护代码的成本。
  • 运维成本:引入缓存层,比如Redis。为保证高可用,需要做主从,高并发需要做集群。

缓存适合读多写少的业务场景, 只要收益大于成本,我们就可以采用缓存。

命中率=命中数/(命中数+没命中数)

高并发下缓存常见问题

一、缓存一致性

无论是先写到Redis里再写MySQL还是先写MySQL再写Redis,这两步写操作不能保证原子性,所以会出现Redis和MySQL里的数据不一致。无论采取何种方式都不能保证强一致性

1. 先写数据库,再删缓存[普通低并发]

Cache Aside Pattern

可能会短暂出现数据不一致情况,但最终都会一致。

为什么更新数据库后不更新而是删除缓存?
冷数据不会经常被访问, 频繁更新缓存代价大
其实删除缓存,而不是更新缓存,就是一个 Lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。

2. 延迟双删

1)先删除缓存
2)再写数据库
3)休眠500毫秒
4)再次删除缓存
延迟的目的是为了删除在写MySQL期间读线程可能把脏数据再次读到Redis里,延迟的时间参照一次从MySQL读数据并写入Redis的时间

3. 只更缓存,不更MySQL,MySQL由缓存异步的更新[高并发]

Write Behind Caching Pattern—

4. 数据异步同步

Canal:基于数据库增量日志解析,提供增量数据订阅和消费https://github.com/alibaba/canal
mysql会将操作记录在Binary log日志中,通过canal去监听数据库日志二进制文件,解析log日志,同步到redis中进行增删改操作。
canal的工作原理:canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议;MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal );canal 解析 binary log 对象(原始为 byte 流)。

同步过程: https://www.cnblogs.com/kyousuke/p/13066017.html

二、缓存击穿(热点key重建优化)

缓存击穿,就是说某个key非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个key在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

  1. 当前key是一个hot key,比如热点娱乐新闻,并发量非常大。
  2. 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL, 多次IO,多个依赖等。

当缓存失效的瞬间,将会有大量线程来重建缓存,造成后端负载加大,甚至让应该崩溃

解决方式
事先知道hot key的情况下:

  1. 可以将热点数据设置为永远不过期;可以通过跑定时任务来定期更新,或者变更数据时主动更新。
  2. 基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。

风险: 重建的时间太长或者并发量太大,将会大量的线程阻塞,同样会加大系统负载。
风险解决: 除了重建线程之外,其它线程拿旧值直接返回。

不知道的情况:

  1. 后端限流 高并发情况下,后端限流是必不可少

既然hot key的危害是因为有大量的重建请求落到了后端,如果后端自己做了限流呢,只有部分请求落到了后端, 其它的都打回去了。一个hot key 只要有一个重建请求处理成功了,后面的请求都是直接走缓存了,问题就解决了。

三、缓存穿透

缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。

缓存常见问题 - 图1

解决思路

  1. 缓存空对象

从数据库找不到时,将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。[注意:这种情况我们一般会将空对象设置一个较短的过期时间]
做好业务过滤。比如我们确定业务ID的范围是[a, b],只要不属于[a,b]的,系统直接返回,直接不走查询。
给缓存的空对象设置一个较短的过期时间,在内存空间不足时可以被有效快速清除。

  1. 由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用布隆过滤器(BloomFilter)或者压缩filter提前拦截,不合法就不让这个请求到数据库层

    布隆过滤器 参考

    image.png
    布隆过滤器是一个很长的二进制向量和一系列随机映射函数。
    布隆过滤器被广泛用于网页黑名单系统、垃圾邮件过滤系统、爬虫的网址判重系统以及解决缓存穿透问题。
    它是一个空间效率占用极少和查询时间极快的算法,但是需要业务可以忍受一个判断失误率。
    布隆过滤器离不开哈希函数
    redis 布隆过滤器主要就两个命令:
  • bf.add 添加元素到布隆过滤器中:bf.add urls [https://jaychen.cc](https://jaychen.cc)
  • bf.exists 判断某个元素是否在过滤器中:bf.exists urls [https://jaychen.cc](https://jaychen.cc)

上面说过布隆过滤器存在误判的情况,在 redis 中有两个值决定布隆过滤器的准确率:

  • error_rate:允许布隆过滤器的错误率,这个值越低过滤器的位数组的大小越大,占用空间也就越大。
  • initial_size:布隆过滤器可以储存的元素个数,当实际存储的元素个数超过这个值之后,过滤器的准确率会下降。

redis 中有一个命令可以来设置这两个值: bf.reserve urls 0.01 100

布隆过滤器和缓存空对象是完全可以结合起来的。具体做法是布隆过滤器用本地缓存实现,因为内存占用极低,不命中时再走redis/memcache这种远程缓存查询。

四、缓存雪崩

缓存常见问题 - 图3

缓存雪崩发生原因:
1.如果我们的缓存挂掉了,这意味着我们的全部请求都跑去数据库了。
2.Redis需要对数据设置过期时间,并采用的是惰性删除+定期删除两种策略对过期键删除。如果缓存数据设置的过期时间是相同的,并且Redis恰好将这部分数据全部删光了。这就会导致在这段时间内,这些缓存同时失效,全部请求到数据库中。

解决

  1. 第一种解决:
  • 事发前:实现Redis的高可用(主从架构+Sentinel 或者Redis Cluster),尽量避免Redis挂掉这种情况发生。
  • 事发中:万一Redis真的挂了,我们可以设置本地缓存(ehcache)+限流(hystrix),尽量避免我们的数据库被干掉(起码能保证我们的服务还是能正常工作的)
  • 事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。
  1. 第二种解决:
    在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期。

  2. 项目资源隔离。避免某个项目的bug,影响了整个系统架构,有问题也局限在项目内部。


参考文章: https://zhuanlan.zhihu.com/p/55303228