SpringCloud自定義loadbalancer實(shí)現(xiàn)標(biāo)簽路由的詳細(xì)方案
一、背景
最近前端反應(yīng)開發(fā)環(huán)境有時(shí)候調(diào)接口會(huì)很慢,原因是有開發(fā)圖方便將本地服務(wù)注冊(cè)到開發(fā)環(huán)境,請(qǐng)求路由到開發(fā)本地導(dǎo)致,
為了解決該問題想到可以通過標(biāo)簽路由的方式避免該問題,實(shí)現(xiàn)前端聯(lián)調(diào)和開發(fā)自測(cè)互不干擾。
該方案除了用于本地調(diào)試,還可以用于用戶灰度發(fā)布。
二、實(shí)現(xiàn)方案
關(guān)于負(fù)載均衡,低版本的SpringCloud用的是Spring Cloud Ribbon,高版本用Spring Cloud LoadBalancer替代了,
Ribbon可以通過實(shí)現(xiàn)IRlue接口實(shí)現(xiàn),這里只介紹高版本的實(shí)現(xiàn)方案。
實(shí)現(xiàn)方案:
idea在環(huán)境變量中設(shè)置tag,本地服務(wù)啟動(dòng)時(shí)讀取環(huán)境變量將tag注冊(cè)到nacos的元數(shù)據(jù)
重寫網(wǎng)關(guān)的負(fù)載均衡算法,從請(qǐng)求頭中獲取到的request-tag和服務(wù)實(shí)例的元數(shù)據(jù)進(jìn)行匹配,如果匹配到則返回對(duì)應(yīng)的
服務(wù)實(shí)例,否則提示服務(wù)未找到。
三、編碼實(shí)現(xiàn)
3.1 order服務(wù)
新建一個(gè)SpringCloud服務(wù)order-service,注冊(cè)元數(shù)據(jù)很簡(jiǎn)單,只需要排除掉NacosDiscoveryClientConfiguration,再寫一個(gè)自己的NacosDiscoveryClientConfiguration配置類即可。
創(chuàng)建MyNacosDiscoveryClientConfiguration
/** * @Author: Ship * @Description: * @Date: Created in 2025/2/12 */ @Configuration( proxyBeanMethods = false ) @ConditionalOnDiscoveryEnabled @ConditionalOnBlockingDiscoveryEnabled @ConditionalOnNacosDiscoveryEnabled @AutoConfigureBefore({SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class}) @AutoConfigureAfter({NacosDiscoveryAutoConfiguration.class}) public class MyNacosDiscoveryClientConfiguration { @Bean public DiscoveryClient nacosDiscoveryClient(NacosServiceDiscovery nacosServiceDiscovery) { return new NacosDiscoveryClient(nacosServiceDiscovery); } @Bean @ConditionalOnProperty( value = {"spring.cloud.nacos.discovery.watch.enabled"}, matchIfMissing = true ) public NacosWatch nacosWatch(NacosServiceManager nacosServiceManager, NacosDiscoveryProperties nacosDiscoveryProperties, ObjectProvider<ThreadPoolTaskScheduler> taskExecutorObjectProvider, Environment environment) { // 環(huán)境變量讀取標(biāo)簽 String tag = environment.getProperty("tag"); nacosDiscoveryProperties.getMetadata().put("request-tag", tag); return new NacosWatch(nacosServiceManager, nacosDiscoveryProperties, taskExecutorObjectProvider); } }
這里代碼基本與NacosDiscoveryClientConfiguration一致,只是加上了設(shè)置元數(shù)據(jù)的邏輯。
@SpringBootApplication(exclude = NacosDiscoveryClientConfiguration.class) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
啟動(dòng)類上需要排除默認(rèn)的NacosDiscoveryClientConfiguration,不然啟動(dòng)會(huì)報(bào)bean重復(fù)注冊(cè)的錯(cuò)誤,或者配置添加spring.main.allow-bean-definition-overriding=true允許重復(fù)注冊(cè)也行。
寫一個(gè)測(cè)試接口,方便后面測(cè)試
/** * @Author: Ship * @Description: * @Date: Created in 2025/2/12 */ @RequestMapping("test") @RestController public class TestController { @GetMapping("") public String sayHello(){ return "hello"; } }
3.2 gateway服務(wù)
新建一個(gè)網(wǎng)關(guān)服務(wù),pom文件如下:
<properties> <java.version>1.8</java.version> <spring-cloud.version>2020.0.3</spring-cloud.version> <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version> <spring-boot.version>2.5.1</spring-boot.version> <maven-compiler-plugin.version>3.1</maven-compiler-plugin.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>${spring-cloud-alibaba.version}</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>${spring-cloud-alibaba.version}</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-compiler-plugin.version}</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
Spring-Cloud-loadBalancer默認(rèn)使用輪詢的算法,即org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer類實(shí)現(xiàn),因此可以參考RoundRobinLoadBalancer實(shí)現(xiàn)一個(gè)TagLoadBalancer,代碼如下:
/** * @Author: Ship * @Description: * @Date: Created in 2025/2/12 */ public class TagLoadBalancer implements ReactorServiceInstanceLoadBalancer { private static final String TAG_HEADER = "request-tag"; private static final Log log = LogFactory.getLog(TagLoadBalancer.class); final AtomicInteger position; final String serviceId; ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; public TagLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) { this(serviceInstanceListSupplierProvider, serviceId, (new Random()).nextInt(1000)); } public TagLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, int seedPosition) { this.serviceId = serviceId; this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; this.position = new AtomicInteger(seedPosition); } @Override public Mono<Response<ServiceInstance>> choose(Request request) { ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier) this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new); return supplier.get(request).next().map((serviceInstances) -> { return this.processInstanceResponse(supplier, serviceInstances, request); }); } private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances, Request request) { Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances, request); if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) { ((SelectedInstanceCallback) supplier).selectedServiceInstance((ServiceInstance) serviceInstanceResponse.getServer()); } return serviceInstanceResponse; } private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) { if (instances.isEmpty()) { if (log.isWarnEnabled()) { log.warn("No servers available for service: " + this.serviceId); } return new EmptyResponse(); } if (request instanceof DefaultRequest) { DefaultRequest<RequestDataContext> defaultRequest = (DefaultRequest) request; // 上下文獲取請(qǐng)求頭 HttpHeaders headers = defaultRequest.getContext().getClientRequest().getHeaders(); List<String> list = headers.get(TAG_HEADER); if (!CollectionUtils.isEmpty(list)) { String requestTag = list.get(0); for (ServiceInstance instance : instances) { String str = instance.getMetadata().getOrDefault(TAG_HEADER, ""); if (requestTag.equals(str)) { return new DefaultResponse(instance); } } log.error(String.format("No servers available for service:%s,tag:%s ", this.serviceId, requestTag)); return new EmptyResponse(); } } int pos = Math.abs(this.position.incrementAndGet()); ServiceInstance instance = instances.get(pos % instances.size()); return new DefaultResponse(instance); } }
這里需要實(shí)現(xiàn)ReactorServiceInstanceLoadBalancer接口,如果請(qǐng)求頭帶有標(biāo)簽則根據(jù)標(biāo)簽路由,否則使用默認(rèn)的輪詢算法。
還要把TagLoadBalancer用起來,所以需要定義一個(gè)配置類TagLoadBalancerConfig,并通過@LoadBalancerClients注解添加默認(rèn)配置,代碼如下:
/** * @Author: Ship * @Description: * @Date: Created in 2025/2/12 */ public class TagLoadBalancerConfig { @Bean public ReactorLoadBalancer reactorTagLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new TagLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name); } } @LoadBalancerClients(defaultConfiguration = {TagLoadBalancerConfig.class}) @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
最后在application.yml文件添加網(wǎng)關(guān)路由配置
spring: application: name: gateway cloud: nacos: config: server-addr: 127.0.0.1:8848 namespace: dev group: DEFAULT_GROUP discovery: server-addr: 127.0.0.1:8848 namespace: dev gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/order/** filters: - StripPrefix=1 server: port: 9000
3.3 代碼測(cè)試
本地啟動(dòng)nacos后啟動(dòng)order(注意需要在idea設(shè)置環(huán)境變量tag=ship)和gateway服務(wù),可以看到order服務(wù)已經(jīng)成功注冊(cè)了元數(shù)據(jù)
然后用Postman請(qǐng)求網(wǎng)關(guān)http://localhost:9000/order/test
可以看到請(qǐng)求成功路由到了order服務(wù),說明根據(jù)tag路由成功了。
去掉環(huán)境變量tag后重新啟動(dòng)Order服務(wù),再次請(qǐng)求響應(yīng)報(bào)文如下:
{
"timestamp": "2025-02-14T12:10:44.294+00:00",
"path": "/order/test",
"status": 503,
"error": "Service Unavailable",
"requestId": "41651188-4"
}
說明根據(jù)requst-tag找不到對(duì)應(yīng)的服務(wù)實(shí)例,代碼邏輯生效了。
四、總結(jié)
聰明的人已經(jīng)發(fā)現(xiàn)了,本文只實(shí)現(xiàn)了網(wǎng)關(guān)路由到下游服務(wù)這部分的標(biāo)簽路由,下游服務(wù)A調(diào)服務(wù)B的標(biāo)簽路由并未實(shí)現(xiàn),其實(shí)現(xiàn)方案也不難,只需要通過上下文傳遞+feign攔截器就可以做到全鏈路的標(biāo)簽路由,有興趣的可以自己試試。
本文代碼已上傳github,順便推廣下前段時(shí)間寫的idea插件CodeFaster(快速生成常用流操作的代碼,Marketplace搜索下載即可體驗(yàn))??。
到此這篇關(guān)于SpringCloud自定義loadbalancer實(shí)現(xiàn)標(biāo)簽路由的詳細(xì)方案的文章就介紹到這了,更多相關(guān)SpringCloud標(biāo)簽路由內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring boot項(xiàng)目集成Camel FTP的方法示例
這篇文章主要介紹了Spring boot項(xiàng)目集成Camel FTP的方法示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-10-10springboot+vue實(shí)現(xiàn)oss文件存儲(chǔ)的示例代碼
對(duì)象存儲(chǔ)服務(wù)是一種海量、安全、低成本、高可靠的云存儲(chǔ)服務(wù),本文主要介紹了springboot+vue實(shí)現(xiàn)oss文件存儲(chǔ)的示例代碼,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02spring security國(guó)際化及UserCache的配置和使用
這篇文章主要介紹下國(guó)際化的配置及UserCache的配置及使用教程,感興趣的朋友參考下實(shí)現(xiàn)代碼吧2017-09-09詳細(xì)談?wù)凧ava中l(wèi)ong和double的原子性
原子性是指一個(gè)操作或多個(gè)操作要么全部執(zhí)行,且執(zhí)行的過程不會(huì)被任何因素打斷,要么就都不執(zhí)行,下面這篇文章主要給大家介紹了關(guān)于Java中l(wèi)ong和double原子性的相關(guān)資料,需要的朋友可以參考下2021-08-08百度翻譯API使用詳細(xì)教程(前端vue+后端springboot)
這篇文章主要給大家介紹了關(guān)于百度翻譯API使用的相關(guān)資料,百度翻譯API是百度面向開發(fā)者推出的免費(fèi)翻譯服務(wù)開放接口,任何第三方應(yīng)用或網(wǎng)站都可以通過使用百度翻譯API為用戶提供實(shí)時(shí)優(yōu)質(zhì)的多語言翻譯服務(wù),需要的朋友可以參考下2024-02-02Java中Stringbuild,Date和Calendar類的用法詳解
這篇文章主要為大家詳細(xì)介紹了Java中Stringbuild、Date和Calendar類的用法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-04-04java教學(xué)筆記之對(duì)象的創(chuàng)建與銷毀
面向?qū)ο蟮木幊陶Z言使程序能夠直觀的反應(yīng)客觀世界的本來面目,并且使軟件開發(fā)人員能夠運(yùn)用人類認(rèn)識(shí)事物所采用的一般思維方法進(jìn)行軟件開發(fā),是當(dāng)今計(jì)算機(jī)領(lǐng)域中軟件開發(fā)和應(yīng)用的主流技術(shù)。2016-01-01Java實(shí)時(shí)監(jiān)控日志文件并輸出的方法詳解
這篇文章主要給大家介紹了關(guān)于Java實(shí)時(shí)監(jiān)控日志文件并輸出的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2017-06-06