Dark Horse SpringCloud+RabbitMQ+Docker+Redis+Search+Distributed Study Notes

write first

Understanding Microservices

  • With the development of the Internet industry, the requirements for services are getting higher and higher, and the service architecture has gradually evolved from a single architecture to the now popular microservice architecture. So what is the difference between these structures?

monolithic architecture

  • Monolithic architecture: develop all the functions of the business in one project and package them into one package for deployment
  • The advantages and disadvantages of the monolithic architecture are as follows
    • 优点
      • simple structure
      • Low deployment cost
    • 缺点
      • High degree of coupling (difficult to maintain and upgrade)

distributed architecture

  • Distributed architecture: split the system according to business functions, each business function module is developed as an independent project, called a service
  • The advantages and disadvantages of distributed architecture are as follows
    • 优点
      • Reduce service coupling
      • Conducive to service upgrade and expansion
    • 缺点
      • Service call relationship is intricate
  • Although the distributed architecture reduces service coupling, there are still many issues to consider when splitting services
    • How to define the fine-grained level of service splitting?
    • How to call between services?
    • How to manage service invocation relationship?
  • One needs to specify a set of effective criteria for approximating distributed architectures

microservice

  • Architectural Characteristics of Microservices
    • Single Responsibility: The granularity of microservice splitting is smaller, and each service corresponds to a unique business capability to achieve a single responsibility
    • Autonomy: independent team, independent technology, independent data, independent deployment and delivery
    • Service-oriented: services provide a unified standard interface, independent of language and technology
    • Strong isolation: service calls should be isolated, fault-tolerant, and degraded to avoid cascading problems (for example, if the credit service is suspended, it cannot affect other services such as user services)
  • The above characteristics of microservices are actually setting a standard for distributed architecture, further reducing the coupling between services, and providing independence and flexibility of services. Achieve high cohesion and low coupling
  • Therefore, microservices can be considered as a distributed architecture solution with well-designed architecture
  • But how should the plan be implemented? What technology stack to choose? Internet companies around the world are actively trying their own microservice implementation solutions
  • Among them, the most eye-catching one in the field of Java is the solution provided by SpringCloud.

SpringCloud

  • SpringCloud is currently the most widely used microservice architecture in China. Official website address: https://spring.io/projects/spring-cloud
  • SpringCloud integrates various microservice functional components, and realizes the automatic assembly of these components based on SpringBoot, thus providing a good out-of-the-box experience.
  • Common components include
    • Microservice registration and discovery
      • Eureka
      • Nacos
      • Consul
    • Service remote call
      • OpenFeign
      • Dubbo
    • Service link monitoring
      • Zipkin
      • Sleuth
    • Unified configuration management
      • SpringCloudConfig
      • Nacos
    • Unified Gateway Routing
      • SpringCloudGateway
      • Zuul
    • flow control, downgrade, protection
      • Hystix
      • Sentinel
  • In addition, the bottom layer of SpringCloud depends on SpringBoot, and has a version compatibility relationship, as follows
Release Train Boot Version
2020.0.x aka llford 2.4.x
Hoxton 2.2.x,2.3.x (Starting with SR5)
Greenwich 2.1.x
Finchley 2.0.x
Edgware 1.5.x
Dalston 1.5.X
  • The learning version of this article is Hoxton.SR10, so the corresponding SpringBoot version is 2.3.x

Summarize

  • Monolithic architecture: simple and convenient, highly coupled, poor scalability, suitable for small projects. Example: Student Management System
  • Distributed architecture: loosely coupled and scalable, but complex and difficult. Suitable for large Internet projects. For example: Jingdong, Taobao
  • Microservices: A Better Approach to Distributed Architecture
    • Advantages: less splitting, more independent services, and lower coupling
    • Disadvantages: the structure is very complex, and the difficulty of operation and maintenance, monitoring and deployment is increased
  • SpringCloud is a one-stop solution for microservice architecture, integrating various excellent microservice functional components

Service splitting and remote invocation

  • Any distributed architecture is inseparable from the splitting of services, and the same is true for microservices

Principles of service splitting

  • Several principles of microservice splitting
    1. Different microservices, do not develop the same business repeatedly
    2. Microservice data is independent, do not access the database of other microservices
    3. Microservices can expose their business as interfaces for other microservices to call

Service split example

  • cloud-demo: parent project, manage dependencies

    • order-service: order microservice, responsible for order-related business
    • user-service: user microservice, responsible for user-related business
  • need

    • Order microservices and user microservices must have their own databases, independent of each other
    • Both order service and user service expose Restful interfaces to the outside world
    • If the order service needs to query user information, it can only call the Restful interface of the user service, and cannot query the user database

Import Sql statement

{% tabs order table and user table 1531 %}

CREATE DATABASE cloud_order;
USE cloud_order;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_order
-- ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `user_id` bigint(20) NOT NULL COMMENT '用户id',
  `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名称',
  `price` bigint(20) NOT NULL COMMENT '商品价格',
  `num` int(10) NULL DEFAULT 0 COMMENT '商品数量',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `username`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of tb_order
-- ----------------------------
INSERT INTO `tb_order` VALUES (101, 1, 'Apple 苹果 iPhone 12 ', 699900, 1);
INSERT INTO `tb_order` VALUES (102, 2, '雅迪 yadea 新国标电动车', 209900, 1);
INSERT INTO `tb_order` VALUES (103, 3, '骆驼(CAMEL)休闲运动鞋女', 43900, 1);
INSERT INTO `tb_order` VALUES (104, 4, '小米10 双模5G 骁龙865', 359900, 1);
INSERT INTO `tb_order` VALUES (105, 5, 'OPPO Reno3 Pro 双模5G 视频双防抖', 299900, 1);
INSERT INTO `tb_order` VALUES (106, 6, '美的(Midea) 新能效 冷静星II ', 544900, 1);
INSERT INTO `tb_order` VALUES (107, 2, '西昊/SIHOO 人体工学电脑椅子', 79900, 1);
INSERT INTO `tb_order` VALUES (108, 3, '梵班(FAMDBANN)休闲男鞋', 31900, 1);

SET FOREIGN_KEY_CHECKS = 1;
CREATE DATABASE cloud_user;
USE cloud_user;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收件人',
  `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES (1, '柳岩', '湖南省衡阳市');
INSERT INTO `tb_user` VALUES (2, '文二狗', '陕西省西安市');
INSERT INTO `tb_user` VALUES (3, '华沉鱼', '湖北省十堰市');
INSERT INTO `tb_user` VALUES (4, '张必沉', '天津市');
INSERT INTO `tb_user` VALUES (5, '郑爽爽', '辽宁省沈阳市大东区');
INSERT INTO `tb_user` VALUES (6, '范兵兵', '山东省青岛市');

SET FOREIGN_KEY_CHECKS = 1;

{% endloss %}

import demo

  • Import the good demo provided by Dark Horse, which contains order-serviceand user-service, modify the database in its configuration file to your own configuration, then start these two services, and start our call case

Realize the remote call case

  • Under the web package in order-service, there is an OrderController, which is an interface for querying orders based on id
@RestController
@RequestMapping("order")
public class OrderController {

   @Autowired
   private OrderService orderService;

    @GetMapping("{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
        // 根据id查询订单并返回
        return orderService.queryOrderById(orderId);
    }
}
  • We open the browser and visit http://localhost:8080/order/101, the data can be queried, but the user at this time is null
{
	"id": 101,
	"price": 699900,
	"name": "Apple 苹果 iPhone 12 ",
	"num": 1,
	"userId": 1,
	"user": null
}
  • Under the web package in user-service, there is also a UserController, which contains an interface for querying users based on id
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 路径: /user/110
     *
     * @param id 用户id
     * @return 用户
     */
    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return userService.queryById(id);
    }
}
  • We open the browser, visit http://localhost:8081/user/1, and the queried data is as follows
{
	"id": 1,
	"username": "柳岩",
	"address": "湖南省衡阳市"
}

case requirements

  • Modify the order query service based on id in order-service, requiring that the user information be queried according to the userId contained in the order while querying the order, and returned together
  • Therefore, we need to initiate an http request to user-service in order-service and call the interface http://localhost:8081/user/{userId}.
  • The approximate steps are as follows
    1. Register an instance of RestTemplate to the Spring container
    2. Modify the queryOrderById method in the OrderService class in the order-service service, and query the User according to the userId in the Order object
    3. Fill the queried User into the Order object and return it together

