SpringBoot升級(jí)后Hystrix沒了以及3個(gè)替代方案分享
寫在前面
公司項(xiàng)目要從 Spring Boot 2.2.x 升級(jí)到 2.7,領(lǐng)導(dǎo)讓我負(fù)責(zé)調(diào)研。
拿到手一看,好家伙,Hystrix 停更了?Feign 的 fallback 怎么報(bào)錯(cuò)了?Ribbon 也不推薦用了?
網(wǎng)上的文檔要么太深?yuàn)W,要么太簡(jiǎn)單。沒辦法,只能自己一點(diǎn)點(diǎn)啃,順便把踩坑經(jīng)歷記下來(lái)。
如果你也是新手,希望這篇筆記能幫到你。
環(huán)境信息
- Spring Boot: 2.2.x → 2.7.18
- Spring Cloud: Hoxton.SR9 → 2021.x
- Hystrix: 1.5.x(停更)→ Resilience4j 1.7.x
- Ribbon: 2.x(停更)→ Spring Cloud LoadBalancer
- OpenFeign: 2.2.x → 3.1.x
坑 1:Hystrix 停更了,用啥替代?
我當(dāng)時(shí)以為很簡(jiǎn)單
直接升級(jí) Spring Boot 和 Spring Cloud,心想依賴會(huì)自動(dòng)更新…
<!-- 我當(dāng)時(shí)看到這個(gè)配置,心想這不簡(jiǎn)單嗎 --> <spring-boot.version>2.7.18</spring-boot.version> <spring-cloud.version>2021.x</spring-cloud.version>
結(jié)果啟動(dòng)失?。?/p>
*************************** APPLICATION FAILED TO RUN *************************** Description: The Spring Cloud project contains incompatible modules: spring-cloud-starter-netflix-hystrix is not compatible with Spring Cloud 2021.x and later.
我當(dāng)時(shí)就懵了。啥?Hystrix 不兼容?
問題根源
后來(lái)查資料才明白:
- Hystrix 停更:Netflix Hystrix 從 2020 年開始就進(jìn)入維護(hù)模式,不再更新
- Spring Cloud 移除:Spring Cloud 2020.x(Illford)開始移除了對(duì) Hystrix 的支持
- 官方推薦:Spring 官方推薦使用 Resilience4j 作為替代方案
好家伙,原來(lái)是時(shí)代的眼淚。
解決方案
用 Resilience4j 替代 Hystrix
<!-- 移除 Hystrix 依賴 -->
<!--
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
-->
<!-- 添加 Resilience4j 依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>配置 Resilience4j:
# application.yml
resilience4j:
circuitbreaker:
instances:
backendA: # 服務(wù)名
registerHealthIndicator: true
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 5
automaticTransitionFromOpenToHalfOpenEnabled: true
timeout:
timeout: 3s
cancelRunningFuture: false使用方式:
@Service
public class MyService {
@CircuitBreaker(name = "backendA", fallbackMethod = "fallback")
public String callExternalService() {
// 調(diào)用外部服務(wù)
return "success";
}
// fallback 方法
public String fallback(Exception e) {
log.error("服務(wù)調(diào)用失敗,執(zhí)行 fallback: {}", e.getMessage());
return "fallback result";
}
}
教訓(xùn):Hystrix 已經(jīng)停更了,別再用!官方推薦 Resilience4j!
坑 2:Feign 的 FallbackFactory 報(bào)錯(cuò)了
我只排除了部分依賴
升級(jí)后,F(xiàn)eign Client 的 fallback 全部報(bào)錯(cuò):
// 原來(lái)的代碼
@FeignClient(name = "soa-protocol-control",
fallbackFactory = ControlVehicleFeignServiceFallback.class)
public interface ControlVehicleFeignService {
// ...
}
啟動(dòng)報(bào)錯(cuò):
java.lang.IllegalStateException: FallbackFactory must return an instance of the FeignClient interface
我當(dāng)時(shí)又懵了。這又是啥?
原因分析
后來(lái)才明白:
- Hystrix 移除:
feign.hystrix.FallbackFactory是 Hystrix 時(shí)代的產(chǎn)物 - 包路徑變更:Spring Cloud 2021.x 開始,F(xiàn)allbackFactory 的包路徑變了
- 返回值要求:fallback 必須返回 FeignClient 接口的實(shí)例
正確做法
1. 修改導(dǎo)入包
// ? 原來(lái)的導(dǎo)入(Hystrix 時(shí)代) import feign.hystrix.FallbackFactory; // ? 現(xiàn)在的導(dǎo)入(Resilience4j 時(shí)代) import org.springframework.cloud.openfeign.FallbackFactory;
2. 實(shí)現(xiàn) FallbackFactory
@Slf4j
@Component
public class ControlVehicleFeignServiceFallbackFactory
implements FallbackFactory<ControlVehicleFeignService> {
@Override
public ControlVehicleFeignService create(Throwable cause) {
log.error("---控車接口回退異常:{}", cause);
return new ControlVehicleFeignService() {
@Override
public BaseResponseVo<Long> executeInstruct(UserControlVehicleDTO dto) {
return BaseResponseVo.fail(PROTOCOL_CONTROL_ERROR);
}
@Override
public BaseResponseVo<Long> selectCurrentRideUserIdByVin(String vin) {
return BaseResponseVo.fail(PROTOCOL_CONTROL_ERROR);
}
// ... 其他方法都返回 fallback 結(jié)果
};
}
}
3. 使用 FallbackFactory
@FeignClient(
name = "soa-protocol-control",
contextId = "userControlVehicle",
fallbackFactory = ControlVehicleFeignServiceFallbackFactory.class // 注意是 fallbackFactory
)
public interface ControlVehicleFeignService {
@PostMapping("/protocol-control/user/control/vehicle")
BaseResponseVo<Long> executeInstruct(@RequestBody UserControlVehicleDTO dto);
@GetMapping("/protocol-control/current/ride-user-id")
BaseResponseVo<Long> selectCurrentRideUserIdByVin(@RequestParam String vin);
// ... 其他接口
}
教訓(xùn):FallbackFactory 的包路徑變了,別用錯(cuò)導(dǎo)入!
坑 3:Ribbon 也不推薦用了?
我以為禁用就完事了
升級(jí)后發(fā)現(xiàn) Ribbon 也停更了,啟動(dòng)時(shí)各種警告:
WARN: Ribbon is now in maintenance mode.
Consider using Spring Cloud LoadBalancer instead.
這又是啥?
原因分析
后來(lái)才明白:
- Ribbon 停更:Netflix Ribbon 也進(jìn)入維護(hù)模式
- 官方推薦:Spring Cloud LoadBalancer 作為替代
- 配置變更:LoadBalancer 的配置方式和 Ribbon 不一樣
正確做法
1. 添加依賴
<!-- Spring Cloud LoadBalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>2. 配置 LoadBalancer
# application.yml
spring:
cloud:
loadbalancer:
ribbon:
enabled: false # 禁用 Ribbon
cache:
ttl: 30000 # 緩存過(guò)期時(shí)間(毫秒)
capacity: 256 # 緩存容量3. 自定義負(fù)載均衡策略
@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
LoadBalancerClientFactory clientFactory) {
return new RandomLoadBalancer(
clientFactory.getLazyProvider("service-name", ServiceInstanceListSupplier.class),
"service-name"
);
}
}
4. 使用 LoadBalancer
@Service
public class MyService {
@Autowired
private RestTemplate restTemplate;
public String callService(String data) {
// 使用服務(wù)名調(diào)用,LoadBalancer 會(huì)自動(dòng)做負(fù)載均衡
String url = "http://service-name/api/" + data;
return restTemplate.getForObject(url, String.class);
}
}
教訓(xùn):Ribbon 已經(jīng)停更了,趕緊換成 LoadBalancer!
配置對(duì)比(Hoxton vs 2021.x)
Hoxton 配置(自動(dòng)配置)
// 啥都不用配,Hystrix 自動(dòng)幫你搞定
@EnableHystrix
@EnableFeignClients
@SpringBootApplication
public class Application {
// ...
}
好處:簡(jiǎn)單,省事
壞處:
- Hystrix 停更了
- 性能不如 Resilience4j
- 配置不靈活
2021.x 配置(手動(dòng)配置)
// 啟用 Resilience4j + OpenFeign + LoadBalancer
@EnableFeignClients
@SpringBootApplication
public class Application {
// ...
}
// Resilience4j 配置
@Configuration
public class Resilience4jConfig {
@Bean
public CircuitBreakerConfig circuitBreakerConfig() {
return CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(10))
.slidingWindowSize(10)
.build();
}
}
好處:
- 官方推薦,持續(xù)更新
- 性能更好
- 配置更靈活
壞處:配置多了點(diǎn)
我的感受:
- 雖然配置多了,但心里有底
- Resilience4j 的 API 更現(xiàn)代
- 出了問題知道從哪查
- 推薦大家都這么配(雖然麻煩點(diǎn))
Fallback 和 FallbackFactory 的區(qū)別
Fallback:簡(jiǎn)單場(chǎng)景
適用場(chǎng)景:只需要返回默認(rèn)值
@FeignClient(
name = "user-service",
fallback = UserFeignClientFallback.class // 直接返回實(shí)例
)
public interface UserFeignClient {
@GetMapping("/user/{id}")
User getUser(@PathVariable Long id);
}
@Component
public class UserFeignClientFallback implements UserFeignClient {
@Override
public User getUser(Long id) {
return new User(0L, "default", "default@example.com");
}
}
好處:簡(jiǎn)單,代碼少
壞處:拿不到異常信息
FallbackFactory:復(fù)雜場(chǎng)景(推薦)
適用場(chǎng)景:需要記錄異常日志、根據(jù)異常類型做不同處理
@FeignClient(
name = "user-service",
fallbackFactory = UserFeignClientFallbackFactory.class // 工廠模式
)
public interface UserFeignClient {
@GetMapping("/user/{id}")
User getUser(@PathVariable Long id);
}
@Slf4j
@Component
public class UserFeignClientFallbackFactory
implements FallbackFactory<UserFeignClient> {
@Override
public UserFeignClient create(Throwable cause) {
log.error("Feign 調(diào)用失?。簕}", cause.getMessage()); // 可以記錄日志
return new UserFeignClient() {
@Override
public User getUser(Long id) {
// 根據(jù)異常類型做不同處理
if (cause instanceof ConnectException) {
return new User(0L, "network-error", "連接失敗");
} else {
return new User(0L, "unknown-error", "未知錯(cuò)誤");
}
}
};
}
}
好處:
- 可以記錄異常日志
- 可以根據(jù)異常類型做不同處理
- 更靈活
壞處:代碼多了點(diǎn)
我的建議:
- 簡(jiǎn)單場(chǎng)景用 Fallback
- 生產(chǎn)環(huán)境推薦用 FallbackFactory(可以記錄日志)
驗(yàn)證步驟
1. 檢查依賴
# 查看項(xiàng)目依賴樹 mvn dependency:tree | Select-String "hystrix|resilience4j|ribbon|loadbalancer"
預(yù)期結(jié)果:
- 不應(yīng)該有
spring-cloud-starter-netflix-hystrix - 應(yīng)該有
spring-cloud-starter-circuitbreaker-resilience4j - 不應(yīng)該有
ribbon - 應(yīng)該有
spring-cloud-starter-loadbalancer
2. 啟動(dòng)測(cè)試
啟動(dòng)應(yīng)用,確認(rèn)沒有以下警告:
? Hystrix is not compatible with Spring Cloud 2021.x ? Ribbon is now in maintenance mode
3. 測(cè)試 Fallback
// 手動(dòng)觸發(fā)異常,測(cè)試 fallback 是否生效
@Test
public void testFallback() {
// 模擬服務(wù)不可用
String result = feignClient.callService();
assertEquals("fallback result", result);
}
我整理的一些配置
Resilience4j 推薦配置
# 這些是我實(shí)測(cè)好用的
resilience4j:
circuitbreaker:
instances:
backendA:
registerHealthIndicator: true
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 5
automaticTransitionFromOpenToHalfOpenEnabled: true
timelimiter:
timeoutDuration: 3sLoadBalancer 推薦配置
spring:
cloud:
loadbalancer:
ribbon:
enabled: false
cache:
ttl: 30000
capacity: 256監(jiān)控指標(biāo)
# Resilience4j 的監(jiān)控指標(biāo)(要加到 Prometheus 里) resilience4j.circuitbreaker.states resilience4j.circuitbreaker.failure.rate resilience4j.circuitbreaker.slow.call.rate
回滾方案
如果升級(jí)后出現(xiàn)問題,可以快速回滾:
- 恢復(fù)
pom.xml中的 Spring Boot 和 Spring Cloud 版本 - 恢復(fù) Hystrix 依賴
- 恢復(fù) Ribbon 依賴
- 恢復(fù)原來(lái)的 Fallback 實(shí)現(xiàn)
- 重新編譯部署
教訓(xùn):升級(jí)前一定要留好退路!
總結(jié)
我踩過(guò)的坑
- 坑 1:以為 Hystrix 還能用 → 啟動(dòng)報(bào)不兼容錯(cuò)誤
- 坑 2:FallbackFactory 導(dǎo)入錯(cuò)誤 → 報(bào)返回值類型錯(cuò)誤
- 坑 3:Ribbon 沒換成 LoadBalancer → 各種警告
核心要點(diǎn)
- 必須替換 Hystrix:用 Resilience4j,官方推薦
- 必須修改 FallbackFactory 導(dǎo)入:從
feign.hystrix換成org.springframework.cloud.openfeign - 必須替換 Ribbon:用 Spring Cloud LoadBalancer
- 必須測(cè)試 fallback:確保服務(wù)降級(jí)正常工作
最后說(shuō)兩句
其實(shí)也沒多難,就是配置多了點(diǎn)。
我剛開始也懵,后來(lái)一點(diǎn)點(diǎn)試,總算是搞定了。
肯定有理解不對(duì)的地方,歡迎大佬指正。
如果你也遇到類似問題,希望這篇筆記能幫到你。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Maven編譯遇到Process terminated問題(四種情況全部解決)
這篇文章主要介紹了Maven編譯遇到Process terminated問題(四種情況全部解決),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
Spring解讀@Component和@Configuration的區(qū)別以及源碼分析
通過(guò)實(shí)例分析@Component和@Configuration注解的區(qū)別,核心在于@Configuration會(huì)通過(guò)CGLIB代理確保Bean的單例,而@Component不會(huì),在Spring容器中,使用@Configuration注解的類會(huì)被CGLIB增強(qiáng),保證了即使在同一個(gè)類中多次調(diào)用@Bean方法2024-10-10
springboot Jpa多數(shù)據(jù)源(不同庫(kù))配置過(guò)程
這篇文章主要介紹了springboot Jpa多數(shù)據(jù)源(不同庫(kù))配置過(guò)程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
SpringBoot使用Spring?Cache高效處理緩存數(shù)據(jù)
Spring?Cache是一個(gè)框架,只要簡(jiǎn)單加一個(gè)注解,就能實(shí)現(xiàn)緩存功能,本文主要介紹了SpringBoot使用Spring?Cache高效處理緩存數(shù)據(jù),感興趣的可以了解一下2023-11-11
Idea 2020.2安裝MyBatis Log Plugin 不可用的解決方法
小編在使用時(shí)發(fā)現(xiàn)Idea 2020.2 MyBatis Log Plugin 收費(fèi)了,這個(gè)可以替代用,小編特此把解決方案分享到腳本之家平臺(tái)供大家參考,感興趣的朋友一起看看吧2020-11-11
詳解在java中進(jìn)行日期時(shí)間比較的4種方法
這篇文章主要介紹了詳解在java中進(jìn)行日期時(shí)間比較的4種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
El表達(dá)式使用問題javax.el.ELException:Failed to parse the expression
今天小編就為大家分享一篇關(guān)于Jsp El表達(dá)式使用問題javax.el.ELException:Failed to parse the expression的解決方式,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12
java實(shí)現(xiàn)切圖并且判斷圖片是不是純色/彩色圖片
本篇文章主要介紹了java實(shí)現(xiàn)切圖并且判斷圖片是否是純色/彩色圖片,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08

