Grain Mall 24 Sentinel Current Limit & Circuit Breaker & Downgrade

insert image description here
insert image description here
All the above methods we added to the seckill service are for the sake of , in addition to being fast, we also need guarantees 稳定.

No matter how fast we are, there will be a limit value. Now suppose that a single machine can process 10,000 orders per second, which is already a super high processing capacity. Five servers are connected to the seckill service, and three of them are offline, but the seckill request gateway Just let go of 100,000 requests and put them all in, then the remaining two servers will not be able to handle them. The peak value of each server is 10,000, and all requests have to be queued, which will cause the accumulation of request time , over time, resources are exhausted, and the server will crash.

So when it is almost guaranteed, we need to ensure stability.

How to ensure stability is in our distributed system. 限流&熔断&降级No matter which distributed system we have, whether it is high concurrency or not, we must consider it, because with these protection methods, our entire cluster can achieve stability.

We used to use springCloud's hystrix, which is not updated anymore, and the supported functions are limited.
In our system, we use it springCloud alibaba的Sentinelto complete the entire system 限流&熔断&降级.
It will protect our entire system very stably. Even a large cluster of hundreds of servers, with the protection of Sentinel, will be very stable when several servers go online or crash.

Current Limiting & Fusing & Degrading

  • What is a blown
    service? Service A calls a function of service B. Due to network instability, or service B is stuck, the function takes too long. If this happens too many times. We can directly disconnect B (A no longer requests the B interface), and those who call B directly return the degraded data without waiting for B's super long execution. In this way, the failure of B will not affect A in cascade.

    If there is no protection, feign calls remotely, and feign has a default timeout period, for example, 3s. If no data is returned within 3s, it is considered that there is a problem with the called service, and the feign interface will report a timeout error, but we can't wait for this For a long time, because this will cause the entire call chain 累积效应,
    a calls b, b calls c, c method now has to wait for 3s, b needs to wait for c, a needs to wait for b, everyone needs to wait, the whole line will be stuck, and the resources cannot be used. If it is released, the throughput will drop, and a large number of requests are queued again, which forms a situation. The 死循环worse the capacity is, the more requests accumulate, and the more requests require more resources for allocation and processing, our machines will be It will freeze and crash.
    So we need to add a circuit breaker mechanism. A calls b. If it is found that b cannot return normally, then we will directly disconnect b in the future. Next, a calls b without paying attention to whether b is successful or not, and returns quickly to fail.
    Fusing can ensure that our entire service will not be affected by cascading. If a service is hung up, the entire call chain will not be stuck for a long time.

  • What is downgrade?
    The whole website is in the peak period of traffic, and the pressure on the server increases sharply. According to the current business situation and traffic, some services and pages are strategically downgraded [停止服务,所有的调用直接返回降级数据]. In this way, the pressure on server resources is relieved to ensure the normal operation of the core business, and at the same time, customers and most customers are properly responded.

    Assume that the traffic is at its peak, and there are a lot of businesses running now, some core businesses, such as shopping carts, orders, etc., and some non-core businesses, such as registration, etc., and the website is currently in the peak period of flash sales. Not enough.
    We can manually allow some non-core businesses, such as registration, to stop the registration business of the server. If there are other businesses on the server, we can give up resources to other core businesses. This is downgrading.
    At the same time, you can return to a downgrade page, prompt 此功能暂时不可用.

  • similarities and differences

    • Same point

      1. In order to ensure the availability and reliability of most services in the cluster and prevent crashes, sacrifice the ego
      2. Users end up experiencing that a feature is unavailable
    • difference:

      1. The circuit breaker is the fault of the callee, which triggers the active rule of the system
      2. The downgrade is based on global considerations, manually stopping some normal services and releasing resources
  • What is flow limiting?
    Control the flow of requests entering the service, so that the service can bear the flow pressure that does not exceed its own capacity.
    For example, the processing capacity of the entire cluster is 1w per second, so the request we put back from the gateway is 1w. For those who are unlucky, report the error directly, retry by yourself, or how to do it.
    Flow limiting is to limit the flow of the entire entrance to ensure that our service will not be completely overwhelmed by the flow exceeding its capacity. As long as the traffic exceeding its capacity is directly discarded, there is no need to deal with it.

Sentinel

Sentinel can be used for current limiting & fusing & downgrading functions.

The difference between Sentinel and Hystrix

insert image description here

1. Isolation strategy

