前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >搭建微服务系统选型和问题记录

搭建微服务系统选型和问题记录

作者头像
简熵
发布2023-03-06 21:47:07
4010
发布2023-03-06 21:47:07
举报
文章被收录于专栏:逆熵逆熵

1.注册中心

选型eureka双节点。AP模型,因为目前已有的项目使用的是eureka+apollo方案。nacos集成配置中心+注册中心双双功能确实很强大,为了快速开发,再加上团队对springcloud整套框架的熟悉度,本次优先使用eureka作为注册中心,简单易用。基于k8s部署。

1.1 maven依赖

代码语言:javascript
复制
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

1.2 关键配置

代码语言:javascript
复制
#关闭自我保护机制,Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server会将当前的实例注册信息保护起来,同时提示这个警告。保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。
#Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。
eureka.server.enable-self-preservation=false

#指示eureka 服务器从收到最后一次心跳后等待的时间(以秒为单位),默认值90,然后它可以从其视图中删除此实例,并禁止流量到此实例。
eureka.instance.lease-expiration-duration-in-seconds=300
# 以IP地址注册到服务中心,相互注册使用IP地址
eureka.instance.preferIpAddress
#读取对等服务器节点复制数据的超时时间 默认值200
eureka.server.peer-node-read-timeout-ms=60000
# 清理无效节点的时间间隔,默认60秒
eureka.server.eviction-interval-timer-in-ms=300000

# 在Spring Cloud中,服务的Instance ID的默认值是${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}} ,也就是机器主机名:应用名称:应用端口 。因此在Eureka Server首页中看到的服务的信息类似如下:itmuch:microservice-provider-user:8000
eureka.instance.instance-id: ${spring.cloud.client.ipAddress}:${server.port}        # 将Instance ID设置成IP:端口的形式
代码语言:javascript
复制
#eureka client间隔多久去拉取服务注册信息,默认为30秒,对于api-gateway,如果要迅速获取服务注册状态,可以缩小该值,比如5秒
eureka.client.registry-fetch-interval-seconds=30

#指示 eureka 客户端需要多久(以秒为单位)向 eureka 服务器发送心跳以指示它仍然存在。如果在 leaseExpirationDurationInSeconds 指定的时间内没有收到心跳,eureka 服务器将从它的视图中删除该实例,从而禁止流量到该实例。,如果该instance实现了HealthCheckCallback,并决定让自己unavailable的话,则该instance也不会接收到流量。
eureka.instance.lease-renewal-interval-in-seconds=30

2.声明式Http服务调用

选型OpenFeign。现在Springcloud alibaba 这一套很流行,我们也曾经尝试使用dubbo作为rpc进行各个微服务之间的调用。很丝滑,不过我们暂时需要这种基于socket连接的RPC,性能强劲,使用声明式Http服务,基于Springmvc注解进行开发,团队成员也很熟练,开箱即用,所以本次选用OpenFeign

2.1 maven依赖

代码语言:javascript
复制
<!--   classpath中存在client依赖,会自动注册 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2.2 启用Feign

代码语言:javascript
复制
@SpringBootApplication
@RestController
@EnableFeignClients
public class Application {

