ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于本地缓存的 fallback 降级机制:Hystrix 服务降级的原理与实战

基于本地缓存的 fallback 降级机制:Hystrix 服务降级的原理与实战 基于本地缓存的 fallback 降级机制Hystrix 服务降级的原理与实战【免费下载链接】advanced-java Core Interview Questions Answers For Experienced Java(Backend) Developers | 互联网 Java 工程师进阶知识完全扫盲涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识项目地址: https://gitcode.com/doocs/advanced-javaHystrix 的 fallback 降级机制是分布式系统高可用保障的最后一道防线当断路器打开、资源池已满、依赖调用异常或超时的时候系统不再等待或报错而是快速返回一个兜底结果避免故障在调用链上蔓延。本篇以 advanced-java 高可用架构系列中的电商商品详情页场景为例完整演示如何在HystrixCommand中通过getFallback()实现从本地缓存读取稍过期的品牌名称这一经典降级方案并深入讲解降级的触发条件、关键参数与最佳实践。一、什么是 fallback 降级机制在复杂的分布式系统中服务 A 会调用服务 B服务 B 又依赖服务 C/D/E。任何一个下游依赖出现延迟或故障都可能耗尽上游服务的线程资源导致整个服务崩溃并不断蔓延参见 电商网站的商品详情页系统架构 中商品服务接口故障导致缓存服务资源耗尽的案例。Hystrix 通过资源隔离、熔断、限流等手段保护系统而当保护机制被触发、或者依赖调用本身失败时就需要fallback 降级机制来给出一个快速返回的兜底结果。所谓降级就是在系统资源紧张或依赖故障时主动放弃一些非核心功能或使用简化方案保证核心功能可用。Hystrix 出现以下四种情况都会去调用 fallback 降级机制断路器处于打开的状态。断路器打开意味着短时间内异常比例过高此时不再调用下游服务直接走降级逻辑快速返回详见 深入 Hystrix 断路器执行原理。资源池已满线程池 队列 / 信号量。请求量超过了线程池与等待队列、或信号量允许的并发上限超出的请求被直接 reject转而执行降级。Hystrix 调用各种接口或者访问外部依赖比如 MySQL、Redis、Zookeeper、Kafka 等等出现了任何异常的情况。run()或construct()执行过程中抛出任意异常都会触发降级。访问外部依赖的时候访问时间过长报了TimeoutException异常。当 command 执行时长超过设定的 timeout 阈值时Hystrix 会标记该 command 为超时并执行降级参见 基于 timeout 机制为服务接口调用超时提供安全保护。二、两种最经典的降级机制当触发降级时fallback 逻辑中返回什么决定了降级的效果。业界最经典、也最推荐的做法有两种1. 纯内存数据在降级逻辑中可以在内存中维护一个 ehcache作为一个纯内存的基于 LRU 自动清理的缓存让数据放在缓存内。如果说外部依赖有异常fallback 这里直接尝试从 ehcache 中获取数据。由于数据完全来自本机内存不涉及任何网络请求响应速度极快能够承受超高并发。2. 默认值fallback 降级逻辑中也可以直接返回一个默认值比如固定文案、空对象、降级商品之类的占位数据保证接口有响应、流程不中断。这两种方案的本质都是fail-fast 快速失败宁可返回一个凑合的结果也不让调用线程被慢速依赖 hang 住。三、fallback 在 Hystrix 执行流程中的位置要真正理解 fallback需要先把它放回 Hystrix 完整的执行流程中看。Hystrix 从执行一个 command 开始共经历 8 大步骤fallback 正是其中最后一道兜底防线步骤八创建 command一个HystrixCommand或HystrixObservableCommand对象代表对某个依赖服务的一次请求。调用 command 执行方法可选execute()同步、queue()异步返回 Future、observe()、toObservable()四种方式最终都依赖toObservable()执行。检查是否开启请求缓存若命中 request cache 则直接返回缓存结果详见 基于 request cache 请求缓存技术优化批量商品数据查询接口。检查是否开启断路器若断路器打开不执行 command直接走 fallback。检查线程池/队列/信号量是否已满已满则直接走 fallback同时发送 reject 事件给断路器统计。执行 command调用run()HystrixCommand或construct()HystrixObservableCommand。若执行超时或抛出异常则走 fallback。断路健康检查将成功、失败、reject、timeout 等事件发送给断路器统计。调用 fallback 降级机制在上述任一异常场景下给出兜底结果。可以看到步骤四、五、六中的断路器打开资源池满超时/异常正是本文第一部分列出的四种触发场景。需要特别强调的是Hystrix 是无法真正终止一个调用严重延迟的依赖服务线程的只能抛出TimeoutException并立刻切换执行降级逻辑保证调用线程不被 hang 死。fallback 未实现或自身异常时的行为如果 command 没有实现 fallback或者 fallback 自身抛出了异常Hystrix 会返回一个不携带任何数据的 Observable。针对不同的 command 执行方式具体表现不同执行方式fallback 为空或异常时的表现execute()直接抛出异常queue()返回一个 Future调用get()时抛出异常observe()返回一个 Observable订阅时立即触发调用者的onError()toObservable()返回一个 Observable订阅时立即触发调用者的onError()因此生产环境中务必为每个 command 提供可靠的降级实现否则降级机制本身会变成新的故障源。四、实战 Demo品牌服务故障时从本地缓存降级下面我们用一个完整的例子演示 fallback 降级是怎么做的。业务场景假设有一份包含brandId的商品数据。正常逻辑是拿到商品数据后根据brandId去调用品牌服务的接口获取品牌的最新名称brandName。假如品牌服务接口挂掉了那么我们可以尝试从本地内存中获取一份稍过期的数据先凑合着用——这就是一次典型的外部依赖故障 → 本地缓存兜底降级。步骤一本地缓存获取数据先定义一个品牌名称的本地缓存类BrandCache内部用一个HashMapLong, String维护brandId - brandName的映射并提供静态方法按品牌 id 查询/** * 品牌名称本地缓存 * */ public class BrandCache { private static MapLong, String brandMap new HashMap(); static { brandMap.put(1L, Nike); } /** * brandId 获取 brandName * * param brandId 品牌id * return 品牌名 */ public static String getBrandName(Long brandId) { return brandMap.get(brandId); } }这是纯内存数据降级策略的最小实现。生产环境中可以把这里的HashMap替换为 ehcache、Caffeine 等带 LRU 自动清理能力的本地缓存组件数据则由后台定时任务或 MQ 消息驱动持续更新参见 电商网站的商品详情页系统架构 中缓存服务从消息队列消费变更消息、推送数据的模式这样降级时拿到的就是稍旧但可用的数据。步骤二实现 GetBrandNameCommand接下来定义GetBrandNameCommand。在run()方法中正常的逻辑是去调用品牌服务的接口获取品牌名称如果调用失败报错就会触发 fallback 降级机制。为了演示方便这里直接模拟接口调用报错抛出异常。而在getFallback()方法中就是我们的降级逻辑直接从本地缓存BrandCache中获取品牌名称的数据。/** * 获取品牌名称的command * */ public class GetBrandNameCommand extends HystrixCommandString { private Long brandId; public GetBrandNameCommand(Long brandId) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(BrandService)) .andCommandKey(HystrixCommandKey.Factory.asKey(GetBrandNameCommand)) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() // 设置降级机制最大并发请求数 .withFallbackIsolationSemaphoreMaxConcurrentRequests(15))); this.brandId brandId; } Override protected String run() throws Exception { // 这里正常的逻辑应该是去调用一个品牌服务的接口获取名称 // 如果调用失败报错了那么就会去调用fallback降级机制 // 这里我们直接模拟调用报错抛出异常 throw new Exception(); } Override protected String getFallback() { return BrandCache.getBrandName(brandId); } }代码中有几点值得注意run()方法中真实场景应该使用 HTTP 客户端调用品牌服务接口类似 基于 Hystrix 线程池技术实现资源隔离 中GetProductInfoCommand使用HttpClientUtils.sendGetRequest(url)的做法这里直接抛异常用于模拟故障。getFallback()与run()的返回类型一致都是String降级时返回BrandCache.getBrandName(brandId)。若缓存中也没有该品牌HashMap.get会返回null这也是一种可接受的兜底结果。如果品牌服务超时而不是报错同样会进入getFallback()逻辑无需任何改动——降级与故障的具体形态解耦。步骤三CacheController 调用接口在CacheController中通过productInfo获取brandId然后创建GetBrandNameCommand并执行去尝试获取brandName。这里执行会报错因为我们在run()方法中直接抛出异常Hystrix 就会去调用getFallback()方法走降级逻辑Controller public class CacheController { RequestMapping(/getProductInfo) ResponseBody public String getProductInfo(Long productId) { HystrixCommandProductInfo getProductInfoCommand new GetProductInfoCommand(productId); ProductInfo productInfo getProductInfoCommand.execute(); Long brandId productInfo.getBrandId(); HystrixCommandString getBrandNameCommand new GetBrandNameCommand(brandId); // 执行会抛异常报错然后走降级 String brandName getBrandNameCommand.execute(); productInfo.setBrandName(brandName); System.out.println(productInfo); return success; } }执行流程为getProductInfoCommand.execute()正常获取商品数据 → 取出brandId→ 创建getBrandNameCommand→execute()时run()抛异常 → Hystrix 自动调用getFallback()→ 从本地缓存取到brandName例如Nike→ 回填到productInfo并返回。对于调用方CacheController而言整个过程无感知它拿到的依然是一个可用的品牌名称只是数据源从远程品牌服务切换成了本地缓存。这就是降级的意义——故障被隔离在 Hystrix 内部而不向外扩散。五、降级逻辑的关键参数FallbackIsolationSemaphoreMaxConcurrentRequests上述代码中配置了降级机制的一个重要参数.withFallbackIsolationSemaphoreMaxConcurrentRequests(15)FallbackIsolationSemaphoreMaxConcurrentRequests用于设置fallback 最大允许的并发请求量默认值是 10。它通过semaphore 信号量的机制对降级逻辑做限流一旦同时执行 fallback 的请求数超过该上限超出的请求会被直接 reject此时只能抛出异常或返回空结果而不再继续执行降级逻辑。这一设计非常必要降级逻辑本身也是要消耗资源的在依赖大规模故障的极端情况下如果不限制降级请求的并发量海量请求同时涌入降级代码可能连兜底自身都被打垮。该参数与资源隔离中的execution.isolation.semaphore.maxConcurrentRequestsSEMAPHORE 隔离策略下允许的最大并发访问量默认值也是 10思路一致都是用信号量做轻量级限流。区别在于前者保护的是降级代码后者保护的是正常执行代码。需要注意的是信号量限流只能控制并发数无法像线程池那样对延迟调用做 timeout 隔离——如果降级逻辑中进行了慢速网络调用请求会一直 block 住。这也引出了下文的实践建议。六、HystrixCommand 与 HystrixObservableCommand 的降级实现差异Hystrix 提供两种 command 基类它们的降级实现方式不同在HystrixCommand中降级逻辑通过实现getFallback()方法完成返回单条结果。本文的GetBrandNameCommand即属于此类。在HystrixObservableCommand中则是实现resumeWithFallback()方法返回一个Observable对象可以用于降级时产出多条结果。HystrixObservableCommand的降级示例如下public class GetProductInfosCommand extends HystrixObservableCommandProductInfo { Override protected ObservableProductInfo resumeWithFallback() { // 降级逻辑返回一个发射兜底数据的 Observable return Observable.just(new ProductInfo(/* 降级商品数据 */)); } }当construct()执行出错或超时时Hystrix 会转而订阅resumeWithFallback()返回的 Observable将降级结果发射给调用方。返回多条数据时可以配合Observable.from(...)逐个发射。七、降级逻辑编写的最佳实践综合 Hystrix 的执行原理深入 Hystrix 执行时内部原理与本 Demo编写 fallback 时有几条关键原则尽量给出默认值或静态逻辑在降级机制中建议返回静态代码或内存缓存中的数据例如本文的BrandCache.getBrandName(brandId)。这类逻辑不涉及网络请求响应极快、可靠性高。尽量避免在降级中进行网络请求降级本就是为了避免网络故障若降级逻辑反而发起新的网络调用如查询数据库一旦下游持续故障降级请求同样会被 hang 住违背 fail-fast 的初衷。若降级中必须进行网络调用请将该调用放在一个独立的 HystrixCommand 中进行隔离这样即使降级链路中的网络调用失败也不会拖垮当前线程仍然可以通过更外层的 fallback 兜底形成降级中的降级。为每个 command 都实现 fallback如第三节所述fallback 缺失或异常时不同执行方式会直接抛错因此完备的降级实现是 Hystrix 容错能力生效的前提。合理设置 fallback 并发上限根据降级逻辑本身的耗时与资源开销调节withFallbackIsolationSemaphoreMaxConcurrentRequests默认值 10可结合 QPS 适当调整避免降级逻辑被打爆。八、小结fallback 降级机制是 Hystrix 高可用体系资源隔离 → 熔断 → 降级中面向用户请求的最终出口无论是断路器打开深入 Hystrix 断路器执行原理、线程池/队列/信号量已满深入 Hystrix 线程池隔离与接口限流、基于 Hystrix 信号量机制实现资源隔离、还是调用异常与超时基于 timeout 机制为服务接口调用超时提供安全保护最终都会收敛到getFallback()或resumeWithFallback()上通过本地缓存或默认值快速返回将故障影响控制在最小范围。本文的BrandCacheGetBrandNameCommandCacheController三段式代码是本地缓存降级最精简可运行的完整范例。把它扩展到真实系统时只需将HashMap换成 ehcache/Caffeine 等 LRU 缓存、将throw new Exception()换成真实的品牌服务 HTTP 调用即可获得一套生产可用的服务降级能力。本系列中所有 Hystrix 主题文档位于 docs/high-availability涵盖隔离、熔断、降级、缓存、超时等完整高可用知识体系可继续深入阅读。【免费下载链接】advanced-java Core Interview Questions Answers For Experienced Java(Backend) Developers | 互联网 Java 工程师进阶知识完全扫盲涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识项目地址: https://gitcode.com/doocs/advanced-java创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表