Register RestTemplate

  • First, we register the RestTemplate instance in the OrderApplication startup class in the order-service service
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

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

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

Implement remote calls

  • Modify the queryById method in the order-service service
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2. 远程查询User
        // 2.1 url地址,这里的url是写死的,后面会改进
        String url = "http://localhost:8081/user/" + order.getUserId();
        // 2.2 发起调用
        User user = restTemplate.getForObject(url, User.class);
        // 3. 存入order
        order.setUser(user);
        // 4.返回
        return order;
    }
}
  • Visit http://localhost:8080/order/101 again, this time you can see User data
{
	"id": 101,
	"price": 699900,
	"name": "Apple 苹果 iPhone 12 ",
	"num": 1,
	"userId": 1,
	"user": {
		"id": 1,
		"username": "柳岩",
		"address": "湖南省衡阳市"
	}
}

provider and consumer

  • In the service call relationship, there will be two different roles
    • 服务提供者: In a business, a service called by other microservices (providing interfaces to other microservices)
    • 服务消费者: In a business, call the services of other microservices (call the interface provided by other microservices)
  • However, the roles of service providers and service consumers are not absolute, but relative to the business
  • If service A calls service B, and service B calls service C, what is the role of service B?
    • For the business of A calling B: A is a service consumer, B is a service provider
    • For the business where B calls C: B is a service consumer, and C is a service provider
  • So service B can be both a service provider and a service consumer

Eureka Registry

  • Suppose our service provider user-service provides three instances, which occupy ports 8081, 8082, and 8083 respectively
  • Then let's think about a few questions
    • 问题一: How to know the ip address and port of the user-service instance when the order-service initiates a remote call?
    • 问题二: There are multiple user-service instance addresses, how to choose when order-service is called?
    • 问题三: How does order-service know whether a user-service instance is healthy or not?

The structure and function of Eureka

  • These problems need to be solved by using the registration center in SpringCloud. The most well-known registration center is Eureka, and its structure is as follows

  • So now to answer the previous questions

    • 问题一: How does order-service know the user-service instance address?
      • The process of obtaining address information is as follows
        1. After the user-service service instance is started, register its own information to the eureka-server (Eureka server), which is called service registration
        2. eureka-server saves the mapping relationship between service name and service instance address list
        3. order-service pulls the instance address list according to the service name, this is called service discovery or service pull
    • 问题二: How does order-service select a specific instance from multiple user-service instances?
      • order-service selects an instance address from the instance list using the load balancing algorithm
      • Initiate a remote call to the instance address
    • 问题三: How does order-service know whether a certain user-service instance is still healthy or not?
      • User-service will initiate a request to eureka-server every once in a while (default 30 seconds), report its own status, and become a heartbeat
      • When no heartbeat is sent for more than a certain period of time, eureka-server will consider the microservice instance to be faulty and remove the instance from the service list
      • When the order-service pulls the service, the fault instance can be ruled out
        {% note warning no-icon %}
        Note: A microservice can be either a service provider or a service consumer, so eureka registers the service , service discovery and other functions are uniformly encapsulated into the eureka-client
        {% endnote %}
  • So our next hands-on steps include

    1. Build a registration center
      • Build Eureka Server
    2. service registration
      • Register user-service and order-service to eureka
    3. service discovery
      • Complete the service pull in order-service, and then select a service through load balancing to realize remote calling

Build eureka-server

  • First, we register the center server: eureka, which must be an independent microservice

Create eureka-server service

  • Under the cloud-demo parent project, create a submodule, just create a maven project here, and then fill in the service information

Introduce eureka dependency

  • Introduce the starter dependency provided by SpringCloud for eureka:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

Write startup class

  • To write a startup class for the eureka-server service, be sure to add a @EnableEurekaServer annotation to enable the registration center function of eureka
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class);
    }
}

Write a configuration file

  • Write an application.yml file with the following content
  • Why do you also need to configure the service name of eureka?
    • eureka will also register itself as a service
server:
  port: 10086 # 服务端口
spring:
  application:
    name: eureka-server # eureka的服务名称
eureka:
  client:
    service-url: # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka

start service

  • Start the microservice, and then visit http://localhost:10086/ in the browser. If you see the following results, it is successful

  • From the figure, we can also see that eureka has indeed registered itself as a service, here Kyleis the host name, which is 127.0.0.1

UP (1) - Kyle:eureka-server:10086

service registration

  • Next, we register user-service to eureka-server

Introduce dependencies

  • In the pom.xml file of user-service, introduce the following eureka-client dependency
<!-- eureka-client -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

configuration file

  • In user-service, modify the application.yml file, add the service name, eureka address
spring:
  application:
    name: order-service
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

Start multiple user-service instances

  • In order to demonstrate a scenario where a service has multiple instances, we add a SpringBoot startup configuration, and then start a user-service. The operation step is to copy a user-service configuration, the name is configured as UserApplication2, and the VM option is also required. , modify the port number -Dserver.port=8082, click OK, in the service tab of IDEA, there will be two user-service startup configurations, one port is 8081, and the other port is 8082
  • After that, we configure order-service in the same way, and start two user-services and one order-service, and then check the eureka-server management page, and find that the services are indeed started, and there are two user-services

service discovery

  • Next, we modify the logic of order-service: pull user-service information from eureka-server to realize service discovery

Introduce dependencies

  • As mentioned before, service discovery and service registration are all encapsulated in eureka-client dependencies, so this step is consistent with service registration
  • In the pom.xml file of order-service, introduce eureka-client dependency
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

configuration file

  • Service discovery also needs to know the eureka address, so the second step is consistent with service registration, which is to configure eureka information
  • In order-service, modify the application.yml file, add the service name, eureka address
spring:
  application:
    name: orderservice
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

Service pull and load balancing

  • Finally, we are going to pull the instance list of user-service service from eureka-server and implement load balancing
  • However, these operations do not require us to do it, but we need to add some annotations
  • In the OrderApplication of order-service, add an @LoadBalancedannotation to the RestTemplate Bean
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

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

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
  • Modify the queryOrderById method in the OrderService class in the order-service service, modify the access path, and use the service name instead of ip and port
public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    // 2. 远程查询User
    // 2.1 url地址,用user-service替换了localhost:8081
    String url = "http://user-service/user/" + order.getUserId();
    // 2.2 发起调用
    User user = restTemplate.getForObject(url, User.class);
    // 3. 存入order
    order.setUser(user);
    // 4.返回
    return order;
}
  • Spring will automatically help us obtain the instance list from the eureka-server side according to the service name user-service, and then complete the load balancing

summary

  1. Build Eureka Server
    • Introduce eureka-server dependency
    • Add @EnableEurekaServer annotation
    • Configure the eureka address in application.yml
  2. service registration
    • Introduce eureka-client dependency
    • Configure the eureka address in application.yml
  3. service discovery
    • Introduce eureka-client dependency
    • Configure the eureka address in application.yml
    • @LoadBalancedAdd annotations to RestTemplate
    • Remote call with the service name of the service provider

Ribbon load balancing

  • In this section, let's explain how the @LoadBalanced annotation implements the load balancing function

Principle of load balancing

  • The underlying layer of Spring Cloud actually uses a component called Ribbon to implement load balancing.
  • Then the request we sent was http://userservice/user/1, how did it become http://localhost:8080/user/1?