    @RequestMapping("/")
    public String home() {
        return "Hello world";
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
2.2.1 contextId属性的作用

@FeignClient注解中有个属性contextId,平时我们项目中只使用了name属性或者value属性,他们作用都是一样的,标识服务提供者的名称。

如果我们使用Feign定义了两个接口,但是目标服务是同一个的时候:

代码语言:javascript
复制
@FeignClient(value = "ORDER-SERVICE")
@RequestMapping("/order")
public interface OrderApi2 {

    @GetMapping("/orderDetail/{id}")
    String getOrderDetail(@PathVariable("id") int id);

}

@FeignClient(value = "ORDER-SERVICE")
@RequestMapping("/order")
public interface OrderApi {

    @GetMapping("/get")
    String getOrder(@RequestParam("id") int id);
}

此时项目会报错:

代码语言:javascript
复制
***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'ORDER-SERVICE.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

产生的原因是:

代码语言:javascript
复制
Map<String, Object> attributes = annotationMetadata
 .getAnnotationAttributes(FeignClient.class.getCanonicalName());

String name = getClientName(attributes);
registerClientConfiguration(registry, name,
                         attributes.get("configuration"));
//  getClientName具体实现
private String getClientName(Map<String, Object> client) {
 if (client == null) {
     return null;
 }
 String value = (String) client.get("contextId");
 if (!StringUtils.hasText(value)) {
     value = (String) client.get("value");
 }
 //省略部分代码
}

已上代码是对每一个FeignClient注册流程的一段代码,其中,对于每一个FeignClient都会创建一个{clientName}.FeignClientSpecification。就是这里因为contextId取值取不到时会获取value的值,所以这里就会重复报错。。

解决方案有2种:

  1. 根据错误提示的解决方案,设置配置属性spring.main.allow-bean-definition-overriding=true,名称相同的bean是否支持覆盖。这个属性值在spring中默认是true,在springboot中默认是false。
  2. 我们就要用到contextId了。contextId设置之后,就是clientName的名字,一旦这里区分了之后,就能解决上面的错误。 @FeignClient(value = "ORDER-SERVICE",contextId = "order1") @RequestMapping("/order") public interface OrderApi2 { } @FeignClient(value = "ORDER-SERVICE",contextId = "order2") @RequestMapping("/order") public interface OrderApi { }
2.2.2 Ambiguous mapping的解决

微服务结构下,项目中为了方便其他服务的调用,会将FeignClient接口都放到一个api jar包,方便其他项目引入。此时一般都是如下定义FeignClient

代码语言:javascript
复制
@FeignClient(value = "ORDER-SERVICE")
@RequestMapping("/order")
public interface OrderApi {

    @GetMapping("/get")
    String getOrder(@RequestParam("id") int id);
}

如果为了方便定义,在服务的controller层实现了已上接口:

代码语言:javascript
复制
@RestController
public class OrderRest implements OrderApi {
    @Override
    public String getOrder(int id) {
        return "hello world";
    }
}

那么,此时会报错:

代码语言:javascript
复制
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'cn.ev.order.api.OrderApi' method 
cn.ev.order.api.OrderApi#getOrder(int)

遇到这种错误,开发人员首先肯定是排查方法有没有重复映射,有没有重复的Mapping或者在实现的时候是否重复添加了mapping注解。但是在这里都不是。产生这种问题的根本原因是RequestMappingHandlerMapping在处理映射时,判断的条件是:存在Controller注解或RequestMapping注解,就会被判定位isHandler,而我们的FeignCLient接口中有@RequestMapping,也会进行映射注册,此时就会产生冲突。

代码语言:javascript
复制
// path:"org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java"
@Override
protected boolean isHandler(Class<?> beanType) {
    return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
            AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

解决方案有2种:

  1. 简单解决,开发者只需要注释掉//@RequestMapping("/order"),将映射的公共部分/order放置到每一个方法上 @FeignClient(value = "ORDER-SERVICE") //@RequestMapping("/order") public interface OrderApi { @GetMapping("/order/get") String getOrder(@RequestParam("id") int id); }
  2. 深入解决,解决FeignClient接口的Mapping映射问题,重写MappingHandler映射方法 @Configuration @ConditionalOnClass({Feign.class}) public class FeignMappingDefaultConfiguration { @Bean public WebMvcRegistrations feignWebRegistrations() { return new WebMvcRegistrationsAdapter() { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new FeignFilterRequestMappingHandlerMapping(); } }; } private static class FeignFilterRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override protected boolean isHandler(Class<?> beanType) { return super.isHandler(beanType) && (AnnotationUtils.findAnnotation(beanType, RestController.class) != null); } } }
2.2.3 Feign Encoder&Decoder

如果没有自定义Decoder,那么对于Feign返回会使用AsyncResponseHandler异步响应处理器进行处理。其中使用的默认DecoderSpringDecoder,并且用OptionalDecoder包装了一下。

ErrorDecoder处理器处理,非200 <=状态码 < 300或者非 【如果是404 并且配置了decode404 = true 并且返回值不是void】,其他错误都会走错误处理器。

2.2.4 自定义异常处理

项目中有2处的自定义已异常需要处理:

