[转]缓存的介绍

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

posted @ 2019-06-06 16:12  问北  阅读(410)  评论(0编辑  收藏  举报