Suppose we now have 100 requests that all come in to execute, and the system is not capable enough to execute 50

  • Thread pool isolation
    If it is Hystrix, such as a hello request, make a thread pool for this request, allocate 50 threads, and the thread pool allocates a thread to execute. If there are not enough threads, call back.
    But if there are too many requests, each request will correspond to a different thread pool, and there will be too many thread pools, and switching between thread pools is also a waste of time, which has a great impact on performance. It may be that the thread pool is not enough to use resources, which may cause service downtime.

  • Semaphore isolation
    There is also a Semaphore in java8, which is the same as our redis Semaphore. As long as the request comes in, if the limit is 50, each request that calls the request has a corresponding semaphore, and a request semaphore -1 comes in. , After executing a request semaphore +1, if it is found that the request comes in 50, the next time it comes in, it will be called back directly.
    There is no need to create a separate thread pool for each request, resulting in resource consumption.

Thread pool isolation also has its advantages. Each request uses its own thread pool, and its own thread pool is blown up, and it has nothing to do with others.
The semaphore is that once someone blows up, there will be some problems in our entire service.

2. Circuit breaker downgrade strategy

  • Based on the response time
    , for example, each request will not be executed as long as it exceeds 1s

  • Abnormal rate
    One request is requested one hundred times, and 90% of them have abnormalities, so I will not request in the future.

  • Abnormal number
    Five out of one hundred requests are abnormal, and no more requests are made.

We have many strategies to limit whether to fuse or downgrade the call chain (service) behind.

3. Dynamic rule configuration

The above strategies can be dynamically configured through the data source, that is, the configuration is persisted in the database, and the previous configuration can still be used even if the service is restarted.

4. System adaptive protection

After knowing the capabilities of the system, it will let in all the traffic during low-peak periods, and limit it during peak periods.

Introduction

Sentinel can be simply divided into Sentinel core library and Dashboard (web visual interface, with a visual interface, it is very convenient to adjust and monitor). The core library does not depend on Dashboard, but it can achieve the best results in combination with Dashboard.

The resource we are talking about can be anything, a service, a method in a service, or even a piece of code. Using Sentinel for resource protection is mainly divided into several steps:

  1. Define resources (which resources are to be protected)
  2. Define rules (define protection rules, such as how many times per second the request will not allow access, and if the cpu load exceeds the number, it will be downgraded)
  3. Check if the rule is in effect

define resources

The following are the three most commonly used ways of defining resources

Method 1: Default adaptation of mainstream frameworks

In order to reduce the complexity of development, we have adapted most of the mainstream frameworks, such as Web Servlet, Dubbo, Spring Cloud, gRPC, Spring WebFlux, Reactor, etc. You only need to introduce the corresponding dependencies to easily integrate Sentinel.

For mainstream web frameworks, all requests come in for adaptation by default, and all requests are resources to be protected.

Method 2: Define resources by throwing exceptions

SphU includes a try-catch style API. In this way, a BlockException will be thrown when the resource is throttled. At this time, exceptions can be caught and logical processing after current limiting can be performed. The sample code is as follows:

// 1.5.0 版本开始可以利用 try-with-resources 特性(使用有限制)
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
try (Entry entry = SphU.entry("resourceName")) {
    
    
  // 被保护的业务逻辑
  // do something here...
} catch (BlockException ex) {
    
    
  // 资源访问阻止,被限流或被降级
  // 在此处进行相应的处理操作
}

Method 4: Define resources by annotation

Sentinel supports defining resources through the @SentinelResource annotation and configuring blockHandler and fallback functions for processing after current limiting. Example:

// 原本的业务方法.
@SentinelResource(blockHandler = "blockHandlerForGetUser")
public User getUserById(String id) {
    
    
    throw new RuntimeException("getUserById command failed");
}

// blockHandler 函数,原方法调用被限流/降级/系统保护的时候调用
public User blockHandlerForGetUser(String id, BlockException ex) {
    
    
    return new User("admin");
}

Types of rules

All Sentinel rules can be dynamically queried and modified in the memory state, and will take effect immediately after modification. At the same time, Sentinel also provides related APIs for you to customize your own rules and strategies.

Sentinel supports the following types of rules: 流量控制规则、熔断降级规则、系统保护规则(cpu、内存超过多少占用率)、来源访问控制规则(设置黑、白名单) 和 热点参数规则.

Flow Control Rules

This is a configuration in json format
insert image description here