source code tracking

  • Why can we access it only by entering the service name? You have to get ip and port before
  • The answer is obviously that someone helped us obtain the ip and port of the service instance based on the service name. It is LoadBalancerInterceptor, this class will intercept the request of the first RestTemplate, then obtain the service list from Eureka according to the service id, and then use the load balancing algorithm to obtain the real service address information and replace the service id
  • Then let's do source code tracking
  1. LoadBalancerInterceptor

    • code show as below
    public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
        private LoadBalancerClient loadBalancer;
        private LoadBalancerRequestFactory requestFactory;
    
        public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
            this.loadBalancer = loadBalancer;
            this.requestFactory = requestFactory;
        }
    
        public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
            this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
        }
    
        public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
            URI originalUri = request.getURI();
            String serviceName = originalUri.getHost();
            Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
            return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
        }
    }
    

    • You can see the intercept method here, which intercepts the user's HTTPRequest request, and then does a few things
      1. request.getURI(): Get the request uri, which is http://user-service/user/1
      2. originalUri.getHost(): Get the host name of the uri path, which is actually the service id, user-service
      3. this.loadBalancer.execute: process service id and user request
    • Here this.loadBalancer is the type of LoadBalancerClient, we continue to follow up
  2. LoadBalancerClient

    • Continue to follow the execute method
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
        ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
        Server server = this.getServer(loadBalancer, hint);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        } else {
            RibbonServer ribbonServer = new RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
            return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
        }
    }
    

    • The code is like this
      1. getLoadBalancer(serviceId): Get ILoadBalancer according to the service id, and ILoadBalancer will take the service id to eureka to get the service list and save it
      2. getServer(loadBalancer, hint): Use the built-in load balancing algorithm to select one from the service list. In this example, you can see that the obtained port is 8081
    • After the release, visit and track again, this time the port 8082 is obtained, and load balancing is indeed achieved
  3. Load balancing strategy IRule

    • In the code just now, you can see that the service is obtained through a getServer method for load balancing. We continue to follow up and find such a piece of code
    public Server chooseServer(Object key) {
        if (this.counter == null) {
            this.counter = this.createCounter();
        }
    
        this.counter.increment();
        if (this.rule == null) {
            return null;
        } else {
            try {
                return this.rule.choose(key);
            } catch (Exception var3) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", new Object[]{this.name, key, var3});
                return null;
            }
        }
    }
    
    • In the try/catch code block, the service selection is this.rule.choose(key), then let's see who this rule is
    • The default value of the rule here is a RoundRobinRule, which is polling
    • So here, the whole process of load balancing is clear to us
  4. Summarize

    • The bottom layer of SpringCloudRibbon adopts an interceptor to intercept the request sent by RestTemplate, modify the address, and use a picture to summarize
    • The whole process is as follows
      1. Intercept our RestTemplate request: http://user-service/user/1
      2. RibbonLoadBalancerClient will get the service name from the request url, which is user-service
      3. DynamicServerListLoadBalancer pulls the service list from eureka according to user-service
      4. eureka returns the list, localhost:8081, localhost:8082
      5. IRule uses built-in load balancing rules, select one from the list, such as localhost:8081
      6. RibbonLoadBalancerClient modifies the request address, replaces user-service with localhost:8081, gets http://localhost:8081/user/1, and initiates a real request

load balancing strategy

load balancing strategy

  • The rules of load balancing are defined in the IRule interface, and IRule has many different implementation classes

  • The meaning of the different rules are as follows

Built-in load balancing rule class Rule description
RoundRobinRule Simply poll the list of services to select a server. It is the default load balancing rule of Ribbon.
AvailabilityFilteringRule Ignore the following two servers: (1) By default, if this server fails to connect 3 times, this server will be set to the "short circuit" state. The short-circuit state will last for 30 seconds, and if the connection fails again, the duration of the short-circuit will increase geometrically. (2) Servers with too high concurrency. If the number of concurrent connections of a server is too high, the client configured with the AvailabilityFilteringRule rule will also ignore it. The upper limit of the number of concurrent connections can be configured by the ..ActiveConnectionsLimit property of the client.
WeightedResponseTimeRule Assign a weight value to each server. The longer the server response time, the less weight this server has. This rule will randomly select a server, and this weight value will affect the server selection.
ZoneAvoidanceRule Server selection is based on the servers available in the region. Use Zone to classify servers. This Zone can be understood as a computer room, a rack, etc. Then poll multiple services in the Zone.
BestAvailableRule Ignore servers that are short-circuited and choose servers with lower concurrency.
RandomRule Randomly select an available server.
RetryRule Selection logic for the retry mechanism
  • The default implementation is ZoneAvoidanceRule, which is a polling scheme

Custom load balancing strategy

  • By defining the implementation of IRule, you can modify the load balancing rules, there are two ways
    1. Code method: In the OrderApplication class in order-service, define an IRule. The load balancing rules defined in this way are valid for all microservices
    @Bean
    public IRule randomRule(){
        return new RandomRule();
    }
    
    1. Configuration file method: In the application.yml file in order-service, adding new configurations can also modify the rules
    user-service: # 给某个微服务配置负载均衡规则,这里是user-service服务
      ribbon:
        NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则 
    

{% note warning no-icon %}
Note: generally use no one's load balancing rules, do not modify
{% endnote %}

hunger loading

  • Ribbon uses lazy loading by default, that is, it goes back to create LoadBalanceClient when it is accessed for the first time, and the request time will be very long
  • The hungry loading will be created when the project starts, reducing the time-consuming for the first visit, and enable the hungry loading through the following configuration
ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients: user-service  # 指定对user-service这个服务进行饥饿加载,可以指定多个服务

summary

  1. Ribbon load balancing rules
    • The rule interface is IRule
    • The default implementation is ZoneAvoidanceRule, select the service list according to the zone, and then poll
  2. Custom load balancing method
    • Code method: flexible configuration, but needs to be repackaged and released when modified
    • Configuration method: Intuitive, convenient, no need to repackage and publish, but global configuration is not possible (only a certain microservice can be specified)
  3. hunger loading
    • Enable starvation loading
    enable: true
    
    • Specify the microservice name for starvation loading, you can configure multiple
    clients: 
      - user-service
      - xxx-service 
    

Nacos Registration Center

  • Domestic companies generally respect Alibaba's technology, such as the registration center, and SpringCloud Alibabaalso launched a Nacosregistration center called

Know and install Nacos

  • Nacos is a product of Alibaba, and now it is a component in SpringCloud. Compared with Eureka, it has more functions and is more popular in China.
  • On the GitHub page of Nacos, there is a download link to download the compiled Nacos server or source code:
    • GitHub homepage: https://github.com/alibaba/nacos
    • GitHub's Release download page: https://github.com/alibaba/nacos/releases
  • After downloading, unzip the file to any directory under the non-Chinese path, the directory description:
    • bin: startup script
    • conf: configuration file
  • The default port of Nacos is 8848. If other processes on your computer occupy port 8848, please try to close the process first.
    • If you cannot close the process occupying port 8848, you can also enter the conf directory of nacos and modify the server.port in the configuration file application.properties
  • The startup of Nacos is very simple, enter the bin directory, open the cmd window and execute the following command
startup.cmd -m standalone
  • Then visit http://localhost:8848/nacos in the browser, the default login account and password are nacos

Service registration to Nacos

  • Nacos is a component of SpringCloudAlibaba, and it SpringCloud Alibabaalso follows the service registration and service discovery specifications defined in SpringCloud. Therefore, there is not much difference between using Nacos and using Eureka for microservices
  • The main difference is
    1. depends on different
    2. service address is different

Introduce dependencies

  • Introduce the dependency of SpringCloudAlibaba in the pom.xml file of the cloud-demo parent project
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.6.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
  • Then introduce nacos-discovery dependencies in the pom files in user-service and order-service
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

{% note warning no-icon %}
Note: Also comment/delete eureka's dependencies
{% endnote %}

Configure Nacos address

  • Add Nacos address in application.yml of user-service and order-service
spring:
  cloud:
    nacos:
      server-addr: localhost:8848

{% note warning no-icon %}
Note: Also comment out the address of eureka
{% endnote %}

restart service

  • After restarting the microservice, log in to the management page of nacos, and you can see the microservice information

Service Hierarchical Storage Model

  • A service can have multiple instances, such as our user-service, can have
    • 127.0.0.1:8081
    • 127.0.0.1:8082
    • 127.0.0.1:8083
  • If these instances are distributed in different computer rooms across the country, for example
    • 127.0.0.1:8081, in Hangzhou computer room
    • 127.0.0.1:8082, in Hangzhou computer room
    • 127.0.0.1:8083, in Shanghai computer room
  • Nacod divides the instances in the same computer room into one集群
  • In other words, user-service is a service, and a service can contain multiple clusters. For example, in Hangzhou and Shanghai, each cluster can have multiple instances, forming a hierarchical model
  • When microservices access each other, they should access the same cluster instance as much as possible, because the local access speed is faster, and only access other clusters when the local cluster is unavailable
    • For example: the order-service in the computer room in Hangzhou should have limited access to the user-service in the same computer room. If it cannot be accessed, it should access the user-service in the computer room in Shanghai