  1. 对常见的controller层抛出的业务异常需要做全局异常处理
  2. 基于Feign调用的服务端报错,客户端对于异常的处理。

问题1的处理:

老生常谈,使用@RestControllerAdvice做全局异常拦截,对于自定义异常可以进行自定义处理。

问题2的处理:

需要借助ErrorDecoderFeign的错误处理器处理。

3.ORM框架

选用MybatisPlus做ORM映射。可以方便地使用框架自带丰富的扩展功能。

3.1 自定义主键策略

自定义主键生成器,并且可以通过对一些注解的处理进行表级别的id个性化设置。以下主要是通过@TableName获取了表名,并且根据表名去路由获取主键初始值和自增步长从而计算nextId。我们可以从数据库获取,也可以从redis中获取。

代码语言:javascript
复制
@Component
public class CustomIdGenerator implements IdentifierGenerator  {
    @Autowired
    private DBSequenceIdGenerator generator;

    public void setGenerator(DBSequenceIdGenerator generator) {
        this.generator = generator;
    }

    @Override
    public Number nextId(Object entity) {
        TableName tableName = entity.getClass().getAnnotation(TableName.class);
        return new Long(generator.genBusinessId(tableName.value(),8));
    }
}

3.2 多数据源支持

MybatisPlus对多数据源的支持属于配置配置开箱即用了。

引入多数据依赖

代码语言:javascript
复制
<!--MybatisPlus 多数据源-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>2.5.6</version>
</dependency>

application.properties中配置多个数据源

代码语言:javascript
复制
# ds1
spring.datasource.dynamic.datasource.ds1.url=
spring.datasource.dynamic.datasource.ds1.username=
spring.datasource.dynamic.datasource.ds1.password=
spring.datasource.dynamic.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver

# ds2
spring.datasource.dynamic.datasource.ds2.url=
spring.datasource.dynamic.datasource.ds2.username=
spring.datasource.dynamic.datasource.ds2.password=
spring.datasource.dynamic.datasource.ds2.driver-class-name=com.mysql.cj.jdbc.Driver

使用@DS进行多数据源的切换,既可以在方法上使用,也可以在类上面使用。

代码语言:javascript
复制
@DS("ds2")
public interface DemoMapper extends BaseMapper<Demo> {}

@Service
@DS("slave")
public class DemoServiceImpl implements DemoService {
    @Override
    @DS("ds1")
    public List selec() {

    }
}

4.配置中心

Apollo配置中心,运维上有现成的支持,简单易用,团队成员也比较熟悉。

4.1 引入maven依赖

代码语言:javascript
复制
<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>1.8.0</version>
</dependency>

4.2 客户端接入

代码语言:javascript
复制
@EnableApolloConfig(value = {"namespace1","namespace2"})
public class Application {
    
}

通过EnableApolloConfig注解启用apollo的配置能力,配置需要监听的多个namespace下的配置信息,

4.3 刷新配置

apollo的客户端配置的更新是通过定时轮询+长轮询,相互补充从而达到“实时”刷新的效果。如果需要监听某个特定的key发生变化,用如下方式@ApolloConfigChangeListener均可以实现。

代码语言:javascript
复制
@ApolloConfigChangeListener
private void userConfigChangeHandler(ConfigChangeEvent changeEvent) {
    if (changeEvent.isChanged(KEY_USER_CACHE_REFRESH)) {
        init();
    }

}
@ApolloConfigChangeListener(interestedKeys = {KEY_USER_CACHE_REFRESH})
private void userConfigChangeHandler1(ConfigChangeEvent changeEvent) {
    init1();

}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-02-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 逆熵架构 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.注册中心
    • 1.1 maven依赖
      • 1.2 关键配置
      • 2.声明式Http服务调用
        • 2.1 maven依赖
          • 2.2 启用Feign
            • 2.2.1 contextId属性的作用
            • 2.2.2 Ambiguous mapping的解决
            • 2.2.3 Feign Encoder&Decoder
            • 2.2.4 自定义异常处理
        • 3.ORM框架
          • 3.1 自定义主键策略
            • 3.2 多数据源支持
            • 4.配置中心
              • 4.1 引入maven依赖
                • 4.2 客户端接入
                  • 4.3 刷新配置
                  相关产品与服务
                  微服务引擎 TSE
                  微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档