insert image description here
The above picture is a classic call tree, if the nodeA is limited

  • strategy call relationship current limiting strategy

    • Directly
      only limit the flow of nodeA

    • Link
      You can specify the entry resource on the link. If root is specified as the entry, then only the current limit called from root to nodeA will take effect. If Entrance2 (or other link) calls nodeA, it will not take effect.

    • Association
      Two resources are associated when there is resource contention or dependency between them.
      For example, there is contention between read and write operations on the same field in the database. If the reading speed is too high, the writing speed will be affected, and if the writing speed is too high, the reading speed will be affected. If read and write operations are allowed to compete for resources, the overhead caused by the contention itself will reduce the overall throughput. Associated current limiting can be used to avoid excessive contention between resources with associated relationships.
      For example, the two resources read_db and write_db represent database read and write respectively. We can set current limiting rules for read_db to achieve the purpose of writing priority : Set strategy to RuleConstant.STRATEGY_RELATE and set refResource to write_db. In this way, when the operation of writing to the library is too frequent, the request for reading data will be limited.

  • controlBehavior
    slow start mode: The traffic increases slowly, and finally increases to the load capacity of the system.

  • controlBehavior

    • Reject directly
      Reject directly, throw an exception

    • Waiting in line
      If the system's concurrent requests are exceeded, wait in line, and we can also set a timeout.

    • warm up slow start mode warm up mode
      The system can withstand a maximum of 100 concurrency, we can set the warm-up time, slowly increase the concurrency within the specified time, and finally reach 100 concurrency after the specified time is reached

Define flow control rules through code
After understanding the definition of the above rules, we can define flow control rules in a hard-coded way by calling the FlowRuleManager.loadRules() method, for example:

private void initFlowQpsRule() {
    
    
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule(resourceName);
    // set limit qps to 20
    rule.setCount(20);
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    rule.setLimitApp("default");
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}

At the same time, these control rules can also be set with a visual console (sentinel dashboard).

console download

The version of the console (a jar package) is consistent with the version of sentinel-core:1.8.0. Use java -jar to start directly.

The account secrets are all sentinel. After logging in, there is no information in the interface. This page is lazy loaded. We send a request and the page will only be displayed if it is a protected resource.

Sentinel integrates springboot

package com.atlinxi.gulimall.seckill;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;


/**
 * 1. 整合Sentinel
 * 		1)导入依赖 spring-cloud-starter-alibaba-sentinel
 * 		2)下载sentinel的控制台
 * 		3)配置sentinel控制台地址信息
 * 		4)在控制台调整参数。【默认所有的流控设置保存在内存中,重启失效】
 *
 *
 * 	2. 每一个微服务都导入actuator 并配置 management.endpoints.web.exposure.include=*
 * 	3. 自定义sentinel流控返回数据
 * 	4. 使用sentinel来保护feign远程调用:熔断
 * 		1)、调用方的熔断保护:feign.sentinel.enabled=true
 * 		2)、调用方手动指定远程服务的降级策略。远程服务被降级处理。默认触发我们的熔断回调方法。
 * 		3)、超大流量的时候,必须牺牲一些远程服务,在服务的提供方(远程服务)指定降级策略,
 * 			提供方是在运行。但是不运行自己的业务逻辑,返回的是默认的熔断数据(限流后的数据),我们写的config返回的数据
 *
 * 	5. 自定义受保护的资源
 * 		1)、代码
 * 			try (Entry entry = SphU.entry("seckillSkus")){
 * 			 	// 业务逻辑
 * 			}catch(Exception e){}
 *
 * 		2)、基于注解
 * 			@SentinelResource("getCurrentSeckillSkusResource")
 *
 * 		无论是1,2方式一定要配置被限流以后的默认返回
 * 		url级别的请求可以设置统一返回:BlockExceptionHandler
 *
 */
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallSeckillApplication {
    
    

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

}

Solve the problem of dependence

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>



<dependency>
			<groupId>com.atlinxi.gulimall</groupId>
			<artifactId>gulimall-common</artifactId>
			<version>0.0.1-SNAPSHOT</version>
			<exclusions>
				<exclusion>
					<groupId>com.alibaba.cloud</groupId>
					<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
				</exclusion>
				<!--
					不知道为什么,有validation-api的时候sentinel启动就报

					Unable to create a Configuration, because no Bean Validation provider could be found. Add a provider like Hibernate Validator (RI) to your classpath.
					然后把validation-api去掉sentinel就可以成功启动了
				-->
				<exclusion>
					<groupId>javax.validation</groupId>
					<artifactId>validation-api</artifactId>
				</exclusion>
			</exclusions>
		</dependency>








