返回 导航

其他

hangge.com

常见应用系统缓存方案介绍(请求、会话级别缓存、客户端缓存、预热预加载)

作者:hangge | 2025-06-24 09:54

一、请求级别缓存

1,基本介绍

(1)所谓的请求级别缓存是指当请求返回响应的时候,缓存也就失效了。
(2)使用这种缓存方案的前提是我们在一个请求里面会反复查询同一个数据多次。
  • 比如说,因为模块划分之后要恪守边界,所以每个模块都会自己去调用接口来获得同一份数据。
  • 那么我们就可以考虑将数据和请求关联在一起,做成一个在请求生命周期内有效的缓存。
(3)和本地缓存之类的方案比起来,请求级别的缓存不需要考虑一致性的问题。因为你的缓存数据在请求返回响应之后就无效了,撑死了就是有人在你处理这个请求的过程中修改了数据,但是你并不知道。而大多数的时候,你可能并不在意这么一点不一致

2,样例说明

(1)假设我们有两个模块,一个是订单模块,一个是支付模块。这两个模块因为封装得都非常好,所以它们都只需要你传入一个 user_id,这两个模块拿着 user_id 去查询对应的用户信息,完成对应的业务。
  • 当没有使用请求级别缓存的时候,它们一共发出两次数据库查询。
  • 当我们用了请求级别缓存的时候,订单模块查询到之后,支付模块只需要直接用就可以。

(2)这个东西一般是需要特殊的中间件来支持的。
  • 比如说在 Java 里面可以直接借助 RequestScopeBean 来缓存数据。
  • 而在 Go 里面可以借助 context.Context 来完成。

二、会话级别缓存

1,基本介绍

(1)类似于请求级别缓存,会话级别缓存是在用户和系统的会话结束之后就失效了。
(2)简单来说,就是你把缓存做成类似于登录态里 Session 那种东西,Session 结束了,那么缓存的数据也失效了。
(3)它的好处同样是数据一致性问题没那么严重。它相当于在用户和你的系统保持会话的期间,使用的数据始终是同一份。比如说用户权限信息,它对一致性的要求很高,但是使用又非常频繁。

2,样例说明

(1)我在做系统性能优化的时候,发现了我们的系统有一个共同点,就是经常需要查询权限信息,来做鉴权。但是经过分析,我发现权限之类的信息在一个会话期间基本不可能修改。
  • 于是我就引入了一个会话级别的缓存,系统先查询会话中缓存的权限信息,找不到就去查询权限模块。
  • 同时,监听对用户权限信息的修改,在发生修改之后,直接清空会话中缓存的权限数据。

(2)这个在 Java 里面也很好落地,因为 JavaSpring 支持 Session-ScopeBean。你只需要把数据放到这种 Bean 里面就可以。

三、客户端缓存

1,基本介绍

    在微服务架构里面,有些时候性能要求非常苛刻,又或者你对一致性要求不是特别高,于是你就会想在调用别人的微服务的时候把结果缓存下来。等到下一次有类似的请求过来,就可以直接用自己缓存的数据了。

2,样例说明

(1)某个服务有很多接口,但是大部分接口都需要用到用户的信息,所以我就做了一个客户端缓存。思路是这样的:
  • 每次我发起调用查询了用户的信息之后,我就会把它缓存起来,设置一个比较短的过期时间,比如说一分钟。
  • 后续如果需要这个数据的时候,我就先从缓存里拿数据,如果缓存里没有数据,再调用用户服务的接口。
  • 这种做法最大好处的是省了一次微服务调用的开销。

(2)和服务端缓存比起来,我这个方案还有一个意想不到的优点,就是服务端的淘汰策略不会影响到我。
  • 我举个例子,虽然服务端也缓存了数据,但是因为服务端是给很多业务提供服务的。那么就可能出现这种问题:某个业务的查询相对我的业务的查询来说,非常高频,或者优先级更高,导致服务端在淘汰键值对的时候,总是优先淘汰我用的键值对。
  • 那么就会出现,服务端整体上缓存命中率很高,但是我这个特定的业务方,缓存命中率很低的问题。而我自己缓存的话,淘汰的肯定就是我这个业务不怎么需要的。

(3)这种用法也是一种反范式的用法,它会加剧数据一致性的问题。因为按照正统的微服务理论来说,应该是服务端缓存数据,而不是在客户端缓存数据。否则的话,如果服务端接收到了请求,修改了数据,客户端这边是没有感知的。
  • 为了解决这个问题,我们又引入了一个变种,也是在客户端这边缓存,但这个缓存是服务端提供的依赖来管理的。比如说在 Java 这边,引入服务端的 Jar 包,服务端的 Jar 包会自己先查询缓存,当缓存没有数据的时候,才会真的发出请求到服务端。
  • 不过,正常来说,除非你同时是服务端的研发,不然很难要求别的组或者别的部门给你提供这样一个依赖。


四、业务相关缓存预加载

1,基本介绍

    所谓的业务相关缓存预加载是指如果从业务上来看,用户调用了 A 接口之后会很快调用 B 接口,那么我就可以在 A 接口调用的时候把 B 接口用得上的数据缓存起来,又或者在调用 A 接口的时候,提前加载 B 接口的缓存。

2,样例说明

(1)比如系统里面,用户调用A 接口之后,大概率会调用 B 接口。所以我就做了两件事,A 接口和 B 接口都需要的数据,我直接缓存起来。如果 B 接口还需要一些额外的数据,那么我可以提前发起微服务调用,拿到数据之后一起缓存起来。

(2)后续用户真的调用了 B 接口,就直接从缓存里拿数据,性能很好。就算没有调用 B 接口,我在缓存的时候,设置的过期时间也很短,也不会有什么问题。


五、缓存预热和预加载

1,两种思路

(1)在启动的时候,就直接把缓存加载到缓存中。可以全量加载,也可以只加载热点数据。在使用这种策略的时候,就要小心缓存雪崩的问题。
(2)启动之后,不是立刻就打过来 100% 的流量,而是先小比例地把流量打过来,在处理这些请求的过程中,缓存中的数据会慢慢加载好。

2,样例说明

(1)之前我使用的本地缓存来优化性能的时候,为了防止在节点刚启动的时候本地缓存中还没有数据,导致性能抖动,我引入了缓存预热的机制。

(2)在节点刚启动的时候,它的权重会很低。这个时候基于权重的负载均衡就只会把少量流量转发过来。而后在运行了一段时间之后,这个节点的权重就会提高到正常数值。这个时候负载均衡就会把正常水平的流量打过来。

评论

全部评论(0)

回到顶部