聊聊本地缓存和分布式缓存( 二 )


redis 是分布式缓存的首选 , 甚至我们一提到缓存 , 很多后端工程师首先想到的就它 。
下图是神州专车订单的 Redis 集群架构。将 Redis 集群拆分成四个分片 , 每个分片包含一主一从 , 主从可以切换 。应用 A 根据不同的缓存 key 访问不同的分片 。

聊聊本地缓存和分布式缓存

文章插图
与本地缓存相比 , 分布式缓存具有以下优点:
1、容量和性能可扩展
通过增加集群中的机器数量 , 可以扩展缓存的容量和并发读写能力 。同时 , 缓存数据对于应用来讲都是共享的 。
2、高可用性
由于数据被分布在多台机器上 , 即使其中一台机器故障 , 缓存服务也能继续提供服务 。
但是分布式缓存的缺点同样不容忽视 。
1、网络延迟
分布式缓存通常需要通过网络通信来进行数据读写 , 可能会出现网络延迟等问题 , 相对于本地缓存而言 , 响应时间更长 。
2、复杂性
分布式缓存需要考虑序列化、数据分片、缓存大小等问题 , 相对于本地缓存而言更加复杂 。
举一个真实的案例 , 这次案例让笔者对于分布式缓存的认知提上了另一个台阶 。
2014年 , 同事开发了比分直播的系统 , 所有的请求都是从分布式缓存 Memcached 中获取后直接响应 。常规情况下 , 从缓存中查询数据非常快 , 但在线用户稍微多一点 , 整个系统就会特别卡 。
通过 jstat 命令发现 GC 频率极高 , 几次请求就将新生代占满了 , 而且 CPU 的消耗都在 GC 线程上 。初步判断是缓存值过大导致的 , 果不其然 , 缓存大小在 300k 到 500k 左右 。
解决过程还比较波折 , 分为两个步骤:
  1. 修改新生代大小 , 从原来的 2G 修改成 4G , 并精简缓存数据大小 (从平均 300k 左右降为 80k 左右);
  2. 把缓存拆成两个部分 , 第一部分是全量数据 , 第二部分是增量数据(数据量很小) 。页面第一次请求拉取全量数据 , 当比分有变化的时候 , 通过 websocket 推送增量数据 。
经过这次优化 , 笔者理解到:缓存虽然可以提升整体速度 , 但是在高并发场景下 , 缓存对象大小依然是需要关注的点 , 稍不留神就会产生事故 。另外我们也需要合理地控制读取策略 , 最大程度减少 GC 的频率 , 从而提升整体性能 。
四、多级缓存开源中国网站最开始完全是用本地缓存框架 Ehcache。后来随着访问量的激增 , 出现了一个可怕的问题:“因为 Java 程序更新很频繁 , 每次更新的时候都要重启 。一旦重启后 , 整个 Ehcache 缓存里的数据都被清掉 。重启后若大量访问进来的话 , 开源中国的数据库基本上很快就会崩掉” 。
于是 , 开源中国开发了多级缓存框架  J2Cache , 使用了多级缓存 Ehcache + Redis  。
多级缓存有如下优势:
  1. 离用户越近 , 速度越快;
  2. 减少分布式缓存查询频率 , 降低序列化和反序列化的 CPU 消耗;
  3. 大幅度减少网络 IO 以及带宽消耗 。
本地缓存做为一级缓存 , 分布式缓存做为二级缓存 , 首先从一级缓存中查询 , 若能查询到数据则直接返回 , 否则从二级缓存中查询 , 若二级缓存中可以查询到数据 , 则回填到一级缓存中 , 并返回数据 。若二级缓存也查询不到 , 则从数据源中查询 , 将结果分别回填到一级缓存 , 二级缓存中 。
聊聊本地缓存和分布式缓存

文章插图
2018年 , 笔者服务的一家电商公司需要进行 App 首页接口的性能优化 。笔者花了大概两天的时间完成了整个方案 , 采取的是两级缓存模式 , 同时利用了 Guava 的惰性加载机制 , 整体架构如下图所示:
聊聊本地缓存和分布式缓存

文章插图
缓存读取流程如下:


推荐阅读