Configure cluster for user-service

  • Modify the application.yml file of user-service and add cluster configuration
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称,杭州
  • Restart both user-service instances
  • Then we copy a user-service startup configuration, set the port number to 8083, then modify the application.yml file, set the cluster name to Shanghai, and then start the service
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: SH # 集群名称,上海
  • So now we have started two user-services with a cluster name of HZ and a user-service with a cluster name of SH, and we can see the following results in the Nacos console

  • Nacos service hierarchical storage model

    1. The first level is service, such as user-service
    2. The second level is the cluster, such as Hangzhou or Shanghai
    3. The third level is an instance, such as a server in a computer room in Hangzhou that deploys user-service
  • How to set cluster properties for an instance

    • Modify the application.yml file and add the spring.cloud.nacos.discovery.cluster-name property

Same-cluster priority load balancing

  • The default ZoneAvoidanceRule cannot achieve load balancing based on the priority of the same cluster
  • Therefore, Nacos provides an implementation of NacosRule, which can preferentially select instances from the same cluster
    1. Configure cluster information for order-service, modify its application.yml file, and configure the cluster name as HZ
    spring:
      cloud:
        nacos:
          server-addr: localhost:8848
          discovery:
            cluster-name: HZ # 集群名称,杭州
    
    1. Modify load balancing rules
    user-service: # 给某个微服务配置负载均衡规则,这里是user-service服务
      ribbon:
        NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
    
  • Then we visit http://localhost:8080/order/101 now and observe the log output of three user-services at the same time. The two user-services whose cluster name is HZ can see the log output, while the user whose cluster name is SH -service can not see the log output
  • Then we will now stop the two user-service services with the cluster name HZ, and now visit http://localhost:8080/order/101, then the user-service with the cluster name SH will output logs
  • NacosRule load balancing strategy
    1. Prioritize the list of statistical group service instances
    2. The local chicken flock cannot find the provider, so it goes to other flocks to look for it, and a warning will be reported
    3. After determining the list of available instances, random load balancing is used to select instances

weight configuration

  • Such a scenario will definitely occur in actual deployment
    • The performance of the server equipment is controlled by Sha Yi. The performance of some machines where some instances are located is good, while others are poor. You want machines with good performance to bear more user requests
    • But by default, NacosRule is randomly selected in the statistical group, and will not consider the problem of machine performance
  • Therefore, Nacos provides weight configuration to control the access frequency. The greater the weight, the higher the access frequency
  • In the Nacos console, find the user-service instance list, click Edit, and you can modify the weight
    {% note info no-icon %}
    Note: If the weight is changed to 0, the instance will never be accessed.
    We can set a certain Change the weight of the service to 0, and then update it, and then it will not affect the normal access of users to other service clusters. After that, we can set a small weight for the updated service, so that there will be a small number of users To access the service, test whether the service is stable (similar to the grayscale test)
    {% endnote %}

environmental isolation

  • Nacos provides namespace to realize environment isolation function
    • There can be multiple namespaces in nacos
    • The namespace can be composed of group, service, etc.
    • Different namespaces are isolated from each other, for example, services in different namespaces are invisible to each other

create namespace

  • By default, all services, data, and groups are in the same namespace named public
  • We click 命名空间-> 新建命名空间-> 填写表单to create a new namespace

Configure namespace for microservices

  • Configuring namespaces for microservices can only be achieved by modifying the configuration
  • For example, modify the application.yml file of order-service
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ
        namespace: ea980a8c-c886-4a2c-8653-d29c62d518bb # 命名空间,填上图中的命名空间ID
  • After restarting the order-service, visit the Nacos console, and you can see the following results. At this time, when you visit the order-service, because the namespace is different, the user-service will not be found. If you visit http://localhost:8080/order/ 101 will report an error

The difference between Nacos and Eureka

  • Nacos service instances can be divided into two types
    1. Temporary instance: If the instance is down for more than a certain period of time, it will be removed from the service list. The default type
    2. Non-temporary instance: If the instance goes down, it will not be removed from the service list, and it can also be called a permanent instance
  • Configure a service instance as a permanent instance
spring:
  cloud:
    nacos:
      discovery:
        ephemeral: false # 设置为非临时实例
  • The overall structure of Nacos and Eureka is similar, service registration, service pull, heartbeat waiting, but there are some differences

  • What Nacos and Eureka have in common

    1. Both support service registration and service pull
    2. Both support the service provider's heartbeat method for health monitoring
  • The difference between Nacos and Eureka

    1. Nacos supports the server to actively detect the provider status: the temporary instance adopts the heartbeat mode, and the non-temporary instance adopts the active detection mode (but the pressure on the server is relatively high, not recommended)
    2. Temporary instances with abnormal heartbeat will be removed, while non-temporary instances will not be removed
    3. Nacos supports the message push mode of service list changes, and the service list updates more timely
    4. The Nacos cluster adopts the AP mode by default. When there are non-temporary instances in the urgent crowd, the CP mode is adopted; Eureka adopts the AP mode

Nacos configuration management

  • In addition to being a registration center, Nacos can also be used for configuration management

Unified configuration management

  • When more and more instances of microservices are deployed, reaching dozens or hundreds, Zhuge’s modification of microservice configuration will make people crazy and error-prone, so we need a unified configuration management solution that can centrally manage all instances Configuration
  • On the one hand, Nacos can centrally manage the configuration, and on the other hand, it can notify the microservice in time when the configuration changes, so as to realize the hot update of the configuration

Add configuration files in Nacos

  • How to manage configuration in Nacos
    • 配置列表 -> 点击右侧加号
  • In the pop-up form, fill in the configuration information
pattern:
  dateformat: yyyy-MM-dd HH:mm:ss


{% note warning no-icon %}
Note: Only configurations that require hot updates need to be managed by Nacos. For some configurations that will not change, it is better to save them locally in the microservice (such as database connection configuration, etc.)
{ % endnote %}

Pull configuration from microservices

  • The microservice needs to pull the configuration managed in Nacos and merge it with the local application.yml configuration to complete the project startup

  • But if the host reads application.yml, how can it know the Nacos address?

  • Spring introduces a new configuration file: bootstrap.yml file, which will be read before application.yml, the process is as follows

    1. Project begining
    2. Load the bootstrap.yml file, get Nacos address, configuration file id
    3. According to the configuration file id, read the configuration file in Nacos
    4. Read the local configuration file application.yml and merge it with the configuration pulled by Nacos
    5. Create a Spring container
    6. load beans
  • Introduce nacos-config dependency

    • First, in the user-service service, introduce the client dependency of nacos-config
    <!--nacos配置管理依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    
  • Add bootstrap.yml

    • Then add a bootstrap.yml file in user-service, the content is as follows
    spring:
      application:
        name: user-service # 服务名称
      profiles:
        active: dev #开发环境,这里是dev 
      cloud:
        nacos:
          server-addr: localhost:8848 # Nacos地址
          config:
            file-extension: yaml # 文件后缀名
    
  • Here, the Nacos address will be obtained according to spring.cloud.nacos.server-addr, and then ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}the configuration will be read according to the file id.

  • In this case, read user-service-dev.yaml

  • To test whether it is actually read, we add business logic in the UserController of user-service, and read the configuration information pattern.dateformat configuration in nacos

    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("/test")
    public String test() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
  • Open the browser, visit http://localhost:8081/user/test, and see the following results, which means that the configuration information has indeed been read

Configure Hot Update

  • Our ultimate goal is to modify the configuration in Nacos, so that the configuration can take effect without restarting in the microservice, that is, configuration hot update
  • To achieve configuration hot update, you can use two methods

method one

  • Add annotation @RefreshScope (refresh scope) on the variable class injected by @Value
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {

    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("/test")
    public String test() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
}
  • Test whether hot update
    • Start the service, open the browser, and visit http://localhost:8081/user/test. Since the date format we configured before is yyyy-MM-dd MM:hh:ss, the date format we see is2022-11-12 22:11:03
    • Then we edit the configuration information directly in Nacos and save it
    pattern:
      dateformat: yyyy年MM月dd日 HH:mm:ss
    
    • Refresh the page directly without restarting the server, and the date format you see is 2022年11月12日 22:16:13, indicating that it is indeed a hot update

way two

  • Use @ConfigurationProperties annotation instead of @Valueannotation
  • In the user-service service, add a class to read pattern.dateformatproperties
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
}
  • Use this class in UserController instead@Value
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
    @Autowired
    private PatternProperties patternProperties;

    @GetMapping("/test")
    public String test() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
    }
}
  • Use the same method to test, so I won’t go into details here

