设为首页 加入收藏

TOP

聊一聊作为高并发系统基石之一的缓存,会用很简单,用好才是技术活(一)
2023-07-25 21:27:59 】 浏览:80
Tags:高并发 石之一 简单

大家好,又见面了。


本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面。如果感兴趣,欢迎关注以获取后续更新。


在服务端开发中,缓存常常被当做系统性能扛压的不二之选。在实施方案上,缓存使用策略虽有一定普适性,却也并非完全绝对,需要结合实际的项目诉求与场景进行综合权衡与考量,进而得出符合自己项目的最佳实践。

缓存使用的演进

现有这么一个系统:

一个互动论坛系统,用户登录系统之后,可以在论坛上查看帖子列表、查看帖子详情、发表帖子、评论帖子、为帖子点赞等操作。

系统中所有的配置数据与业务数据均存储在数据库中。随着业务的发展,注册用户量越来越多,然后整个系统的响应速度也越来越慢,用户体验越来越差,用户逐渐出现流失。

本地缓存的牛刀小试

为了挽救这一局面,开发人员需要介入去分析性能瓶颈并尝试优化提升响应速度,并很快找到响应慢的瓶颈在数据库的频繁操作,于是想到了使用缓存来解决问题。

于是,开发人员在项目中使用了基于接口维度的短期缓存,对每个接口的请求参数(帖子ID)与响应内容缓存一定的时间(比如1分钟),对于相同的请求,如果匹配到缓存则直接返回缓存的结果即可,不用再次去执行查询数据库以及业务维度的运算逻辑。

JAVA中有很多的开源框架都有提供类似的能力支持,比如Ehcache或者Guava CacheCaffeine Cache等,可以通过简单的添加注解的方式就实现上述需要的缓存效果。比如使用Ehcache来实现接口接口缓存的时候,代码使用方式如下(这里先简单的演示下,后续的系列文档中会专门对这些框架进行深入的探讨):

@Cacheable(value="UserDetailCache", key="#userId")
public UserDetail queryUserDetailById(String userId) {
    UserEntity userEntity = userMapper.queryByUserId(userId);
    return convertEntityToUserDetail(userEntity);
}

基上面的本地缓存策略改动后重新上线,整体的响应性能上果然提升了很多。本地缓存的策略虽然有效地提升了处理请求的速度,但新的问题也随之浮现。有用户反馈,社区内的帖子列表多次刷新后会出现内容不一致的情况,有的帖子刷新之后会从列表消失,多次刷新后偶尔会出现。

其实这就是本地缓存在集群多节点场景下会遇到的一个很常见的缓存漂移现象:

因为业务集群存在多个节点,而缓存是每个业务节点本地独立构建的,所以才出现了更新场景导致的本地缓存不一致的问题,进而表现为上述问题现象。

集中式缓存的初露锋芒

为了解决集群内多个节点间执行写操作之后,各节点本地缓存不一致的问题,开发人员想到可以构建一个集中式缓存,然后所有业务节点都读取或者更新同一份缓存数据,这样就可以完美地解决节点间缓存不一致的问题了。

业界成熟的集中式缓存有很多,最出名的莫过于很多人都耳熟能详的Redis,或者是在各种面试中常常被拿来与Redis进行比较的Memcached。也正是由于它们出色的自身性能表现,在当前的各种分布式系统中,Redis近乎已经成为了一种标配,常常与MySQL等持久化数据库搭配使用,放在数据库前面进行扛压。比如下面图中示例的一种最简化版本的组网架构:

开发人员对缓存进行了整改,将本地缓存改为了Redis集中式缓存。这样一来:

  1. 缓存不一致问题解决:解决了各个节点间数据不一致的问题。

  2. 单机内存容量限制解决:使用了Redis这种分布式的集中式缓存,扩大了内存缓存的容量范围,可以顺便将很多业务层面的数据全部加载到Redis中分片进行缓存,性能也相比而言得到了提升。

