缓存大体可以分为三类: 客户端缓存;服务端缓存;网络中的缓存。
根据规模和部署方式缓存也可以分为:单体缓存;缓存集群;分布式缓存。
缓存一定是离用户越近越好
缓存的分类:
- 客户端缓存
- 页面缓存
- 浏览器缓存
- APP缓存
- 网络缓存
- Web代理缓存(Nginx)
- 边缘缓存(CDN)
- 服务端缓存
- 数据库缓存(innodb_buffer_pool_size)
- 应用级缓存(Caffeine,Ehcache)
- 缓存预热
- 平台级缓存(Redis)
缓存的数据一致性
缓存更新方案:
- 先更新缓存,再更新数据库(一般不考虑)
- 更新缓存成功,更新数据库出现异常了,导致缓存数据与数据库数据完全不一致,而且很难察觉,因为缓存中的数据一直都存在。
- 先更新数据库,再更新缓存(一般不考虑)
- 原因跟第一个一样,数据库更新成功了,缓存更新失败,同样会出现数据不一致问题。
- 先删除缓存,后更新数据库(不建议)
- A进程删除缓存后,还没来得及更新数据库,就被B进程查询数据库(缓存为空)后再次更新了缓存(相当于没删)
- 解决方案:延时双删(删缓存 - 写数据库 - 休眠1s再删缓存)
- A进程删除缓存、写数据库后,主从同步时,被B从从库查数据(缓存为空,从库复制还未完成),再次更新了缓存(相当于没删)
- 解决方案:1. 延时双删;2. 更新redis的查询操作,指向主库查询
- 异步双删,增加吞吐量
- 不推荐的原因:1. 缓存穿透到DB;2. 休眠时间不好设置
- A进程删除缓存后,还没来得及更新数据库,就被B进程查询数据库(缓存为空)后再次更新了缓存(相当于没删)
- 先更新数据库,后删除缓存
- 依然有并发问题:进程A查询数据库(缓存为空)后写入缓存前,被进程B更新了数据库,导致缓存与DB不一致
- 概率极低,因为需要让进程B写数据库的操作耗时比进程读数据库还短,而读库操作一般比写库要快
- 解决方案:1. 设置缓存失效时间;2. 异步延时删除;3. 使用Canal监听binlog日志,更新缓存
- 依然有并发问题:进程A查询数据库(缓存为空)后写入缓存前,被进程B更新了数据库,导致缓存与DB不一致
删除缓存失败的解决方案:
- 使用MQ再次删除
- 使用canal采集binlog日志,发送到MQ中,然后通过ACK机制确认处理删除缓存
缓存更新的设计模式
- Cache Aside (最常用,因为强一致的实现性能往往较差):缓存失效时由调用方加载(上面的第四种方案)
- Read/Write Through:把更新数据库操作由缓存自己代理了
- Read Through:缓存失效时由用缓存服务自己来加载
- Write Through:当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库。
- Write Behind Caching:,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库
- 数据不是强一致性的,而且可能会丢失
- 实现复杂
缓存数据一致性
- 本地缓存
- 设置失效时间(毛刺现象)
- 设置双缓存
- 只有两份本地缓存都没有,才会到远程获得。
- 正式缓存是最后一次写入后经过固定时间过期
- 备份缓存是设置最后一次访问后经过固定时间过期
- 异步刷新:正式缓存只有无效才会被重新写入,备份缓存无论是否无效都会重新写入
- 强制本地缓存失效和手动刷新本地缓存(CacheManagerController)
- 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 节点之间
- 负责在客户端和Redis 节点之间转发请求和响应。客户端只与代理服务打交道,代理收到客户端的请求之后,会转发到对应的Redis 节点上,节点返回的响应再经由代理转发返回给客户端。
- 负责监控集群中所有Redis 节点的状态,如果发现存在问题节点,就及时进行主从切换。
- 维护集群的元数据,这个元数据主要是集群所有节点的主从信息,以及槽和节点的关系映射表。像开源的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