configuration sharing

  • In fact, when the microservice starts, go back to Nacos to read multiple configuration files, for example
    • [spring.application.name]-[spring.profiles.active].yaml, for example: user-service-dev.yaml
    • [spring.application.name].yaml, for example: userservice.yaml
  • Does not[spring.application.name].yaml contain environments, so can be shared by multiple environments
  • Then let's test the configuration sharing through the case

Add an environment sharing configuration

  • Data IDWe add a file in Nacos user-service.yml, and the written configuration content is as follows
pattern:
  envSharedValue: 多环境共享属性值
  • Modify the user-service-dev.yml file
pattern:
  dateformat: yyyy/MM/dd HH:mm:ss
  env: user-service开发环境配置

Read shared configuration in user-service

  • Modify our PatternProperties class, add envSharedValue and env properties
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
    private String envSharedValue;
    private String env;
}
  • At the same time, modify UserController and add a method
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
    @Autowired
    private PatternProperties patternProperties;

    @GetMapping("/prop")
    public PatternProperties prop(){
        return patternProperties;
    }
}
  • Modify the startup item of UserApplication2, change its profile value to test (change the environment), and create a new user-service-test.yml configuration
pattern:
  dateformat: yyyy-MM-dd HH:mm:ss
  env: user-service测试环境配置
  • Now, our UserApplication loads the two configuration files user-service-dev.yml and user-service.yml
  • Our UserApplication2 loads the two configuration files user-service-test.yml and user-service.yml
  • Start these two services, open the browser to visit http://localhost:8081/user/prop and http://localhost:8082/user/prop respectively, the results you see are as follows {% tabs browser visits two applications
    respectively %}
{
	"dateformat": "yyyy/MM/dd HH:mm:ss",
	"envSharedValue": "多环境共享属性值",
	"env": "user-service开发环境配置"
}
{
    
    
	"dateformat": "yyyy-MM-dd HH:mm:ss",
	"envSharedValue": "多环境共享属性值",
	"env": "user-service测试环境配置"
}

{% endloss %}

  • It can be seen that both dev and test environments have read the value of the attribute envSharedValue, and dev and test also have their own unique attribute values

Configure shared priorities

  • When Nacos and service idiots have the same attribute at the same time, the priority is also divided into high and low
  • service name-profile.yaml > service name.yaml > local configuration
    • user-service-dev.yaml > user-service.yaml > application.yaml

Build a Nacos cluster

