Java开发从入坑到入沟-缓存的内存,带宽和钱(一)

779 阅读5分钟

几乎每个正经Saas产品的公司都会用到Redis缓存,并且沉迷不能自拔。

因为Redis简直太棒了,反应迅速,超高并发,作为中心化的存储还可以用来处理分布式锁,什么地方慢了,开发大兄弟的第一反应就是把慢的东西缓存到redis去。

Redis中缓存的东西也是千奇百怪,小的可能只有1个字节,大的可能是个复杂对象的列表(MB级别)。用户量越来越大,新功能越来越多,日积月累,Redis的内存占用一路飙升,从2g到4g,4g到8g,8g到16g。

直到有一天Redis出口带宽开始频繁报警。想想也不难理解,要的东西又多又快又杂,Redis自己性能还高,最后居然是带宽先撑不住了。

难道又要充钱么?查了下发现要提高带宽,xx云需要把内存再提高一倍,这不是捆绑销售么,算了算要多花不少钱,而且长此以往也不是回事儿。这玩意总有个极限,就算充到最高vip等级,万一还不够怎么办呢?

只要思想不滑坡,办法总比困难多。

作为一个朴实的劳动人民,碰到要花钱的事儿,一定会想尽办法捂住口袋!

朴实劳动人民的奇思妙想1:马马虎虎的本地缓存

这是最容易想到的办法。效果自然也是马马虎虎。

把一部分缓存挪到应用节点本地,不再存储到redis,减少redis内存和带宽占用。现在本地缓存框架也不少了,最出名的是caffine,提供了完善的缓存,失效机制,性能非常棒,和Spring Cache无缝对接,没什么学习成本。

这个方案的最大问题,是多个节点的缓存难以同步。

比如一个用户key “U2205”的缓存,从Redis挪到了每个应用节点的内存上。

之前任何一个应用节点修改了用户状态后,会同时刷新Redis缓存,其他应用节点都从Redis读,不会读到旧数据。

现在“U2205”的缓存挪到每个应用节点的内存了,一个节点更新了”U2205“的用户状态,没办法通知到其他应用节点了,其他节点只能老老实实等到本地缓存失效后,才能读到最新的值。

这在很多场景下是不可忍受的。

朴实劳动人民的奇思妙想2:Redis Pub/Sub

Redis提供了Pub/Sub的事件订阅模式,记得18年的时候Redis 大佬 Ben Malec 讨论过使用这种方式在各个节点间同步缓存值的几种方案。

然而redis的事件机制比较简单,没有重试,补发,有些情况会同步失败,导致失败节点的缓存一直无法更新,虽然只是缓存没有更新,但难免担心极端情况会出大篓子。

朴实劳动人民的奇思妙想3:节点缓存有效性指示器和延时通知

这回朴实的劳动人民终于想到了捂住口袋的方法!

缓存其实就是一个个键值对,key和value一一对应。

为了减少redis的内存占用和网络消耗,value一定要存储在本地内存,别无它路。

可以利用Redis存储一个节点缓存有效性指示器,来完成各个应用节点之间的缓存同步。

抱歉发明了一个新词汇让大家困扰了。让我郑重地解释一下这个词儿。

节点缓存有效性指示器(indicator):

存储在Redis中,说明一个key在各个应用节点的本地缓存是否有效。

举个例子。

我们有两台应用服务器节点,N1和N2。两个节点都在本地缓存了Key ”U2205”的值。

那么在Redis中会维护一个“U2205”的节点缓存有效性指示器。它看起来就像是这样 {“U2205”:[N1,N2]}。 含义是U2205这个key在N1和N2节点中的缓存是有效的,可以直接拿来用。

我们做了什么? 现在Redis中只缓存了一个key的有效服务器列表,每个服务器的名字可以相当短,比如以数字命名每个服务器,当你有10个服务器节点(1,2,3…10)连着redis的时候,这个指示器的存储大概只占了20个字节。这比目前动辄几K上M的缓存大小下降了几百倍!而缓存占用下降直接导致了网络流量下降。

那么这个指示器如何工作呢?

多图预警

节点更新后,依赖有效性指示器,节点间同步缓存的流程

多节点缓存同步

传统的缓存方案,Value全部存储在Redis中,导致Redis内存占用高,Redis出口带宽高

sp1

优化后的方案,Redis只存储节点缓存有效性指示器,内存占用和出口带宽大大减少

sp2

当节点1编辑了本地Key1的值为Value4后,重置Key1在Redis的节点有效性指示器为N1

sp3

当节点2希望访问Key1时,Redis告知节点2,它本地的缓存已经失效。于是节点2重新从库中查询了最新的Key1的值,查到了Value4,并向Redis申请将Key1的有效指示器列表中添加节点2

sp4

准备动手

至此,我们终于有了一个看起来比较靠谱的思路,来解决中心化Redis的内存和带宽问题。

这个方案看起来并不复杂,勤劳的大白已经在尝试着手实现了,过两天给大家分享下,看看效果如何。