10.高并发缓存


缓存大体可以分为三类: 客户端缓存;服务端缓存;网络中的缓存。
根据规模和部署方式缓存也可以分为:单体缓存;缓存集群;分布式缓存。

缓存一定是离用户越近越好

缓存的分类:

  • 客户端缓存
  • 页面缓存
  • 浏览器缓存
  • APP缓存
  • 网络缓存
  • Web代理缓存(Nginx)
  • 边缘缓存(CDN)
  • 服务端缓存
  • 数据库缓存(innodb_buffer_pool_size)
  • 应用级缓存(Caffeine,Ehcache)
    • 缓存预热
  • 平台级缓存(Redis)

缓存的数据一致性

缓存更新方案:

  1. 先更新缓存,再更新数据库(一般不考虑)
    • 更新缓存成功,更新数据库出现异常了,导致缓存数据与数据库数据完全不一致,而且很难察觉,因为缓存中的数据一直都存在。
  2. 先更新数据库,再更新缓存(一般不考虑)
    • 原因跟第一个一样,数据库更新成功了,缓存更新失败,同样会出现数据不一致问题。
  3. 先删除缓存,后更新数据库(不建议)
    • A进程删除缓存后,还没来得及更新数据库,就被B进程查询数据库(缓存为空)后再次更新了缓存(相当于没删)
      • 解决方案:延时双删(删缓存 - 写数据库 - 休眠1s再删缓存)
    • A进程删除缓存、写数据库后,主从同步时,被B从从库查数据(缓存为空,从库复制还未完成),再次更新了缓存(相当于没删)
      • 解决方案:1. 延时双删;2. 更新redis的查询操作,指向主库查询
    • 异步双删,增加吞吐量
    • 不推荐的原因:1. 缓存穿透到DB;2. 休眠时间不好设置
  4. 先更新数据库,后删除缓存
    • 依然有并发问题:进程A查询数据库(缓存为空)后写入缓存前,被进程B更新了数据库,导致缓存与DB不一致
      • 概率极低,因为需要让进程B写数据库的操作耗时比进程读数据库还短,而读库操作一般比写库要快
      • 解决方案:1. 设置缓存失效时间;2. 异步延时删除;3. 使用Canal监听binlog日志,更新缓存

删除缓存失败的解决方案:

  1. 使用MQ再次删除
  2. 使用canal采集binlog日志,发送到MQ中,然后通过ACK机制确认处理删除缓存

缓存更新的设计模式

  • Cache Aside (最常用,因为强一致的实现性能往往较差):缓存失效时由调用方加载(上面的第四种方案)
  • Read/Write Through:把更新数据库操作由缓存自己代理了
  • Read Through:缓存失效时由用缓存服务自己来加载
  • Write Through:当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库。
  • Write Behind Caching:,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库
    • 数据不是强一致性的,而且可能会丢失
    • 实现复杂

缓存数据一致性

  1. 本地缓存
    • 设置失效时间(毛刺现象)
    • 设置双缓存
      • 只有两份本地缓存都没有,才会到远程获得。
      • 正式缓存是最后一次写入后经过固定时间过期
      • 备份缓存是设置最后一次访问后经过固定时间过期
      • 异步刷新:正式缓存只有无效才会被重新写入,备份缓存无论是否无效都会重新写入
    • 强制本地缓存失效和手动刷新本地缓存(CacheManagerController)
  2. Redis使用canal更新缓存

Redis集群

Redis Cluster

每个集群的槽数都是固定的16 384(即16×1024)个,客户端可以连接集群的任意一个节点来访问集群的数据,如果数据不在当前这个节点上,那就向客户端返回一个重定向的命令。支持主从复制(内部通过选举来确定有主节点的存活),支持读写分离(需要客户端的支持)。

大厂不用Redis Cluster构建集群

