• [转]缓存的介绍


    1. 什么是缓存

    缓存有很多种,从 CPU 缓存、磁盘缓存到浏览器缓存等,本文所说的缓存,主要针对后端系统的缓存。也就是将程序或系统经常要使用的对象存在内存中,以便在使用时可以快速调用,也可以避免加载数据或者创建重复的实例,以达到减少系统开销,提高系统效率的目的。

    2. 为什么要用缓存

    我们一般都会把数据存放在关系型数据库中,不管数据库的性能有多么好,一个简单的查询也要消耗毫秒级的时间,这样我们常说的 QPS 就会被数据库的性能所限制,我们想要提高QPS,只能选择更快的存储设备。

    在日常开发有这样的一种场景:某些数据的数据量不大、不经常变动,但访问却很频繁。受限于硬盘 IO 性能或者远程网络等原因,每次都直接获取会消耗大量的资源。可能会导致我们的响应变慢甚至造成系统压力过大,这在一些业务上是不能忍的,而缓存正是解决这类问题的神器。

    但是有一点需要注意,就是缓存的占用空间以及缓存的失效策略,下文也会提到。

    3. 使用缓存的场景

    对于缓存来说,数据不常变更且查询比较频繁是最好的场景,如果查询量不够大或者数据变动太频繁,缓存也就是失去了意义。

    日常工作使用的缓存可以分为内部缓存和外部缓存。

    内部缓存一般是指存放在运行实例内部并使用实例内存的缓存,这种缓存可以使用代码直接访问。

    外部缓存一般是指存放在运行实例外部的缓存,通常是通过网络获取,反序列化后进行访问。

    一般来说对于不需要实例间同步的,都更加推荐内部缓存,因为内部缓存有访问方便,性能好的特点;需要实例间同步的数据可以使用外部缓存。

    下面对这两种类型的缓存分别的进行介绍。

    3.1 内部缓存

    为什么要是用内部缓存

    在系统中,有些数据量不大、不常变化,但是访问十分频繁,例如省、市、区数据。针对这种场景,可以将数据加载到应用的内存中,以提升系统的访问效率,减少无谓的数据库和网路的访问。

    内部缓存的限制就是存放的数据总量不能超出内存容量,毕竟还是在 JVM 里的。

    最简单的内部缓存 - Map

    功能强大的内部缓存 - Guava Cache / Caffeine

    Guava中本地缓存基本原理为:ConcurrentMap(利用分段锁降低锁粒度) + LRU算法。

    本地缓存的优点:

    • 直接使用内存,速度快,通常存取的性能可以达到每秒千万级

    • 可以直接使用 Java 对象存取

    本地缓存的缺点:

    • 数据保存在当前实例中,无法共享

    • 重启应用会丢失

    Guava Cache 的替代者 Caffeine

    Spring 5 使用 Caffeine 来代替 Guava Cache,应该是从性能的角度考虑的。从很多性能测试来看 Caffeine 各方面的性能都要比 Guava 要好。

    Caffeine 的 API 的操作功能和 Guava 是基本保持一致的,并且 Caffeine 为了兼容之前 Guava 的用户,做了一个 Guava 的 Adapter, 也是十分的贴心。

    如果想了解更多请参考:是什么让 Spring 5 放弃了使用 Guava Cache?

    3.2 外部缓存

    最著名的外部缓存 - Redis / Memcached

    也许是 Redis 太有名,只要一提到缓存,基本上都会说起 Redis。但其实这类缓存的鼻祖应该是 LiveJournal 开发的 Memcached。

    Redis / Memcached 都是使用内存作为存储,所以性能上要比数据库要好很多,再加上Redis 还支持很多种数据结构,使用起来也挺方便,所以作为很多人的首选。

    Redis 确实不错,不过即便是使用内存,也还是需要通过网络来访问,所以网络的性能决定了 Reids 的性能;

    我曾经做过一些性能测试,在万兆网卡的情况下,对于 Key 和 Value 都是长度为 20 Byte 的字符串的 get 和 set 是每秒10w左右的,如果 Key 或者 Value 的长度更大或者使用数据结构,这个会更慢一些;

    作为一般的系统来使用已经绰绰有余了,从目前来看,Redis 确实很适合来做系统中的缓存。

    如果考虑多实例或者分布式,可以考虑下面的方式:

    • Jedis 的 ShardedJedis( 调用端自己实现分片 )

    • twemproxy / codis( 第三方组件实现代理 )

    • Redis Cluster( 3.0 之后官方提供的集群方案 )

    这些方案各有特点,这次先不展开讨论,有兴趣的可以先研究一下。

    Redis有很多优点:

    • 很容易做数据分片、分布式,可以做到很大的容量

    • 使用基数比较大,库比较成熟

    同时也有一些缺点:

    • Java 对象需要序列化才能保存

    • 如果服务器重启,再不做持久化的情况下会丢失数据,即使有持久化也容易出现各种各样的问题

    4. 缓存的更新策略

    使用缓存时,更新策略是非常重要的。最常见的缓存更新策略是 Cache Aside Pattern:

    • 失效:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

    • 命中:应用程序从 cache 中取数据,取到后返回。

    • 更新:先把数据存到数据库中,成功后,再让缓存失效。

    不管是内部缓存还是外部缓存,都可以使用这样的更新策略,如果缓存系统支持,也可以通过设置过期时间来更新缓存。

    更多的更新策略可以参考左耳朵耗子的这篇缓存更新的套路。

    5. 缓存使用常见误区

    序列化方案的选择

    序列化的选择,尽量避免使用 Java 原生的机制,因为原生的序列化依赖 serialVersionUID 来判断版本,如果改变就无法正常的反序列化。

    一般推荐使用 Json 或者 Hessian、ProtoBuf 等二进制方式。

    缓存大对象

    在缓存中存放大对象,存取的代价都比较高。实际使用时,往往只是需要其中的一部分,这样会导致每一次读取都消耗更多的网络和内存资源,也会浪费缓存的容量。

    当然如果每次都是用完整的对象,这样做是没有问题的。

    使用缓存进行数据共享

    使用缓存来当作线程甚至进程之间的数据共享方式,会让系统间产生隐形的依赖,并且也可能会产生一些竞争,常常会发生问题。所以不推荐使用这种方式来共享数据。

    没有及时更新或者删除缓存中已经过期或失效的数据

    这个理解起来就很简单了,如果没有及时更新或者删除,就有可能读取到错误的数据,从而导致业务的错误。

    对于支持设置过期时间的缓存系统,可以对每一个数据设置合适的过期时间,来尽量避免这样的情况。

    6. libshmcache介绍

    libshmcache使用场景

        如果需要缓存的数据量不是太大,比如不超过100w个key,对缓存读写性能要求又比较高的情况下,可以考虑使用

        libshmcache采用的开源协议为BSD,托管在github,地址:https://github.com/happyfish100/libshmcache

  • 相关阅读:
    13_graphicals_view.md
    8_菜单栏、工具栏和状态栏.md
    2_按钮&对象数.md
    11_事件.md
    9_对话框.md
    序列化与反序列化 未知结构的数据 Any interface类型
    nil 接口不是 nil
    图片存储格式之一,由JPEG格式衍生而来,后缀为".jfif"。
    Why goroutines instead of threads? https://go.dev/doc/faq#atomic_maps
    源码 //go:nosplit
  • 原文地址:https://www.cnblogs.com/ibigboy/p/10985586.html
Copyright © 2020-2023  润新知