查看原文
其他

四种缓存的避坑总结

ImportNew 2022-10-20

The following article is from 编程一生 Author 编程一生

背景


分布式、缓存、异步和多线程被称为互联网开发的四大法宝。今天我总结一下项目开发中常接触的四种缓存实际项目中遇到过的问题。

JVM 堆内缓存


JVM 堆内缓存因为可以避免 Memcached、Redis 等集中式缓存网络通信故障问题,目前还在项目中广泛使用。

堆内缓存需要注意 GC 的问题。假如我们的设计是定时的从远程来拉取数据更新本地缓存。一定要注意两点:第一不要全量拉取覆盖,第二不要把一个大对象整体替换为新对象。

先说全量拉取覆盖。全量拉取会有很大的网络开销,会造成网络流量尖刺。有人说没事,我们带宽很足,内网访问,不怕不怕。但是稳定性需要修炼的一项是削峰填谷。让系统在平稳的环境中运行。不然,在拉取大缓存新数据的数据突然来了个突发流量?根据墨菲定律,凡是有几率会发生的事情就一定会发生。编程需谨慎。

再说大对象整体替换的问题,这会造成 GC 问题。伪代码如下:

List<POJO> oldList = initList();public void refresh() { List<POJO> newList = dataFromNetworkService.getAll(); oldList = new List(); for(POJO pojo : newList) { oldList.add(pojo); }}

如果从网上拉取的数据和在缓存里存储的数据,对象类型没有发生改变。引起的转换开销还稍微小点。因为比如对象 POJO 存在一个列表里。这个列表虽然很大,但是里面存的都是对象的引用。实际的 POJO 并没有发生变化。上面伪代码虽然新建一个 List 对象,遍历添加新对象比直接 oldList=newList 要傻些。但是遍历过程实际上 POJO 对象没有发生改变。所以这里影响 GC 的只是 oldList 这个对象(不包括从网络上拉取回来数据的过程)。

但是如果代码这样写:

List<POJO2> oldList = initList();public void refresh() { List<POJO1> newList = dataFromNetworkService.getAll(); oldList = new List(); for(POJO2 pojo : newList) { oldList.add(Beanutils.copy(new POJO2(), pojo)); }}


遍历过程将会将原来的 POJO1 全部新建一遍,这些对象一般情况下全部先进入堆内存的新生代,再经过数次 Young GC 后进入老年代。会造成GC频繁。

我所做过的项目,一般认为一天一到两次 Full GC 为合理值。这样,如果比如预先知道某个时间点有大促,可通过提前触发 GC 等方式避免高峰期爆发 Full GC。Young GC 至少是 5 分钟一次,甚至更久触发认为是正常。这样可以通过控制避过秒杀等场景。


JVM 堆外缓存


堆外缓存的内存回收原理使用的是 Java 的虚引用。这个设计可以避免 JVM 的 GC 问题,但是处理不好可能会造成更严重的后果:整个机器内存被打满,机器可能会挂掉。其实挂掉一台在一般企业的生产环境还好,因为一般都会有容灾的冗余机器。但是更常见的一种情况是机器忙于 swap 内存交换,机器活着但是响应很慢。属于半死不活。这个问题我没在线上遇到过,但是我同事之前在超级大厂的时候遇到过。

有的同学说那我严格算好内存,做好监控。这里面要就要依赖人为的因素来做紧急处理。而人是稳定性中最不可靠的。因为问题通常不发生在人清醒、手里事情很少的时候。而是一种雪上加霜的存在。比如大促时,流量上来了,线程数会增多,每个线程都会申请线程栈资源,系统处理 IO,这时候系统会申请更多的 buffers/cached 内存。


Linux 的 buffers/cached


Linux 系统上运行一下 top 命令或者 free 命令,都能够看到 buffers 和 cached 相关的数据。需要注意的是通常我们看到的监控数据空闲内存百分比,并非是下面显示的 free/total,而是 (free+buffers+cached)/total。


buffers 在 Linux 系统中通常被作为与块存储的 IO 缓存使用。所谓块存储可简单理解为将数据直接写到裸磁盘。而 cached 则一般会用于文件系统的 IO 缓存。比如 page cache 这种内存换页功能。

听不明白也没关系,因为事实上它们两个经常配合使用。比如与磁盘交换数据、进行网络通信时都会用。buffers 和 cached 是实实在在被操作系统的系统进程在使用的,但是如果用户进程需要可以很快释放。所以通常会将它算到剩余可用内存里。

但是这个也要注意了。比如在 IO 密集型的系统,如果 buffers/cached 被大幅占用,会降低 IO 速度,进而降低系统吞吐。甚至有可能一个请求几秒才能到达应用程序,造成请求超时。

集中式缓存


Redis 缓存其实也有本机代理,可以缓存一些活跃的数据在本机上,本机可以在取不到数据时不需要跨网络通信。但是因为 Redis 本质是 key-value 的结构。如果需要根据通配符取数据全量,如果网络出现故障,可能会影响数据的完整性。

但是 Redis 缓存最让人担心的是不规范的使用方法。比如存一个很大的 value。具体这个对网络和存储造成的问题就不详细说了。可以想象下马桶堵了的情景。


总结


贝尔实验室的面向对象编程专家 Tom Cargill 说:


最初 90% 的开发工作将会用去你最初 90% 的开发时间,剩下的 10% 的开发量将会用去你另外一个 90% 的开发时间。


我理解剩下 10% 占用了 90% 的时间是由于超出了原有知识贮备,需要临时抱佛脚,甚至需要拿着锤子找钉子造成的。所以或者也可以这样做:

每周持续投入 5% 的学习时间,10% 的思考时间,再用 100% 的时间去完成 100% 的开发。


- EOF -

推荐阅读  点击标题可跳转

1、经理让我复盘上次 Redis 缓存雪崩事故

2、缓存和数据库一致性问题,看这篇就够了

3、基于SSD的Kafka应用层缓存架构设计与实现


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

点赞和在看就是最大的支持❤️



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

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