应用缓存通常分两种,本地缓存和远程缓存。本地缓存就是内存缓存 LocalCache,远程缓存就是分布式共享缓存比如 Redis。本地缓存在访问性能上远胜过远程缓存,但是在一致性上要弱一些。我们平时经常会用到的 Guava Cache 就是内存缓存技术框架。

Redis6 反复提到的「客户端缓存」就是本地缓存,这意味着 Redis 欲将缓存的魔爪从分布式共享缓存延伸到内存缓存,进一步榨干缓存的技术市场。如果该技术未来普遍流行起来,内存缓存相关技术框架也会被打掉半壁江山。Redis 誓要将缓存能力做到极致。

我们平时经常说的 CAP 定律,是说在分布式系统中,如果出现了网络分区 P,一致性 C 和可用性 A 不能两全。这里的可用性可以不严格的简单理解为访问性能,性能慢的难以忍受就是不可用。内存缓存舍一致性得高性能,远程缓存舍高性能得一致性。

到这里可能有读者要提问了,Redis 不是最终一致性的超高性能存储数据库么,怎么到这里它又成了「舍高性能」得「一致性」呢?

有这个疑问是正常的,因为这里说的舍和得只是相对于内存缓存而言的。相比于内存缓存,远程缓存的读写涉及到网络 IO,性能上自然要弱一些。

Redis 和本地双缓存 redis缓存和本地缓存_java

如果一个 API 服务有多个物理进程,每个进程里面都有一份内存缓存的数据(比如全局配置参数),这多个进程的内存缓存的数据在同一时间就会不一致。API 服务进程可能会选择每隔 N 秒轮询式从远程缓存同步一次最新的数据到内存,那么在这 N 秒范围内,数据的一致性是要打折的。如果没有这个内存缓存,API 服务获取全局配置参数总是要从远程缓存获取最新的参数,这就不存在配置一致性问题。

那 Redis 要对这个「客户端缓存」做到什么程度呢?它如何平衡性能和一致性的问题呢?上面的例子中提到的多进程之间本地缓存不一致的本质在于「轮询式」的时间间隔。如果轮询的够快,数据也就会更加一致一些,但是这也会对远程缓存增加访问压力。还有一种比较明显的方式就是当远程缓存中的数据发生变动时,主动通知各进程更新本地缓存,那么不一致的问题就可以得到非常显著的缓解。

Redis6 的这个「客户端缓存」就是用的这种方式,主动通知客户端 —— 你的数据过时了,请赶快刷新。看到这里,对 Redis 稍微熟悉一点的同学可能很快就会想到 Redis 有个 Pub/Sub 的订阅更新能力是不是可以实现这个小需求,何必要大张旗鼓发明「客户端缓存」这个新概念呢?

好,下面我们来看一下 Redis 提供的 Pub/Sub 该如何才能做到这一点呢?有两种方式

  1. 使用自定义的 channel,当远程缓存变化时,修改方(业务进程中的生产方)需要执行 Publish 指令。消费方订阅这个 channel,收到消息时刷新本地缓存。这里生产和消费就有了一定程度的耦合,消费者能不能及时刷新缓存取决于生产者有没有配合 Publish 消息。而且每个业务点需要一个独立的不一样的 channel 名称,不能混淆。
  2. 使用 Redis 自带的 Keyspace Notification Event 内置的一些 channel。当某个 Key 被删除时,会向 del channel 发送一个 Del 事件。当某个 Key 过期时,会向 expire channel 发送一个 Expire 事件 。会有非常多的内置 channel。当某个 Key 被 Set 时,会向  set channel 发送一个 Set 事件等等。这里的问题在于客户端需要监听处理很多的 内置channel 才能知道内存缓存关联的那个 Redis Key 值是否发生了变化。如果开启了 Keyspace Notification Event,事件发生的太频繁了,Redis 的性能也会受到显著的影响。除此之外,这里还存在一个明显的惊群问题,我不想关心的事件 Redis 也会通知给我,因为这里的内置 channel 是所有 key 共享的,任意的 key 发生的变化,channel 的消费者都能收到相应的事件。

基于这个原因,Redis6 对「客户端缓存」进行了重新设计,让它使用起来更加方便而且不会显著导致 Redis 本身的性能下降。有了「客户端缓存」,Redis 服务器本身的访问压力也会显著减轻,应用程序只需要访问本地内存就可以得到期望的数据,如此 Redis 就可以应用于更高的并发应用场景。

Redis6 将「客户端缓存」称为「Client Key Tracking」,表示客户端对指定的 Key 感兴趣,它会订阅这些 Key 的修改通知,如果 Key 发生了变化,客户端会立即收到一个「缓存失效」通知。紧接着客户端就会清空并重建本地缓存。

那如何订阅具体的 Key 呢,Redis6 提供了两种方式,自动订阅和手动订阅。自动订阅就是客户端的某个开关打开后,服务器会自动帮助客户端订阅它所读取的所有的 Key。这种自动订阅的方式虽然很方便,在某些特定的场合下可能并不合适。所以 Redis 也提供了 手动订阅的方式,需要在每一条需要缓存的读 Key 命令之前打上一条特殊的标记表示接下来的这条指令读取的值会缓存在内存里。

除此之外,Redis 还提供了前缀订阅指令(也叫广播指令),可以让客户端一次性订阅以固定前缀开头的所有的 Key。这种方式需要小心使用,如果前缀对应的 Key 非常多而且修改又很频繁就会给服务器带来广播风暴,严重影响服务器的性能。

使用 Client Key Tracking 的原则就是读多写少,比如业务系统使用的全局配置参数

  1. 变化频繁的 Key 不要本地缓存,缓存刷新过于频繁
  2. 读频率低的 Key 不要缓存,缓存意义不大

遗憾的是,大部分企业都还没能用得上 Redis5,就更别提 Redis6 了。对于这个新特性,我们还是慢慢等着时间来逐步接受它吧。