<!--
	上面的方法是可以解决这个问题的,但此时又有一个问题,要是必须用到validation-api怎么办
	添加以下依赖即可,就不需要排除validation-api,sentinel也可以正常启动
-->
<dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
            <version>2.0.2</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.18.Final</version>
            <scope>compile</scope>
        </dependency>
// 每个微服务都需要配置
spring:
  cloud:
    sentinel:
      transport:
      	// 微服务和控制台之间的通信
        port: 8719
        // 控制台的地址
        dashboard: localhost:8080

Limit the flow of ordinary requests

Normal requests are relative to remote calls.

At this time, when we request http://seckill.gulimall.com/currentSeckillSkus, the sentinel console will display the seckill service, and there will be a request in 簇点链路the menu bar . We can directly perform and set the request on the console . We quickly refresh the page in the page, and a prompt will appear. And printing the log in the method will not be executed. This is the simplest flow control./currentSeckillSkus流控QPS为1Blocked by Sentinel (flow limiting)

Solve the problem that there is no data in real-time monitoring of the console

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>

# 暴露所有资源
management.endpoints.web.exposure.include=*

The data returned by default after requesting current limiting is sentinel official by default

Here we use our own defined

package com.atlinxi.gulimall.seckill.config;


import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.atlinxi.common.exception.BizCodeEnume;
import com.atlinxi.common.utils.R;
import org.springframework.context.annotation.Configuration;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Configuration
public class SeckillSentinelConfig implements BlockExceptionHandler {
    
    
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
    
    
        R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMessage());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString(error));
    }

}

Next, we introduce each microservice spring-boot-starter-actuator, and then we perform a series of operations in the mall, such as searching, details page, adding to the shopping cart, submitting an order, etc., and then we can see all our services in the sentinel console.

Now you can easily adjust the flow control of each microservice and each request in the console.

Fuse protection mechanism for remote calls

This is configured on the caller, that is, if a calls b, the configuration of a is performed.

Before the fuse protection mechanism is enabled, if the b service goes down/times out, the page will directly report an error. If it is
enabled, the page returns normally, but the information originally returned by the b service is replaced by the fusing method we specified.

package com.atlinxi.gulimall.product.feign;

import com.atlinxi.common.utils.R;
import com.atlinxi.gulimall.product.feign.fallback.SeckillFeignServiceFallBack;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;


// 如果远程调用失败,就用SeckillFeignServiceFallBack实现类方法返回的信息
@FeignClient(value = "gulimall-seckill",fallback = SeckillFeignServiceFallBack.class)
public interface SeckillFeignService {
    
    

    @GetMapping("/sku/seckill/{skuId}")
    R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}








package com.atlinxi.gulimall.product.feign.fallback;

import com.atlinxi.common.exception.BizCodeEnume;
import com.atlinxi.common.utils.R;
import com.atlinxi.gulimall.product.feign.SeckillFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class SeckillFeignServiceFallBack implements SeckillFeignService {
    
    


    @Override
    public R getSkuSeckillInfo(Long skuId) {
    
    
        log.info("熔断方法调用。。。getSkuSeckillInfo");
        return R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(),BizCodeEnume.TOO_MANY_REQUEST.getMessage());
    }
}

The caller manually specifies the downgrade policy of the remote service

熔断和降级的关系
We have written the method to be called by default after the fuse is written in the above code, but the code of the fuse will not be executed until the feign remote call times out or the server is down.
We use downgrading, such as specifying RT as 1, if it exceeds it, call the fuse code, and wait for the set time window to expire, then try to call the original method again.

Before the fuse downgrade is enabled, if service a calls service b and service b hangs up, service a will directly throw an exception.

In our project, when the product service queries the product details page, it will remotely call the seckill service to confirm whether the product has a seckill.

We enable the circuit breaker downgrade in the product service, so that if we perceive that the seckill service call fails, we will enable the circuit breaker, and our product details page is still normal, and the page will not directly report an error due to the failure of the seckill service call.

downgrade rules
insert image description here

  • RT, Average Response Time
    When 5 requests continue to enter within 1 second, and the average response time exceeds the threshold (the above figure, we specified 1ms), then within the next time window, calls to this method will be automatically blocked.
    After the time window expires, another attempt is made to call the method.