Redis Cluster 采用了一种去中心化的流言(Gossip)协议来传播集群配置的变化。传播速度比较慢,而且是集群规模越大,传播的速度就越慢。

比如说10个节点的状态信息约1kb。同时redis集群内节点,每秒都在发ping消息。在这种情况下,一个总节点数为200的Redis集群,默认情况下,这时ping/pong消息占用带宽达到25M,这还只是槽的范围是0 ~16383 的情况。其次redis 的集群主节点越多,心跳包的消息体内携带的数据越多。如果节点过1000 个,也会导致网络拥堵。因此redis 作者,不建议redis cluster 节点数量超过1000个

构建超大规模集群

代理

比较常用的方法是采用一种基于代理的方式,即在客户端和Redis 节点之间

  1. 负责在客户端和Redis 节点之间转发请求和响应。客户端只与代理服务打交道,代理收到客户端的请求之后,会转发到对应的Redis 节点上,节点返回的响应再经由代理转发返回给客户端。
  2. 负责监控集群中所有Redis 节点的状态,如果发现存在问题节点,就及时进行主从切换。
  3. 维护集群的元数据,这个元数据主要是集群所有节点的主从信息,以及槽和节点的关系映射表。像开源的Redis 集群方案twemproxy 和 Codis,采用的都是代理服务这种架构。

优点:对客户端透明,从客户端的视角来看,整个集群就像是一个超大容量的单节点Redis 一样。除此之外,由于分片算法是受代理服务控制的,因此扩容比较方便,新节点加入集群后,直接修改代理服务中的元数据就可以完成扩容。

缺点:由于增加了一层代理转发,因此每次数据访问的链路变得更长了,这必然会导致一定的性能损失。而且代理服务本身也是集群的单点。当然,我们可以把代理服务也做成一个集群来解决单点问题,那样集群就更复杂了。

客户端

把代理服务的寻址功能前移到客户端中。客户端在发起请求之前,首先会查询元数据,客户端可以自行缓存元数据,这样访问性能基本上就与单机版的Redis 一样了。如果某个分片的主节点宕机了,就会选举新的主节点,并更新元数据中的信息。对集样的扩容操作也比较简单,除了必须完成数据的迁移工作之外,再更新一下元数据就可以了。

当然元数据服务仍然是一个单点,但是它的数据量不大,访问量也不大,相对来说比较容易实现。利用已有的ZooKeeper、etcd 甚至MySQL 都可以被用来实现上述元数据服务。定制客户端的Redis 集群方案应该是最适合超大规模Redis 集群的方案,在性能、弹性、高可用等几个方面的表现都非常好,缺点是整个架构比较复杂,客户端不能通用,需要开发定制化的Redis 客户端,所以往往只有规模足够大的企业才能负担得起高昂的定制开发成本。

总结梳理

缓存一定是离用户越近越好

Redis数据一致性

  • 先更新缓存,再更新数据库(更新DB异常)

  • 先更新数据库,再更新缓存(更新缓存失败)

  • 先删除缓存,后更新数据库(更新DB之前被其他进程更新了缓存)

    • 主从复制问题:更新缓存的查询指定为主库

    • 异步延时双删

  • 先更新数据库,后删除缓存(写入缓存之前被其他进程更新了DB)

    • 低概率:要求写库操作快于读库

    • 异步延时双删

  • 使用Canal监听binlog日志,更新缓存

本地缓存

  • 设置双缓存

  • 正式缓存是最后一次写入后经过固定时间过期

  • 备份缓存是设置最后一次访问后经过固定时间过期

  • 异步刷新:正式缓存只有无效才会被重新写入,备份缓存无论是否无效都会重新写入

  • 缓存预热

大厂不用Redis Cluster构建集群

  • 代理

  • 把代理服务的寻址功能前移到客户端中

    • ZooKeeper保存元数据

设计模式

Cache Aside、Read/Write Through、Read Through、Write Through、Write Behind Caching


文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录