似乎使用集中式缓存已经是分布式系统中的最优解了,但是现实情况真的就这么简单么?也不尽然

多级缓存的珠联璧合

在尝到了集中式缓存的甜头之后,暖心的程序员们想到要彻底为数据库减压,将所有业务中需要频繁使用的数据全部同步存储到Redis中,然后业务使用的时候直接从Redis中获取相关数据,大大地减少了数据库的请求频次。但是改完上线之后,发现有些处理流程中并没有太大的性能提升。缘何如此?只因为对集中式缓存的过分滥用!分析发现这些流程的处理需要涉及大量的交互与数据整合逻辑,一个流程需要访问近乎30次Redis!虽然Redis的单次请求处理性能极高,甚至可以达到微秒级别的响应速度,但是每个流程里面几十次的网络IO交互,导致频繁的IO请求,以及线程的阻塞唤醒切换交替,使得系统在线程上下文切换层面浪费巨大

那么,要想破局,最常规的手段便是尝试降低对集中式缓存(如Redis)的请求数量,降低网络IO交互次数。而如何来降低呢? —— 又回到了本地缓存!集中式缓存并非是分布式系统中提升性能的银弹,但我们可以将本地缓存与集中式缓存结合起来使用,取长补短,实现效果最大化。如图所示:

上图演示的也即多级缓存的策略。具体而言:

  • 对于一些变更频率比较高的数据,采用集中式缓存,这样可以确保数据变更之后所有节点都可以实时感知到,确保数据一致;

  • 对于一些极少变更的数据(比如一些系统配置项)或者是一些对短期一致性要求不高的数据(比如用户昵称、签名等)则采用本地缓存,大大减少对远端集中式缓存的网络IO次数。

这样一来,系统的响应性能又得到了进一步的提升。

通过对缓存使用策略的一步步演进,我们可以感受到缓存的恰当使用对系统性能的帮助作用。

无处不在的缓存

缓存存在的初衷,就是为了兼容两个处理速度不一致的场景对接适配的。在我们的日常生活中,也常常可以看到“缓存”的影子。比如对于几年前比较盛行的那种带桶的净水器(见下图),由于净水的功率比较小,导致实时过滤得到纯净水的水流特别的缓慢,用户倒一杯水要等2分钟,体验太差,所以配了个蓄水桶,净水机先慢慢的将净化后的水存储到桶中,然后用户倒水的时候可以从桶里快速的倒出,无需焦急等待 —— 这个蓄水桶,便是一个缓存器

编码源于生活,CPU高速缓存设计就是这一生活实践在计算机领域的原样复制。缓存可以说在软件世界里无处不在,除了我们自己的业务系统外,在网络传输操作系统中间件基础框架中都可以看到缓存的影子。如:

  1. 网络传输场景

比如ARP协议,基于ARP缓存表进行IP与终端硬件MAC地址之间的缓存映射。这样与对端主机之间有通信需求的时候,就可以在ARP缓存中查找到IP对应的对端设备MAC地址,避免每次请求都需要去发送ARP请求查询MAC地址。

  1. MyBatis的多级缓存

MyBatis作为JAVA体系中被广泛使用的数据库操作框架,其内部为了提升处理效率,构建了一级缓存二级缓存,大大减少了对SQL的重复执行次数。

  1. CPU中的缓存

CPU内存之间有个临时存储器(高速缓存),容量虽比内存小,但是处理速度却远快于普通内存。高速缓存的机制,有效地解决了CPU运算速度内存读写速度不匹配的问题。

缓存的使用场景

缓存作为互联网类软件系统架构与实现中的基石般的存在,不仅仅是在系统扛压或者接口处理速度提升等性能优化方案,在其他多个方面都可以发挥其独一

首页 上一页 1 2 3 下一页 尾页 1/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇使用Spring AOP实现系统操作日志.. 下一篇day53-马踏棋盘

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目