When our downgrade strategy takes effect, our custom circuit breaking method is still called.

Customize protected resources

code based

public List<SeckillSKuRedisTo> getCurrentSeckillSkus() {
    
    

        // 1. 确定当前时间属于哪个秒杀场次
        long time = new Date().getTime();

        /**
         * sentinel,我们假设这段逻辑运行时间可能会较长,进行自定义受保护资源
         *
         * seckillSkus,受保护资源的名称,就可以在sentinel dashboard进行流量控制,熔断降级等所有的策略
         */
        try (Entry entry = SphU.entry("seckillSkus")){
    
    

            Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");

            for (String key : keys) {
    
    
                // seckill:sessions:1682784000000_1682791200000
                String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
                String[] s = replace.split("_");
                long start = Long.parseLong(s[0]);
                long end = Long.parseLong(s[1]);

                if (time>=start && time<=end){
    
    

                    // 2. 获取这个秒杀场次需要的所有商品信息

                    // -100到100,代表的就是长度,我们秒杀的商品肯定没有这么多,所以肯定能取全
                    List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);

                    BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);

                    List<String> list = hashOps.multiGet(range);

                    if (list!=null){
    
    
                        List<SeckillSKuRedisTo> collect = list.stream().map(item -> {
    
    
                            SeckillSKuRedisTo redis = JSON.parseObject(item.toString(), SeckillSKuRedisTo.class);

                            return redis;

                        }).collect(Collectors.toList());

                        return collect;
                    }

                    break;

                }
            }

        }catch (BlockException e){
    
    
            log.error("资源被限流,{}",e.getMessage());


        }





        return null;
    }

annotation based

public List<SeckillSKuRedisTo> blockHandler(BlockException e){
    
    
        log.error("getCurrentSeckillSkusResource被限流了。。。");
        return null;

    }

    /**
     * 返回当前时间可以参与的秒杀商品信息
     * @return
     *
     *
     * sentinel
     *
     *  blockHandler 限流/降级之后要调用的方法
     *      返回值需要与原方法保持一致
     *      我们也可以在限流方法中获取到原方法的参数

		fallback 函数名称,可选项,用于在抛出异常的时候提供fallback处理逻辑,可以针对所有类型的异常进行处理
			方法的返回值类型必须与原函数保持一致
			方法的参数列表需要为空,或者可以额外多一个Throwable类型的参数用来接收对应的异常
			需与原方法在一个类中,如果不在,需定义为静态方法,用fallbackClass来指定在哪个类中
     */
    @Override
    @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
    public List<SeckillSKuRedisTo> getCurrentSeckillSkus() {
    
    

Gateway flow control

Previously, we used sentinel to perform operations such as flow control/downgrade on each microservice request and our custom resources, but in this case, the request has entered the microservice before it is controlled by sentinel.
If we add sentinel control at the gateway layer, the request will be blocked directly at the gateway layer without forwarding to the microservice, so the effect will be more obvious.
Let's now integrate sentinel at the gateway layer.

引入之后需要重启服务和sentinel控制台jar包,在控制台的gateway菜单就可以看到页面的变化
<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
			<version>2021.1</version>
		</dependency>

insert image description here

  • API name
    The id of the routing rule configured in the default gateway is the resource name of sentinel

  • Interval
    how long to count once

  • Burst size
    is the number of additional requests allowed when responding to burst requests.

  • Request attributes
    can be restricted based on request attributes

Custom return data after current limit

package com.atlinxi.gulimall.gateway.config;

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.fastjson.JSON;
import com.atlinxi.common.exception.BizCodeEnume;
import com.atlinxi.common.utils.R;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Configuration
public class SentinelGatewayConfig {
    
    

    public SentinelGatewayConfig() {
    
    
        GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
    
    

            // 网关限流了请求,就会调用此回调
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
    
    

                R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMessage());

                String errJson = JSON.toJSONString(error);

                Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errJson), String.class);


                return body;
            }
        });
    }
}

Every time after talking with the school, Su Yinglan shared it with Xiaoyi immediately, "Come here, I will report to you about my work". She omitted the specific negotiation process and summarized things in a language that a child could understand.

https://baijiahao.baidu.com/s?id=1760481532554271247

A Mom's "Battle" Against School Violence

Guess you like

Origin blog.csdn.net/weixin_44431371/article/details/130501172