基础概念 sidecar: 微服务异构,就是指可以让其他第三方(语言)服务,接入springcloud(nacos)里面进行管理等
框架源码:alibaba/spring-cloud-alibaba:Sidecar
需求
需要接入第三方服务,第三方服务以接口方式提供
第三方服务可以被其他第三方服务替换
第三方服务可能不支持集群部署,就是部署多个相同的实例,数据不共享
需要支持集群部署
需要监控第三方服务
集成到alibaba springcloud框架
接入方式feign
设计 项目框架采用边车模式(sidecar),但是不集成alibaba-sidecar
,手动进行实现,因为需要支持多同类型第三方服务,需要对数据进行包装,
备选方案:集成alibaba-sidecar
,因为异构只能直接代理,因此数据的包装可以采用过滤器和解码器进行处理
支持同类型第三方服务扩展替换 采用工厂设计模式进行搭建工程
支持集群部署 采用边车系统部署模式,一个第三方服务一个该服务
支持第三方服务监控 采用重写心跳,在心跳里面对第三方服务进行监控并绑定为自己的服务状态。
测试发现心跳是down的状态不熔断,只是降级。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Component public class SidecarHealthIndicator extends AbstractHealthIndicator { @Autowired AiConfig aiConfig; @Override protected void doHealthCheck (Health.Builder builder) throws Exception { try { String result; if (aiConfig.aiFaceType.equals(FaceType.NT.name())) { result = HttpUtil.get(aiConfig.aiFaceUrl + "/version" , aiConfig.aiFaceUrlTimeout); builder.withDetail("version" , result); } else if (aiConfig.aiFaceType.equals(FaceType.KS.name())) { result = HttpUtil.get(aiConfig.aiFaceUrl + "/version" , aiConfig.aiFaceUrlTimeout); JSONObject r = JSONUtil.parseObj(result); builder.withDetail("version" , r.getStr("platform_version" )); } else { result = HttpUtil.get(aiConfig.aiFaceUrl + "/version" , aiConfig.aiFaceUrlTimeout); builder.withDetail("version" , result); } builder.up(); } catch (Exception e) { builder.down(e); } } }
第三方服务不支持集群,数据不共享(不考虑异常情况) 方案1: 在业务包装接口里面实现向其他实例进行数据同步 在数据存储类型的接口里面查询该服务的其他实例,然后发同样的数据到该服务的其他实例。
注意事项:由于该服务也部署了复数个实例,因此估计需要采用redis等中间件实现那些服务已经发送过,不然会形成服务间的死循环
方案2: 利用feign的重试机制 在接口里面返回指定错误码,然后根据错误码进行重试,然后计数重试次数(可采用redis进行计数),当重试次数达到了实例的个数,就说明每个实例都请求了一次了,数据都存在于每个实例了。
缺点:如果10个实例,每个实例处理请求时间2s,10个就需要20s,因为是按顺序进行请求的
方案3: 利用feign拦截器异步请求其他实例(目前采用) 可以在拦截器里面设置header标志,标志其他服务不需要拦截,向其他服务请求,不然也会形成服务间的死循环
拦截器两种实现方式
在feign指定配置类@FeignClient(...,configuration = MyConfiguration.class)
实现1⃣️feign.RequestInterceptor/2⃣️HandlerInterceptor/3⃣️ClientHttpRequestInterceptor
接口,进行全局拦截
这里采用接口拦截模式,配置模式会在其他项目里面引入
拦截器用2⃣️HandlerInterceptor
,因为1⃣️feign.RequestInterceptor
不知道为什么拦截不生效
具体实现见附录一:spring HandlerInterceptor器的实现并读取body
步骤:
继承HttpServletRequestWrapper
实现一个读取并保存requestBody的类BodyReaderHttpServletRequestWrapper.java
新建一个过滤器BodyReadFilter.java
用于调用BodyReaderHttpServletRequestWrapper
进行保存body
新建一个拦截器StatefulFeignInterceptor.java
实现HandlerInterceptor
中的preHandle
新建一个配置StatefulConfig.java
用于启用拦截器StatefulFeignInterceptor
注意:如果要在拦截器里面使用@Autowired
功能,就必须使用bean注入该类,不能用注解@Component
等进行注入
向其他服务发送请求的逻辑,在StatefulFeignInterceptor
里面的preHandle
进行实现就可以了,代码如下
sub的作用时为了防止死循环,子服务不进行转发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 if ("true" .equals(request.getHeader("sub" ))) { log.info("sub request " + request.getRequestURI()); } else { ThreadUtil.execAsync(() -> { String uri = request.getRequestURI(); log.info("main request " + uri); List<String> urls = aiConfig.aiFaceStatefulUrls; if (urls.contains(uri)) { BodyReaderHttpServletRequestWrapper requestWrapper = null ; try { requestWrapper = new BodyReaderHttpServletRequestWrapper (request); } catch (IOException e) { log.error("read body error: {}" , e.getMessage()); } String body = IoUtil.read(requestWrapper.getInputStream(), requestWrapper.getCharacterEncoding()); log.debug("请求体:{}" , body); String ip = discoveryProperties.getIp(); List<ServiceInstance> instanceList = discoveryClient.getInstances("xkiot-ai" ); for (ServiceInstance serviceInstance : instanceList) { if (!ip.equals(serviceInstance.getHost())) { String url = serviceInstance.getUri().toString() + uri; HttpRequest.post(url).header("sub" , "true" ).body(body).execute(true ).body(); } } } }); } return true ;
注意事项:如果服务里面需要创建一个用户id,然后每台服务的用户id要一致,只能通过接口传入用户id,或者把用户id共享到redis内存里面(比较麻烦)
方案4: 利用feign解码器异步请求其他实例 解码器是对请求结果进行处理,因此如果使用该模式,估计需要用中间件redis来解决服务间的死循环
方案5: 幻想方案,在某个地方设置或重写,可以让feign支持向所有实例发送请求 方案6: 幻想方案,利用事务或异步请求合并处理结果,该模式可以解决异常情况 方案7: 解决第三方有状态服务的部署,第三方服务实现数据共享 附录一:spring HandlerInterceptor器的实现并读取body BodyReaderHttpServletRequestWrapper.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import org.springframework.util.StreamUtils;import javax.servlet.ReadListener;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletRequestWrapper;import java.io.BufferedReader;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.InputStreamReader;public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper { private byte [] requestBody = null ; public BodyReaderHttpServletRequestWrapper (HttpServletRequest request) throws IOException { super (request); requestBody = StreamUtils.copyToByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream () { final ByteArrayInputStream bodyStream = new ByteArrayInputStream (requestBody); return new ServletInputStream () { @Override public int read () { return bodyStream.read(); } @Override public boolean isFinished () { return false ; } @Override public boolean isReady () { return false ; } @Override public void setReadListener (ReadListener readListener) { } }; } @Override public BufferedReader getReader () { return new BufferedReader (new InputStreamReader (getInputStream())); } }
BodyReadFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import org.springframework.stereotype.Component;import javax.servlet.*;import javax.servlet.annotation.WebFilter;import javax.servlet.http.HttpServletRequest;import java.io.IOException;@Component @WebFilter(urlPatterns = "/**", filterName = "BodyReadFilter") public class BodyReadFilter implements Filter { @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ServletRequest requestWrapper = null ; if (servletRequest instanceof HttpServletRequest) { requestWrapper = new BodyReaderHttpServletRequestWrapper ((HttpServletRequest) servletRequest); } if (requestWrapper == null ) { filterChain.doFilter(servletRequest, servletResponse); } else { filterChain.doFilter(requestWrapper, servletResponse); } } }
StatefulFeignInterceptor.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import cn.hutool.core.io.IoUtil;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@Slf4j public class StatefulFeignInterceptor implements HandlerInterceptor { @Autowired AiConfig aiConfig; @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (aiConfig.aiFaceStatefulUrls.contains(request.getRequestURI())) { BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper (request); String body = IoUtil.read(requestWrapper.getInputStream(), requestWrapper.getCharacterEncoding()); log.debug("请求体:{}" , body); } return true ; } }
StatefulConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public class StatefulConfig implements WebMvcConfigurer { @Bean public StatefulFeignInterceptor statefulFeignInterceptor () { return new StatefulFeignInterceptor (); } @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(statefulFeignInterceptor()).addPathPatterns("/**" ); } }
额外 Nacos 的cp/ap模式
AP模式(nacos默认模式)不支持数据一致性,所以只支持服务注册的临时实例
CP模式支持服务注册的永久实例,满足数据的一致性
这里的数据一致性,让我一度认为是指服务的所有实例数据一致,让我以为可以设置过后,每个实例都会发请求
参考 SpringBoot常用拦截器(HandlerInterceptor,ClientHttpRequestInterceptor,RequestInterceptor)