Java for You Java 5.1 分布式缓存架构

5.1 分布式缓存架构

什么是缓存 Cache 缓存:存储在计算机上的一个原始数据复制集,以便于访问。【维基百科】 缓存是介于数据访问者和数据源之间的一种高速存储,当数据需要多次读取的时候,用于加快读取的速度。 缓存(Cache)和缓冲(Buffer)的分别? 缓

什么是缓存 Cache

缓存:存储在计算机上的一个原始数据复制集,以便于访问。【维基百科】

缓存是介于数据访问者和数据源之间的一种高速存储,当数据需要多次读取的时候,用于加快读取的速度。

缓存(Cache)和缓冲(Buffer)的分别?

缓冲常用于硬盘,网络等低速介质的读取。

无处不在的缓存

  • CPU 缓存
  • 操作系统缓存
  • 数据库缓存
  • JVM 编译缓存
  • CDN 缓存
  • 代理与反向代理缓存
  • 前端缓存
  • 应用程序缓存
  • 分布式对象缓存
  • 缓存数据存储(Hash 表)

    也有树结构,但哈希表常见。

    缓存的关键指标

    缓存命中率

    缓存命中率

  • 缓存是否有效依赖于能多少次重用同一个缓存响应业务请求,这个度量指标被称作缓存命中率。
  • 如果查询一个缓存,十次查询九次能够得到正确结果,那么它的命中率是 90%。
  • 影响缓存命中率的主要指标

  • 缓存键集合大小
  • 缓存可使用内存空间
  • 缓存对象生存时间
  • 缓存键集合大小

    缓存中的每个对象使用缓存键进行识别,定位一个对象的唯一方式就是对缓存键执行精确匹配。

    例如,如果想为每个商品缓存在线商品信息,你需要使用商品ID作为缓冲键。

    换句话说,缓冲键空间是你的应用能够生成的所有键的数量。

    从统计数字上看,应用生成的唯一键越多,重用的机会越小。

    例如,如果想基于客户 IP 地址缓冲天气数据,则可能有多达 40 亿个键(这是所有可能的 IP 地址的数量)。

    如果要基于客户来源国家缓冲天气数据,则可能仅需几百个缓冲键(世界上所有国家的数量)。

    一定要想办法减少可能的缓存键数量。键数量越少,缓存的效率越高。

    缓存可使用内存空间

    缓存可使用内存空间直接决定了缓存对象的平均大小和缓冲对象数量。

    因为缓存通常存储在内存中,缓存对象可用空间受到严格限制且相对昂贵。

    如果想缓存更多的对象,就需要先删除老的对象,再添加新的对象。

    替换(清除)对象会降低缓存命中率,因为缓存对象被删除后,将来的请求就无法命中了。物理上能缓存的对象越多,缓存命中率就越高。

    缓存对象生存时间

    缓存对象生存时间成为 TTL(Time To Live)。在某些场景中,例如,缓存天气预报数据 15 分钟没问题。

    在这个场景下,你可以设置缓冲对象预定义 TTL 为 15 分钟。在其他场景中,你可能不能冒险使用过于陈旧的数据。例如,电商系统中的商品价格,管理员修改后需要让缓存失效。

    对象缓存的时间越长,缓存对象被重用的可能性就越高。

    几种常见的缓存

    代理缓存

    反向代理缓存

    key: URL

    value: url 上对应的资源

    多层反向代理缓存

    restful 风格的 URL 相对统一。

    内容分发网络(CDN)

    视频网站严重依赖 CDN。

    CDN 同时配置静态文件和动态内容

    通读缓存(read-through)

  • 代理缓存,反向代理缓存,CDN 缓存都是通读缓存。
  • 通读缓存给客户端返回缓存资源,并在请求未命中缓存时获取实际数据。
  • 客户端连接的是通读缓存而不是生成响应的原始服务器。
  • 旁路缓存(cache-aside)

  • 对象缓存是一种旁路缓存,旁路缓存通常是一个独立的键值对(key-value)存储。
  • 应用代码通常会询问对象缓存需要的对象是否存在,如果存在,它会获取并使用缓存的对象,如果不搓澡或已过期,应用会连接主数据源来组装对象,并将其保存回对象缓存中以便将来使用。
  • 浏览器对象缓存

    本地对象缓存

  • 对象直接缓存在应用程序内存中。
  • 对象存储在共享内存,同一台机器的多个进程可以访问它们。
  • 缓存服务器作为独立应用和应用程序部署在同一个服务器上。
  • 本地对象缓存构建分布式集群

    已淘汰。

    远程分布式对象缓存

    Memcached 分布式对象缓存

    缓存模型

    Memcached 分布式缓存访问模型

    集群:提供相同功能,对外提供服务。

    路由算法:对 key 哈希,取模,取到目标服务器。

    缓存算法

    分布式对象缓存的一致性 hash 算法

  • 一致性哈希环
  • 0~2 的 32 次 -1
  • 服务器节点放在环上
  • key 的哈希值放到环中,距离服务器节点最近。
  • 问题:负载不均衡。
  • 一致性 Hash 节点扩容

    key0 和 key3 起初访问 node1,node3 扩容后,key0 和 key3 开始访问node3,首次 miss,路由到数据源,带回数据 put 到 node 3 中,二次访问能命中 node 3 的缓存。

    基于虚拟节点的一致性 Hash 算法

    各种介质数据访问延迟

    技术栈各个层次的缓存

    缓存为什么能显著提升性能

  • 缓存数据通常来自内存,比磁盘上的数据有更快的访问速度。
  • 缓存存储数据的最终结果形态,不需要中间计算,减少 CPU 资源的消耗。
  • 缓存降低数据库、磁盘、网络的负载压力,使这些 I/O 设备获得更好的响应特性。
  • 缓存是系统性能优化的大杀器

  • 技术简单
  • 性能提升显著
  • 应用场景多
  • 合理使用缓存

    使用缓存对提升系统性能有很多好处,但是不合理的使用缓存可能非但不能提高系统的性能,还会成为系统的累赘,甚至风险。

    实践中,缓存滥用的情景屡见不鲜——过分依赖缓存、不合适的数据访问特性。

    频繁修改的数据:这种数据如果缓存起来,由于频繁修改,应用还来不及读取就已失效或更新,徒增系统负担。一般说来,数据的读写比在 2:1 以上,缓存才有意义。

    没有热点的访问

    没有热点的访问:缓存使用内存作为存储,内存资源宝贵而有限,不能将所有数据都缓存起来,如果应用系统访问数据没有热点,不遵循二八定律,即大部分数据访问不是集中在小部分数据上,那么缓存就没有意义,因为大部分数据还没有被再次访问就已经被挤出缓存了。

    【LRU】

    数据不一致与脏读

    数据不一致与脏读:一般会对缓存的数据设置失效时间,一旦超过失效时间,就要从数据库中重新加载。因此应用要容忍一定时间的数据不一致,如卖家已经编辑了商品属性,但是需要过一段时间才能被买家看到。

    在互联网应用中,这种延迟通常是可以接受的,但是具体应用仍需慎重对待。

    还有一种策略是数据更新时立即更新缓存,不过也会带来更多系统开销和事务一致性的问题。因此数据更新时通知缓存失效,删除该缓存数据,是一种更加稳妥的做法。

    “计算机科学中只有三件事最困难:缓存失效,命名事物,计数错误。”——Phil Karlton

    缓存雪崩

    缓存雪崩:缓存是为了提高数据读取性能的,缓存数据丢失或者缓存不可用不会影响到应用程序的处理——它可以从数据库直接获取数据。

    但是随着业务的发展,缓存会承担大部分的数据访问压力,数据库已经习惯了有缓存的日子,所以当缓存服务器崩溃的时候,数据库会因为完全不能承受如此大的压力而宕机,进而导致整个网站不可用。

    这种情况,被称为缓存雪崩,发生这种故障,甚至不能简单的重启缓存服务器和数据库服务器来恢复网站访问。

    缓存预热

    缓存预热:缓存中存放的是热点数据,热点数据又是缓存系统利用 LRU(最近最久未用)算法对不断访问的数据筛选淘汰出来的,这个过程需要花费较长的时间,在这段时间,系统的性能和数据库负载都不太好,那么最好在缓存系统启动的时候就把热点数据加载好,这个缓存预加载手段叫做缓存预热(warm up)。

    对于一些元数据如城市地名列表、类目信息,可以启动时加载数据库中全部数据到缓存进行预热。

    缓存穿透

    缓存穿透:如果不恰当的业务、或者恶意攻击持续高并发的请求某个不存在的数据,因为缓存没有保存该数据,所有的请求都会落到数据库上,会对数据库造成很大的压力,甚至崩溃。

    一个简单的对策是将不存在的数据也缓存起来(其 value 值为 null),并设定一个较短的失效时间。【空缓存】

    Redis VS Memcached

  • Redis 支持复杂的数据结构
  • Redis 支持多路复用异步 I/O 高性能
  • Redis 支持主从复制高可用
  • Redis 原生集群与 share nothing 集群模式
  • Redis 集群

  • Redis 集群预分好 16384 个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384 的值,决定将一个 key 放到哪个桶中。
  • Redis=cluster 把所有的物理节点映射到[0-16383] slot 上(不一定是平均分配),cluster 负责维护 slot 与服务器的映射关系。
  • 客户端与 Redis 节点直连,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
  • 所有的 Redis 节点彼此互联。
  • 动手验证

    public String get(String key) {
        String v = redis.get(key);
        if (v != null) return v;
    
        // 拿不到重建锁就直接返回兜底值,别让并发全打穿到数据库
        Boolean locked = redis.set(key + ":lock", "1", "SET NX EX 10");
        if (!Boolean.TRUE.equals(locked)) return null;
        try {
            v = db.query(key);
            // 空值也缓存,并给 TTL 加随机抖动,避免大批 key 同时失效
            redis.set(key, v == null ? "" : v, 60 + ThreadLocalRandom.current().nextInt(60));
            return v;
        } finally {
            redis.del(key + ":lock");
        }
    }
    

    延伸阅读:官方与权威资料

    相关阅读(站内)

    常见问题

    Q:G1 GC 和 CMS 垃圾回收器有什么区别?

    G1 把堆拆成多个 Region,用标记-整理避免内存碎片,目标是可预测的停顿时间;CMS 用标记-清除,回收快但会产生碎片。JDK 9 起 G1 成为默认收集器,JDK 14 移除 CMS。

    Q:G1 的 Remembered Set 是干什么的?

    Region 化之后跨 Region 的引用都要记账,Remembered Set 记录其他 Region 指向本 Region 的指针,回收某个 Region 时不用扫全堆,这是 G1 停顿可控的关键之一。

    Q:怎么调优 G1 的停顿时间?

    三步走:先把 -XX:MaxGCPauseMillis 设在合理区间(别贪低,太小会疯狂扩容堆)、再看 GC 日志里的 Mixed GC 频率定位大对象、用 -XX:InitiatingHeapOccupancyPercent 控制并发标记起手时机。

    Q:为什么 Java 的 String 要设计成不可变?

    不可变才能安全共享与缓存哈希值,也能安全地放进字符串常量池;一旦可变,其他线程持有引用时修改会破坏哈希表与缓存的一致性。

    Q:Java 里 == 和 equals 的区别是什么?

    == 比较引用地址,equals 比较对象内容。String、Integer 等包装类重写了 equals,自定义对象不重写 equals 时,equals 等价于 ==。


    本文首发于 java4u.cn,转载请注明出处。

    关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。

    本文由 java4u.cn 发布,可自由转载、引用,但需署名作者且注明文章出处(作者:白色蜗牛,出处:java4u.cn)。如转载至微信公众号,请在文末添加作者公众号二维码。 https://java4u.cn/java_base/3047.html

    作者: 蜗牛

    下一篇
    5.1 分布式缓存架构

    已经没有了

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注

    联系我们

    联系我们

    公众号:蜗牛互联网

    在线咨询: QQ交谈

    关注微信
    微信扫一扫关注我们

    微信扫一扫关注我们

    关注微博
    返回顶部