查看原文
其他

缓存实战二三事

丁浪 技术琐话 2021-08-09

上次列举了一些缓存相关的常见问题和解决思路,这些问题是在实际工作中可能会遇到的。很多系统在业务量不大时可能不会暴露出问题,但是遇到“高并发”就会产生很多问题,缓存也不例外。所以在选择缓存策略和使用缓存之前,我们非常有必要结合当前的业务场景和需求去分析设计。一般我会从访问频率、读写比例、一致性要求等几个维度去分析。

 

冷数据:

访问频度低,缓存的收益不明显。

热数据:

访问频度高,缓存收益一般明显。

是否使用缓存,优先参考的是业务的访问频度和并发量,而不是执行速度。什么意思呢?比如某个业务执行需要3s才能完成,我们觉得有点慢,但它本身使用频度很低,也不存在什么并发,那我们肯定会优先去给那些执行1s但是存在并发的业务加缓存。

 

读多写少:

适合缓存,收益明显。

写多读少

不太适合缓存,收益不明显,额外增加系统复杂度。

 

一致性要求低:

业务可以容忍(某段时间)出现不一致,可以最终一致。先天适合缓存,设计难度较低。

一致性要求高:

业务数据敏感,无法容忍不一致(或者可容忍时间非常短)。缓存设计难度相对较大。

 

面对的场景不同,缓存设计和处理策略也不同。我曾经见过一个系统的代码,为了避免上面提到的“缓存并发”问题,直接在缓存帮助类中公共的get方法上加了lock,这显然是不合理的。首先,并不是所有业务都会有“缓存穿透”的问题,其次,这种处理方式也太低级。

 

缓存并发导致的穿透问题如何解决

下面具体的聊聊我在实际工作中一般是如何应对解决“缓存并发穿透”问题的。

方案A(后台刷新):

在缓存过期之前,通过后台线程或者job主动更新缓存。例如,缓存的过期时间为30分钟,而后台job则每隔29分钟执行一次(job中查询出最新的数据并写入到缓存中)。

这种方案比较容易理解,但会增加系统复杂度。比较适合那些key相对固定、cache粒度较大的业务,key比较分散的则不太适合,实现起来也比较复杂。

 

方案B(检查更新):

将缓存key的过期时间(绝对时间)也一起保存到缓存中(可以拼接,也可以加新字段,也可以采用单独的key保存,反正需要两者建立好关联关系)。在每次执行get操作后,都将get出来的缓存过期时间与当前系统时间做一个对比,如果发现缓存过期时间-当前系统时间<=1分钟,则主动更新缓存。这样就能保证缓存中始终是最新的(和方案A的思路本质上一样,就是为了保证缓存“始终是最新的”且“永不过期”),不用担心缓存失效和一致性的问题。当然,这个1分钟只是举例,可以根据实际情况定义或者配置的。

这种方案在特殊情况下也会有问题。假设缓存过期时间是11:30分,而11:29到11:30这1分钟时间里恰好没有get请求过来,恰好请求都在11:30分的时候并发过来,那就悲剧了。这种情况比较极端,但并不是没有可能。因为“高并发”也可能是阶段性在某个时间点爆发。

 

方案C(分级缓存):

分级缓存。采用L1和L2缓存方式,L1缓存失效时间短,L2缓存失效时间长。请求优先从L1缓存获取数据,如果L1缓存未命中则加锁,只有1个线程获取到锁,改线程从数据库中读取,再将数据set到L1缓存和L2缓存中,而其他线程依旧从L2缓存获取数据并返回。

这种方式,主要是通过避免缓存同时失效并结合锁机制实现。所以,当数据更新时,只能淘汰L1缓存,不能同时将L1和L2中的缓存同时淘汰。L2缓存中可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案可能会造成额外的缓存空间浪费。

 

方案D(互斥锁):

加锁等待。采用互斥锁的方式。

注意,不能直接在缓存加载逻辑判断时直接采用synchronize。下面列出几种常见做法:

 

方法1:

这种方式,确实能够防止缓存失效时并发打到数据库,但缓存没有失效的时候呢?也需要排队获取锁然后去获取数据,岂不是大大降低了系统的吞吐量。

还有另外一种写法:

方法2:

这种方式,当缓存命中的时候,系统的吞吐量是不会受影响的。但是,缓存失效的时候,请求还是会打到数据库中,只不过不是并发的,而是阻塞进行的,无疑会牺牲用户体验,并给数据库带来额外的压力。

方法3:

这种做法呢。似乎避免了前面那2种问题,但似乎还是不太完美。因为执行双重检查的那里,虽然是避免了请求打到数据库,但是命中缓存的过程依旧是排队进行的。

例如:当缓存失效时,有30个请求并发读库。使用同步加双重检查机制,可以让1个线程先读库然后写缓存了,剩下的29个线程命中缓存。但是,那29个线程是串行排队的在读缓存,效率方面肯定有影响。

方法4:

 

使用互斥锁的方式来实现,可以有效避免前面几种问题。为了方便演示和测试,就直接使用的Java中的ReentranLock。

在实际分布式场景中,可以使用redis、tair、zookeeper等提供的分布式锁来实现,感兴趣的朋友自行查阅相关资料,这里不展开。

新书推荐:《深入分布式缓存》


京东购书,扫描二维码:


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存