背景介绍
A服务要调用B服务,之后的一段时间内,要对B服务的这个线程进行操作,如果A,B服务都是集群部署的情况下,我们怎么实现。以下是我一个不太成熟的方案。 (可以理解为 A服务需要多次调用到B服务的某一台实例,仅限个别的接口需要)
架构介绍
NACOS配置、注册中心,feign调用
方案1
初步想法是把相关的几个个接口通过Hash负载到同一台服务上,话不多说,直接上代码
通过NACOS配置接口:例如:/aaa/a1,/aaa/a2,/aaa/a3 要负载到同一台服务,/bbb/b1:/bbb/b2:/bbb/b3 要负载到同一台服务
feign.loadPolicy.hash.urls=/aaa/a1:/aaa/a2:/aaa/a3;/bbb/b1:/bbb/b2:/bbb/b3
在服务启动后,把这些配置存储到hashUrlsMap缓存中
key1 = /aaa/a1 value = /aaa/a1:/aaa/a2:/aaa/a3
key1 = /aaa/a2 value = /aaa/a1:/aaa/a2:/aaa/a3
key1 = /aaa/a3 value = /aaa/a1:/aaa/a2:/aaa/a3
当请求被FeignClientInterceptor拦截时,获取相同的value值,取hash,通过hash请求相同的服务
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class FeignClientInterceptor implements RequestInterceptor {
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static Map<String, String> hashUrlsMap = new HashMap<>();
@Value("${feign.loadPolicy.hash.urls:}")
private String hashUrls;
@PostConstruct
public void initHashUrlsMap() {
if (StringUtils.isEmpty(hashUrls)) {
log.info("==========未配置 feign.loadPolicy.hash.urls===========");
return;
}
String[] pairs = hashUrls.split(",");
for (String pair : pairs) {
if(!pair.contains(":")){
continue;
}
String[] keyValue = pair.split(":");
for (int i = 0; i < keyValue.length; i++) {
hashUrlsMap.put(keyValue[i], pair);
}
}
log.info("==========初始化initHashUrlsMap===========");
for (String key : hashUrlsMap.keySet()) {
String value = hashUrlsMap.get(key);
log.info("============Key: " + key + ", Value: " + value);
}
}
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("webToken", getWebToken());
String url = (requestTemplate.feignTarget().url() + requestTemplate.url()).replace("http://", "");
String thisUrl = url.substring(url.indexOf("/"));
if (!CollectionUtils.isEmpty(hashUrlsMap) && StringUtils.isNotEmpty(hashUrlsMap.get(thisUrl))) {
String item = hashUrlsMap.get(thisUrl);
threadLocal.set(Math.abs(item.hashCode()));
}
}
public static String getWebToken() {
HttpServletRequest request = getRequest();
if(null != request){
return getRequest().getHeader("webToken");
}
return null;
}
public static HttpServletRequest getRequest() {
if(RequestContextHolder.getRequestAttributes() != null) {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
} else {
return null;
}
}
}
自定义MyHashRule 继承 ZoneAvoidanceRule,如果请求不需要hash负载则调用默认的负载规则
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j
public class MyHashRule extends ZoneAvoidanceRule {
@Override
public Server choose(Object key) {
if (FeignClientInterceptor.threadLocal.get() != null) {
try {
ILoadBalancer lb = getLoadBalancer();
List<Server> servers = getPredicate().getEligibleServers(lb.getAllServers(), key);
if (servers.size() == 0) {
return null;
}
return servers.get(FeignClientInterceptor.threadLocal.get() % servers.size());
} finally {
FeignClientInterceptor.threadLocal.remove();
}
}
return super.choose(key);
}
}
就这样,第一版就顺利完成了,直到有一天.....
突然有人提问,服务扩容,缩容了怎么办
...
...
...
给了我当头一棒,这一棒,敲到了我本就格格不入的发际线,干!!!
我需要给这些服务排序,我怎么排序,怎么搞,只能夜以继日 粪发涂墙。。。
然后方案2就诞生了
方案2
思路还是那个思路,我在注册的时候,主动给注册中心加上注册时间,然后用注册时间排序不就行了
上代码:添加注册逻辑
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.registry.NacosServiceRegistry;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import java.util.Map;
@Primary
@Component
public class RegistrationTimeServiceRegistry extends NacosServiceRegistry {
public RegistrationTimeServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {
super(nacosDiscoveryProperties);
}
@Override
public void register(Registration registration) {
Map<String, String> metadata = registration.getMetadata();
metadata.put("registration-time", String.valueOf(System.currentTimeMillis()));
super.register(registration);
}
}
其实在这里 hashUrlsMap 的作用就是过滤一下需要被我负载的url了,没什么大用了,大家自行优化
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class FeignClientInterceptor implements RequestInterceptor {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static Map<String, String> hashUrlsMap = new HashMap<>();
@Value("${feign.loadPolicy.hash.urls:}")
private String hashUrls;
@PostConstruct
public void initHashUrlsMap() {
if (StringUtils.isEmpty(hashUrls)) {
log.info("==========未配置 feign.loadPolicy.hash.urls===========");
return;
}
String[] pairs = hashUrls.split(",");
for (String pair : pairs) {
if(!pair.contains(":")){
continue;
}
String[] keyValue = pair.split(":");
for (int i = 0; i < keyValue.length; i++) {
hashUrlsMap.put(keyValue[i], pair);
}
}
log.info("==========初始化initHashUrlsMap===========");
for (String key : hashUrlsMap.keySet()) {
String value = hashUrlsMap.get(key);
log.info("============Key: " + key + ", Value: " + value);
}
}
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("webToken", getWebToken());
String url = (requestTemplate.feignTarget().url() + requestTemplate.url()).replace("http://", "");
String thisUrl = url.substring(url.indexOf("/"));
if (!CollectionUtils.isEmpty(hashUrlsMap) && StringUtils.isNotEmpty(hashUrlsMap.get(thisUrl))) {
threadLocal.set(requestTemplate.feignTarget().name());
}
}
public static String getWebToken() {
HttpServletRequest request = getRequest();
if(null != request){
return getRequest().getHeader("webToken");
}
return null;
}
public static HttpServletRequest getRequest() {
if(RequestContextHolder.getRequestAttributes() != null) {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
} else {
return null;
}
}
}
通过MyHashRule 获取注册时间最早的服务进行负载,如果它已经挂了,线程已不在、无需处理了
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class MyHashRule extends ZoneAvoidanceRule {
@Autowired
private DiscoveryClient discoveryClient;
private static ConcurrentHashMap<String, Server> map = new ConcurrentHashMap<>();
@Override
public Server choose(Object key) {
String apiName = FeignClientInterceptor.threadLocal.get();
if (apiName != null) {
try {
return chooseServer(apiName);
} finally {
FeignClientInterceptor.threadLocal.remove();
}
}
return super.choose(key);
}
private Server chooseServer(String serviceName) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
if (instances.isEmpty()) {
return null;
}
ServiceInstance earliestInstance = instances.stream()
.filter(instance -> instance.getMetadata().containsKey("registration-time") &&
!instance.getMetadata().get("registration-time").isEmpty())
.min(Comparator.comparingLong(instance ->
Long.parseLong(instance.getMetadata().get("registration-time"))))
.orElse(null);
if(earliestInstance == null){
return null;
}
return new Server(earliestInstance.getHost(), earliestInstance.getPort());
}
}
这只是我一个不成熟的想法,各位辣鸡有什么意见尽管提 不说了 去植发了