Cluster structure diagram

  • In the Nacos production environment, it must be deployed as a cluster state
  • The official Nacos cluster diagram
  • It contains 3 Nacos nodes, and then a load balancer proxy 3 Nacos. The load balancer here can use Nginx. Regarding the basic use of Nginx, I also introduced it in a previous article
    {% link St. Regis Takeaway Project Optimization, https://cyborg2077.github.io/2022/10/18 /ReggieOptimization/, https://pic1.imgdb.cn/item/6335135c16f2c2beb100182d.jpg %}
  • Our planned cluster structure
  • Addresses of 3 Nacos nodes
node ip port
nacos1 192.168.150.1 8845
nacos2 192.168.150.1 8846
nacos3 192.168.150.1 8847

Build a cluster

  • Basic steps to build a cluster
    1. Build the database and initialize the database table structure
    CREATE DATABASE IF NOT EXISTS nacos_config;
    USE nacos_config;
    CREATE TABLE `config_info` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `data_id` VARCHAR(255) NOT NULL COMMENT 'data_id',
    `group_id` VARCHAR(255) DEFAULT NULL,
    `content` LONGTEXT NOT NULL COMMENT 'content',
    `md5` VARCHAR(32) DEFAULT NULL COMMENT 'md5',
    `gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
    `src_user` TEXT COMMENT 'source user',
    `src_ip` VARCHAR(50) DEFAULT NULL COMMENT 'source ip',
    `app_name` VARCHAR(128) DEFAULT NULL,
    `tenant_id` VARCHAR(128) DEFAULT '' COMMENT '租户字段',
    `c_desc` VARCHAR(256) DEFAULT NULL,
    `c_use` VARCHAR(64) DEFAULT NULL,
    `effect` VARCHAR(64) DEFAULT NULL,
    `type` VARCHAR(64) DEFAULT NULL,
    `c_schema` TEXT,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
    
    /******************************************/
    /*   数据库全名 = nacos_config   */
    /*   表名称 = config_info_aggr   */
    /******************************************/
    CREATE TABLE `config_info_aggr` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `data_id` VARCHAR(255) NOT NULL COMMENT 'data_id',
    `group_id` VARCHAR(255) NOT NULL COMMENT 'group_id',
    `datum_id` VARCHAR(255) NOT NULL COMMENT 'datum_id',
    `content` LONGTEXT NOT NULL COMMENT '内容',
    `gmt_modified` DATETIME NOT NULL COMMENT '修改时间',
    `app_name` VARCHAR(128) DEFAULT NULL,
    `tenant_id` VARCHAR(128) DEFAULT '' COMMENT '租户字段',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
    
    
    /******************************************/
    /*   数据库全名 = nacos_config   */
    /*   表名称 = config_info_beta   */
    /******************************************/
    CREATE TABLE `config_info_beta` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `data_id` VARCHAR(255) NOT NULL COMMENT 'data_id',
    `group_id` VARCHAR(128) NOT NULL COMMENT 'group_id',
    `app_name` VARCHAR(128) DEFAULT NULL COMMENT 'app_name',
    `content` LONGTEXT NOT NULL COMMENT 'content',
    `beta_ips` VARCHAR(1024) DEFAULT NULL COMMENT 'betaIps',
    `md5` VARCHAR(32) DEFAULT NULL COMMENT 'md5',
    `gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
    `src_user` TEXT COMMENT 'source user',
    `src_ip` VARCHAR(50) DEFAULT NULL COMMENT 'source ip',
    `tenant_id` VARCHAR(128) DEFAULT '' COMMENT '租户字段',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
    
    /******************************************/
    /*   数据库全名 = nacos_config   */
    /*   表名称 = config_info_tag   */
    /******************************************/
    CREATE TABLE `config_info_tag` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `data_id` VARCHAR(255) NOT NULL COMMENT 'data_id',
    `group_id` VARCHAR(128) NOT NULL COMMENT 'group_id',
    `tenant_id` VARCHAR(128) DEFAULT '' COMMENT 'tenant_id',
    `tag_id` VARCHAR(128) NOT NULL COMMENT 'tag_id',
    `app_name` VARCHAR(128) DEFAULT NULL COMMENT 'app_name',
    `content` LONGTEXT NOT NULL COMMENT 'content',
    `md5` VARCHAR(32) DEFAULT NULL COMMENT 'md5',
    `gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
    `src_user` TEXT COMMENT 'source user',
    `src_ip` VARCHAR(50) DEFAULT NULL COMMENT 'source ip',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
    
    /******************************************/
    /*   数据库全名 = nacos_config   */
    /*   表名称 = config_tags_relation   */
    /******************************************/
    CREATE TABLE `config_tags_relation` (
    `id` BIGINT(20) NOT NULL COMMENT 'id',
    `tag_name` VARCHAR(128) NOT NULL COMMENT 'tag_name',
    `tag_type` VARCHAR(64) DEFAULT NULL COMMENT 'tag_type',
    `data_id` VARCHAR(255) NOT NULL COMMENT 'data_id',
    `group_id` VARCHAR(128) NOT NULL COMMENT 'group_id',
    `tenant_id` VARCHAR(128) DEFAULT '' COMMENT 'tenant_id',
    `nid` BIGINT(20) NOT NULL AUTO_INCREMENT,
    PRIMARY KEY (`nid`),
    UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
    KEY `idx_tenant_id` (`tenant_id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
    
    /******************************************/
    /*   数据库全名 = nacos_config   */
    /*   表名称 = group_capacity   */
    /******************************************/
    CREATE TABLE `group_capacity` (
    `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `group_id` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
    `quota` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
    `usage` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '使用量',
    `max_size` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
    `max_aggr_count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
    `max_aggr_size` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
    `max_history_count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
    `gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_group_id` (`group_id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
    
    /******************************************/
    /*   数据库全名 = nacos_config   */
    /*   表名称 = his_config_info   */
    /******************************************/
    CREATE TABLE `his_config_info` (
    `id` BIGINT(64) UNSIGNED NOT NULL,
    `nid` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    `data_id` VARCHAR(255) NOT NULL,
    `group_id` VARCHAR(128) NOT NULL,
    `app_name` VARCHAR(128) DEFAULT NULL COMMENT 'app_name',
    `content` LONGTEXT NOT NULL,
    `md5` VARCHAR(32) DEFAULT NULL,
    `gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `src_user` TEXT,
    `src_ip` VARCHAR(50) DEFAULT NULL,
    `op_type` CHAR(10) DEFAULT NULL,
    `tenant_id` VARCHAR(128) DEFAULT '' COMMENT '租户字段',
    PRIMARY KEY (`nid`),
    KEY `idx_gmt_create` (`gmt_create`),
    KEY `idx_gmt_modified` (`gmt_modified`),
    KEY `idx_did` (`data_id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
    
    
    /******************************************/
    /*   数据库全名 = nacos_config   */
    /*   表名称 = tenant_capacity   */
    /******************************************/
    CREATE TABLE `tenant_capacity` (
    `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `tenant_id` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
    `quota` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
    `usage` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '使用量',
    `max_size` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
    `max_aggr_count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
    `max_aggr_size` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
    `max_history_count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
    `gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_tenant_id` (`tenant_id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
    
    
    CREATE TABLE `tenant_info` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `kp` VARCHAR(128) NOT NULL COMMENT 'kp',
    `tenant_id` VARCHAR(128) DEFAULT '' COMMENT 'tenant_id',
    `tenant_name` VARCHAR(128) DEFAULT '' COMMENT 'tenant_name',
    `tenant_desc` VARCHAR(256) DEFAULT NULL COMMENT 'tenant_desc',
    `create_source` VARCHAR(32) DEFAULT NULL COMMENT 'create_source',
    `gmt_create` BIGINT(20) NOT NULL COMMENT '创建时间',
    `gmt_modified` BIGINT(20) NOT NULL COMMENT '修改时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
    KEY `idx_tenant_id` (`tenant_id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
    
    CREATE TABLE `users` (
    `username` VARCHAR(50) NOT NULL PRIMARY KEY,
    `password` VARCHAR(500) NOT NULL,
    `enabled` BOOLEAN NOT NULL
    );
    
    CREATE TABLE `roles` (
    `username` VARCHAR(50) NOT NULL,
    `role` VARCHAR(50) NOT NULL,
    UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
    );
    
    CREATE TABLE `permissions` (
    `role` VARCHAR(50) NOT NULL,
    `resource` VARCHAR(255) NOT NULL,
    `action` VARCHAR(8) NOT NULL,
    UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
    );
    
    INSERT INTO users (username, PASSWORD, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
    
    INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
    
    1. Configure Nacos
      • We enter the conf directory of Nacos, modify the configuration file cluster.conf.example, rename it to cluster.conf, and then add content. If an error is reported when starting later, replace 127.0.0.1 here with the real IP of the machine
      127.0.0.1:8845
      127.0.0.1:8846
      127.0.0.1:8847
      
      • Then modify the application.properties file and add the database configuration
      spring.datasource.platform=mysql
      
      db.num=1
      
      db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
      db.user.0=root
      db.password.0=root
      
    2. Start the Nacos cluster
      • Copy 3 copies of the nacos folder and name them respectively: nacos1, nacos2, nacos3
      • Then modify the application.properties in these three folders respectively
        • nacos1
        server.port=8845
        
        • nacos2
        server.port=8846
        
        • nacos3
        server.port=8847
        
    3. Nginx reverse proxy
      • Modify the conf/nginx.conf file and paste the following configuration into the http block
      upstream nacos-cluster {
          server 127.0.0.1:8845;
          server 127.0.0.1:8846;
          server 127.0.0.1:8847;
      }
      
      server {
          listen       80;
          server_name  localhost;
      
          location /nacos {
              proxy_pass http://nacos-cluster;
          }
      }
      
      • Start nginx, and then visit http://localhost/nacos in the browser
      • At the same time, change the Nacos address in bootstrap.yml to localhost:80, and change both user-service and order-service
      spring:
        cloud:
          nacos:
            server-addr: localhost:80 # Nacos地址
      
      • Restart the service, and you can see the managed service

        {% note warning no-icon %} in Nacos
        . If an error is reported, please replace the previous 127.0.0.1 with the local IP, such as 192.168.1.7
        {% endnote %}

Feign remote call

  • Let's take a look at the code we used to initiate remote calls using RestTemplate
String url = "http://user-service/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
  • The following problems exist:
    1. Poor code readability and inconsistent programming experience
    2. URLs with complex parameters are difficult to maintain (Baidu randomly searches for a Chinese term, and then sees how long the URL is and how many parameters it has)
  • We can use Feign to solve the problems mentioned above
  • Feign is a declarative http client, the official website address https://github.com/OpenFeign/feign, its role is to help us send http requests elegantly

Feign replaces RestTemplate

  • The steps to use Feign are as follows
    1. Introduce dependencies
      • We introduce Feign's dependency in the pom file of the order-service service
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-openfeign</artifactId>
      </dependency>
      
    2. add annotation
      • Add annotations to the startup class of order-service @EnableFeignClientsto enable the function of Feign
    3. Write a Feign client
      • Create a new com.itcast.order.client package in order-service, and then create a new interface, the content is as follows
      @FeignClient("user-service")
      public interface UserClient {
          @GetMapping("/user/{id}")
          User findById(@PathVariable("id") Long id);
      }
      
      • This client is mainly based on SpringMVC annotations to declare remote call information, such as
        1. Service name: user-service
        2. Request method: GET
        3. Request path: /user/{id}
        4. Request parameter: Long id
        5. Return value type: User
      • In this way, Feign can help us send http requests without using RestTemplate to send them ourselves
    4. test
      • Modify the queryOrderById method in the OrderService class in order-service, and use Feign client instead of RestTemplate
        {% tabs FEIGn instead of RestTemplate %}
      @Service
      public class OrderService {
          @Autowired
          private OrderMapper orderMapper;
      -   @Autowired
      -   private RestTemplate restTemplate;
      +   @Autowired
      +   private UserClient userClient;
      
          public Order queryOrderById(Long orderId) {
              Order order = orderMapper.findById(orderId);
      -       String url = "http://user-service/user/" + order.getUserId();
      -       User user = restTemplate.getForObject(url, User.class);
      +       User user = userClient.findById(order.getUserId());
              order.setUser(user);
              return order;
          }
      }
      
      The modified code is much more elegant than before
      @Service
      public class OrderService {
          @Autowired
          private OrderMapper orderMapper;
          @Autowired
          private UserClient userClient;
      
          public Order queryOrderById(Long orderId) {
              // 1. 查询订单
              Order order = orderMapper.findById(orderId);
              // 2. 利用Feign发起http请求,查询用户
              User user = userClient.findById(order.getUserId());
              // 3. 封账user到order
              order.setUser(user);
              // 4. 返回
              return order;
          }
      }
      
      {% endloss %}
    5. Summarize
      • Steps to use Feign
        1. Introduce dependencies
        2. Add the @EnableFeignClients annotation to the main startup class
        3. Write the FeignClient interface
        4. Use methods defined in FeignClient instead of RestTemplate

custom configuration

  • Feign can support many custom configurations, as shown in the table below
type effect illustrate
feign.Logger.Level Modify log level Contains four different levels: NONE, BASIC, HEADERS, FULL
feign.codec.Decoder Parser for the response result Parsing the results of http remote calls, such as parsing json strings into java objects
feign.codec.Encoder request parameter encoding Encode request parameters for sending via http requests
feign. Contract Supported Annotation Formats The default is the annotation of SpringMVC
feign. Retryer Failure retry mechanism The retry mechanism for request failure, the default is no, but Ribbon's retry will be used
  • Under normal circumstances, the default value is enough for our use. If you need to customize, you only need to create a custom @Bean to override the default Bean. The following uses the log as an example to demonstrate how to customize the configuration

configuration file method

  • Modifying Feign's log level based on the configuration file can target a single service
feign:  
  client:
    config: 
      userservice: # 针对某个微服务的配置
        loggerLevel: FULL #  日志级别
  • can also target all services
feign:  
  client:
    config: 
      default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: FULL #  日志级别 
  • The log levels are divided into four
    1. NONE: Do not record any log information, which is the default value
    2. BASIC: Only log the request method, URL, response status code and execution time
    3. HEADERS: On the basis of BASIC, the request and response header information is additionally recorded
    4. FULL: Record details of all requests and responses, including header information, request body, metadata

Java code method

  • You can also modify the log level based on Java code, first declare a class, and then declare a Logger.Level object
public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; //日志级别设置为 BASIC
    }
}
  • If you want to take effect globally, put it in the @EnableFeignClients annotation of the startup class
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
  • If it is locally effective, put it in the corresponding @FeignClient annotation
@FeignClient(value = "user-service", configuration = DefaultFeignConfiguration.class)

Feign usage optimization

  • The bottom layer of Feign initiates http requests and depends on other frameworks. Its bottom layer client implementation includes
    1. URLConnection: default implementation, does not support connection pooling
    2. Apache HttpClient: supports connection pooling
    3. OKHttp: support connection pool
  • Therefore, the main means to improve the performance of Fring is to use the connection pool instead of the default URLConnection
  • Here we use Apache's HttpClient to demonstrate
    1. Introduce dependencies
      • Introduce Apache's HttpClient dependency in the pom file of order-service
      <!--httpClient的依赖 -->
      <dependency>
          <groupId>io.github.openfeign</groupId>
          <artifactId>feign-httpclient</artifactId>
      </dependency>
      
    2. Configure connection pool
      • Add configuration in application.yml of order-service
      feign:
        client:
          config:
            default: # default全局的配置
              logger-level: BASIC # 日志级别,BASIC就是基本的请求和响应信息
        httpclient:
          enabled: true # 开启feign对HttpClient的支持
          max-connections: 200 # 最大的连接数
          max-connections-per-route: 50 # 每个路径的最大连接数
      
  • Summary, Feign optimization
    1. Log level try to use BASIC
    2. Use HttpClient or OKHttp instead of URLConnection
      • Introduce feign-httpclient dependency
      • Enable the httpclient function in the configuration file and set the connection pool parameters

Best Practices

  • The so-called best practice is the experience summed up in the use process, the best way to use
  • Careful observation shows that Feign's client and service provider's controller code are very similar
    {% tabs Feign's client and service provider's controller code are very similar%}
@FeignClient(value = "user-service",configuration = DefaultFeignConfiguration.class)
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return userService.queryById(id);
    }
}

{% endloss %}

  • Except for the method name, the rest of the code is almost exactly the same. Is there a way to simplify this repetitive code writing?

Inheritance

  • These two parts of the same code can be shared through inheritance
    1. Define an API interface, use the definition method, and make a statement based on SpringMVC annotations
    public interface UserAPI{
        @GetMapping("/user/{id}")
        User findById(@PathVariable("id") Long id); 
    }
    
    1. Both Feign client and Controller inherit this interface
      {% tabs Feign client and Controller both inherit this interface%}
    @FeignClient(value = "user-service")
    public interface UserClient extends UserAPI{}
    
    @RestController
    public class UserController implents UserAPI{
        public User findById(@PathVariable("id") Long id){
            // ...实现业务逻辑
        }
    }
    
    {% endloss %}
  • advantage
    1. Simple
    2. code sharing
  • shortcoming
    1. Service provider and service consumer are tightly coupled
    2. The annotation mapping in the parameter list will not be inherited, so the method, parameter list, and annotation must be declared again in the Controller

Extraction method

  • Extract Feign's Client as an independent module, and put the POJO related to the interface and the default Feign configuration into this module, and provide it to all consumers
  • For example, extract the default configurations of UserClient, User, and Feign into a feign-api package, and all microservices can refer to this dependency package and use it directly

Implement extraction-based best practices

  1. extract
    • First create a new module named feign-api, and then introduce feign's starter dependency in the pom file
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    • Then copy the UserClient, User, and DefaultFeignConfiguration written in order-service to the feign-api project
  2. Use feign-api in order-service
    • First, delete the classes or interfaces such as UserClient, User, DefaultFeignConfiguration in order-service
    • Then introduce our own feign-api dependency in the pom file in order-service
    <dependency>
        <groupId>cn.itcast.demo</groupId>
        <artifactId>feign-api</artifactId>
        <version>1.0</version>
    </dependency>
    
    • Then modify the red part of the code involving the above three components in order-service
  3. Troubleshooting Package Scanning Issues
  • Now UserClient is under the cn.itcast.feign.clients package, and the @EnableFeignClients annotation of order-service is under the cn.itcast.order package, not in the same package, and UserClient cannot be scanned
    • Method 1: Specify the package that Feign should scan
    @EnableFeignClients(basePackages = "cn.itcast.feign.clients")
    
    • Method 2: Specify the Client interface to be loaded
    @EnableFeignClients(clients = {UserClient.class})
    

Gateway service gateway

  • SpringCloudGateway is a new project of SpringCloud. The project is a gateway developed based on Spring 5.0, SpringBoot2.0 and ProjectReactor and other responsive and event streaming technologies. It aims to provide a simple and effective unified API route for the microservice framework management style

Why do you need a gateway

  • Gateway is the gatekeeper of our services and the unified entrance of all microservices

  • The core functional characteristics of the gateway

    1. request routing
    2. access control
    3. Limiting
  • The architecture diagram is as follows

  • Routing and load balancing: All requests must first pass through the gateway, but the gateway does not process business, but forwards the request to a microservice according to certain rules. This process is called routing. Of course, when there are multiple target services for routing, load balancing is also required

  • Access control: the gateway is the entrance of the microservice, it needs to check whether the user is qualified to request, if not, it will be intercepted

  • Current limiting: When the request volume is too high, the gateway will release the request according to the speed that the microservice can accept to avoid excessive service pressure

  • The implementation of the gateway in Spring Cloud includes two

    1. gateway
    2. zul
  • Zuul is a Servlet-based implementation and belongs to blocking programming. SpringCloudGateway is based on WebFlux provided in Spring5, which belongs to the implementation of responsive programming and has better performance.

gatewayquickstart

  • Next, let's demonstrate the basic routing function of the gateway. The basic steps are as follows
    1. Create a SpringBoot project gateway and introduce gateway dependencies
      • Just create a maven project and import dependencies as follows
      <!--网关-->
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-gateway</artifactId>
      </dependency>
      <!--nacos服务发现依赖-->
      <dependency>
          <groupId>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      </dependency>
      
    2. Write startup class
    @SpringBootApplication
    public class GatewayApplication {
        public static void main(String[] args) {
            SpringApplication.run(GatewayApplication.class,args);
        }
    }
    
    1. Write basic configuration and routing rules
    server:
      port: 10010 # 网关端口
    spring:
      application:
        name: gateway # 服务名称
      cloud:
        nacos:
          server-addr: localhost:80 # nacos地址(我这里还是用的nginx反向代理,你们可以启动一个单体的nacos,用8848端口)
        gateway:
          routes:
            - id: user-service # 路由id,自定义,只需要唯一即可
              uri: lb://user-service # 路由的目标地址,lb表示负载均衡,后面跟服务名称
              # uri: http://localhost:8081 # 路由的目标地址,http就是固定地址
              predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
                - Path=/user/** # 这个是按照路径匹配,只要是以/user开头的,就符合规则
            - id: order-service # 按照上面的写法,再配置一下order-service
              uri: lb://order-service 
              predicates: 
                - Path=/order/** 
    
    1. Start the gateway service for testing
      • Restart the gateway, when accessing http://localhost:10010/user/1, the /user/** rule is met, and the request is forwarded to http://user-service/user/1, the result is as follows
      {
          "id": 1,
          "username": "柳岩",
          "address": "湖南省衡阳市"
      }
      
      • When accessing http://localhost:10010/order/101, the /order/** rule is met, and the request is forwarded to http://order-service/order/101, the result is as follows
      {
          "id": 101,
          "price": 699900,
          "name": "Apple 苹果 iPhone 12 ",
          "num": 1,
          "userId": 1,
          "user": {
              "id": 1,
              "username": "柳岩",
              "address": "湖南省衡阳市"
          }
      }
      
      1. Flowchart of gateway land tour
  • Summarize
    • Steps to build a gateway
      1. Create a project and introduce nacos and gateway dependencies
      2. Configure application.yml, including basic service information, nacos address, routing
    • Routing configuration includes
      1. route id: the unique representation of the route
      2. Routing destination (uri): the destination address of the routing, http stands for fixed address, lb stands for load balancing based on service name
      3. Routing assertions (predicates): Rules for judging routing
      4. Route filters (filters): process the request or response
  • Next, let's focus on learning the detailed knowledge of routing assertions and routing filters

assertion factory

  • The assertion rules we write in the configuration file are just strings, which will be Predicate Factoryread and processed, and turned into conditions for routing judgments
  • For example, Path=/user/**it is matched according to the path. This rule is org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactoryhandled by the class. There are more than a dozen assertion factories like this in SpringCloudGatewway
name illustrate example
After is a request after a certain point in time - After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before is a request before some point in time - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between is a request before a certain two points in time - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie Requests must contain certain cookies - Cookie=chocolate, ch.p
Header Requests must contain certain headers - Header=X-Request-Id, \d+
Host The request must be to access a certain host (domain name) - Host=.somehost.org,.anotherhost.org
Method The request method must be specified - Method=GET,POST
Path The request path must conform to the specified rules - Path=/red/{segment},/blue/**
Query The request parameters must contain the specified parameters - Query=name, Jack or - Query=name
RemoteAddr The requester's ip must be in the specified range - RemoteAddr=192.168.1.1/24
Weight weight processing
  • 关于更详细的使用方法,可以参考官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories

过滤器工厂

  • GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理

路由过滤器的种类

  • Spring提供了31中不同的路由过滤器工厂,例如
名称 说明
AddRequestHeader 给当前请求添加一个请求头
RemoveRequestHeader 移除请求中的一个请求头
AddResponseHeader 给响应结果中添加一个响应头
RemoveResponseHeader 从响应结果中移除有一个响应头
RequestRateLimiter 限制请求的流量
  • 官方文档的使用举例
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: https://example.org
        filters:
        - AddRequestHeader=X-Request-red, blue
  • This listing adds X-Request-red:blue header to the downstream request’s headers for all matching requests.

  • 关于更详细的使用方法,可以参考官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories

请求头过滤器

  • 下面我们以AddRequestHeader为例,作为讲解
    {% note info no-icon %}
    需求:给所有进入user-service的请求都添加一个请求头:Truth=Welcome to Kyle’s Blog!
    {% endnote %}
  • 只需要修改gateway服务的application.yml文件,添加路由过滤即可
server:
  port: 10010 # 网关端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:80 # nacos地址
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/user/**
          filters:
            - AddRequestHeader=Truth, Welcome to Kyle's Blog! # 添加请求头
  • 当前过滤器写在user-service路由下,因此仅仅对访问user-service的请求有效,我们在UserController中编写对应的方法来测试
@GetMapping("/test")
public void test(@RequestHeader("Truth") String tmp) {
    System.out.println(tmp);
}
  • 重启网关和user-service,打开浏览器访问http://localhost:10010/user/test, 控制台会输出Welcome to Kyle's Blog!,证明我们的配置已经生效

默认过滤器

  • 如果要对所有的路由都生效,则可以将过滤器工厂写到default下,格式如下
server:
  port: 10010 # 网关端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:80 # nacos地址
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/user/**
      default-filters: 
        - AddRequestHeader=Truth, Welcome to Kyle's Blog! # 添加请求头
  • 重启网关服务,打开浏览器访问http://localhost:10010/user/test, 控制台依旧会输出Welcome to Kyle's Blog!,证明我们的配置已经生效

小结

  • 过滤器的作用是什么?
    • 对路由的请求或响应做加工处理,比如添加请求头
    • 配置在路由下的过滤器只对当前路由请求生效
  • default-filters的作用是什么?
    • 对所有路由都生效的过滤器

全局过滤器

  • 上面提到的31中过滤器的每一种的作用都是固定的,如果我们希望拦截请求,做自己的业务逻辑,则无法实现,这就要用到我们的全局过滤器了

全局过滤器的作用

  • 全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理的逻辑是固定的,而GlobalFilter的逻辑需要我们自己编写代码实现
  • 定义的方式就是实现GlobalFilter接口
public interface GlobalFilter {
    /**
     *  处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
     *
     * @param exchange 请求上下文,里面可以获取Request、Response等信息
     * @param chain 用来把请求委托给下一个过滤器 
     * @return {@code Mono<Void>} 返回标示当前过滤器业务结束
     */
    Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
  • 在filter中编写自定义逻辑,可以实现下列功能
    1. 登录状态判断
    2. 权限校验
    3. 请求限流等

自定义全局过滤器

  • 需求:定义全局过滤器,拦截请求,判断请求参数是否满足下面条件
    1. 参数中是否有authorization
    2. authorization参数值是否为admin
  • 如果同时满足,则放行,否则拦截
  • 具体实现如下
    • 在gateway模块下新建cn.itcast.gateway.filter包,然后在其中编写AuthorizationFilter类,实现GlobalFilter接口,重写其中的filter方法
    public class AuthorizationFilter implements GlobalFilter {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1. 获取请求参数
            MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
            // 2. 获取authorization参数
            String authorization = params.getFirst("authorization");
            // 3. 校验
            if ("admin".equals(authorization)) {
                // 4. 满足需求则放行
                return chain.filter(exchange);
            }
            // 5. 不满足需求,设置状态码,这里的常量底层就是401,在restFul中401表示未登录
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            // 6. 结束处理
            return exchange.getResponse().setComplete();
        }
    }
    
  • 重启网关,测试我们的拦截器是否生效,打开浏览器访问http://localhost:10010/user/1,无法正常访问;加上需要的请求参数访问http://localhost:10010/user/1?authorization=admin, 可以看到正常数据
{
    "id": 1,
    "username": "柳岩",
    "address": "湖南省衡阳市"
}

过滤器执行顺序

  • 请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
  • 请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
  • 那么排序的规则是什么呢?
    • 每个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前(默认值为2147483647,即int最大值)
    • GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,需要我们自己指定
      {% tabs asdasd %}
    public class AuthorizationFilter implements GlobalFilter, Ordered {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1. 获取请求参数
            MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
            // 2. 获取authorization参数
            String authorization = params.getFirst("authorization");
            // 3. 校验
            if ("admin".equals(authorization)) {
                // 4. 满足需求则放行
                return chain.filter(exchange);
            }
            // 5. 不满足需求,设置状态码,这里的常量底层就是401,在restFul中401表示未登录
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            // 6. 结束处理
            return exchange.getResponse().setComplete();
        }
    
        @Override
        public int getOrder() {
            return -1;
        }
    }
    
    @Order(-1)
    @Component
    public class AuthorizationFilter implements GlobalFilter {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1. 获取请求参数
            MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
            // 2. 获取authorization参数
            String authorization = params.getFirst("authorization");
            // 3. 校验
            if ("admin".equals(authorization)) {
                // 4. 满足需求则放行
                return chain.filter(exchange);
            }
            // 5. 不满足需求,设置状态码,这里的常量底层就是401,在restFul中401表示未登录
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            // 6. 结束处理
            return exchange.getResponse().setComplete();
        }
    }
    
    {% endtabs %}
    • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
    • 当过滤器的order值一样时,会按照defaultFilter > 路由过滤器 > GlobalFilter的顺序执行
      • 例如下面这种情况下的order值就会相同,如果我们在自定义全局过滤器中设定的order也为1,那么也会冲突
      server:
        port: 10010 # 网关端口
      spring:
        application:
          name: gateway # 服务名称
        cloud:
          nacos:
            server-addr: localhost:80 # nacos地址
          gateway:
            routes:
              - id: user-service
                uri: lb://user-service
                predicates:
                  - Path=/user/**
                filters:
                  - AddRequestHeader=Truth, Welcome to Kyle's Blog! # 1
                  - AddRequestHeader=Truth, Welcome to Kyle's Blog! # 2
                  - AddRequestHeader=Truth, Welcome to Kyle's Blog! # 3
            default-filters:
              - AddRequestHeader=Truth, Welcome to Kyle's Blog! # 1
              - AddRequestHeader=Truth, Welcome to Kyle's Blog! # 2
              - AddRequestHeader=Truth, Welcome to Kyle's Blog! # 3
      
    • 详细内容,可以查看源码:
      • org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。
      • org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链

跨域问题

什么是跨域问题

  • 跨域:域名不一致就是跨域,主要包括
    1. 域名不同:www.baidu.comwww.baidu.orgwww.js.commiaosha.js.com
    2. 域名相同,端口不同:localhost:8080和localhost:8081
  • 跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
  • 解决方案:CORS
    • CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
    • 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

解决跨域问题

  • 在gateway服务的application.yml文件中,添加下面的配置
spring:
  cloud:
    gateway:
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许哪些网站的跨域请求 
              - "http://localhost:9527"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

Guess you like

Origin blog.csdn.net/qq_33888850/article/details/129770124