谷粒商城二十四Sentinel限流&熔断&降级

在这里插入图片描述
在这里插入图片描述
我们在秒杀服务加的以上所有手段都是为了,除了快之外,我们还需要保证稳定

我们即使再快也会有一个极限值,现在假设单机下每秒处理一万个单,这已经是超高的处理能力了,秒杀服务上了五台服务器,有三台掉线,但是秒杀请求网关直接放过了10w请求,全部放进来,那剩下的两台服务器就处理不过来,每台服务器的顶峰值是1w,所有的请求都得排队,排着排着就造成了请求的时间累积,时间一长,资源耗尽,服务器就要崩溃了。

所以快保证了以后,我们就需要保证稳定。

如何保证稳定,那就是在我们分布式系统中的限流&熔断&降级,我们无论哪个分布式系统,不管是不是高并发,都要考虑,因为有了这些的保护手段,我们的整个集群就可以达到稳定。

我们以前是用springCloud的hystrix,不更新了,而且支持的功能也是有限的,
在我们的系统里面,我们使用springCloud alibaba的Sentinel,来完成整个系统的限流&熔断&降级
会把我们整个系统保护的非常稳定,即使百台服务器的大集群,有了Sentinel的保护,上线或者崩溃几台服务器,都会非常的稳定。

限流&熔断&降级

  • 什么是熔断
    A 服务调用 B 服务的某个功能,由于网络不稳定问题,或者 B 服务卡机,导致功能时间超长。如果这样子的次数太多。我们就可以直接将 B 断路了(A 不再请求 B 接口),凡是调用 B 的直接返回降级数据,不必等待 B 的超长执行。 这样 B 的故障问题,就不会级联影响到 A。

    如果没有任何保护,feign远程调用,feign有一个默认超时时间,例如是3s,3s时间如果不返回数据,就认为被调用的服务出问题了,feign接口就会报超时错误,但我们等不了这么久,因为这样就会引起整个调用链的累积效应
    a调用b,b调用c,c方法现在要等3s,b需要等c,a需要等b,大家都需要等,就会全线卡死,资源不能得到释放,吞吐量就会下降,大量的请求又在排队,这就形成了一个死循环,能力越不行,请求累积的越多,越多的请求又需要越多的资源进行分配处理,我们的机器就会整个卡死,宕机。
    所以我们需要加入熔断机制,a调用b,如果发现b不能正常返回,那以后我们直接把b进行断路,接下来a调用b不需要关注b是否成功,直接快速返回失败。
    熔断可以保证我们整个服务不受级联影响,一个服务挂了不会让整个调用链长时间的卡死。

  • 什么是降级
    整个网站处于流量高峰期,服务器压力剧增,根据当前业务情况及流量,对一些服务和页面进行有策略的降级[停止服务,所有的调用直接返回降级数据]。以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应。

    假设流量处于高峰期,现在有超多的业务正在运行,一些核心的业务,购物车、订单等,还有一些非核心的业务,比如注册之类的,现在网站正在秒杀的高峰期间,大家资源不够用了。
    我们可以手动的让一些非核心业务,比如注册,把服务器的注册业务停掉,该服务器上如果有其他业务,就可以把资源让给其他核心业务,这就是降级。
    同时可以返回一个降级页面,提示此功能暂时不可用

  • 异同

    • 相同点

      1. 为了保证集群大部分服务的可用性和可靠性,防止崩溃,牺牲小我
      2. 用户最终都是体验到某个功能不可用
    • 不同点:

      1. 熔断是被调用方故障,触发的系统主动规则
      2. 降级是基于全局考虑,手动停止一些正常服务,释放资源
  • 什么是限流
    对打入服务的请求流量进行控制,使服务能够承担不超过自己能力的流量压力。
    比如整个集群的处理能力就是每秒1w,那我们从网关处放回的请求就是1w,其他一些运气不好的,直接报告错误,自己重试也好,怎么做也好。
    限流就是把整个入口的流量来做一个限制,保证我们的服务不会被超过它的能力的流量全部压垮。只要超过它能力的流量直接丢弃,也不用去处理了。

Sentinel

限流&熔断&降级功能都可以用Sentinel做。

Sentinel与Hystrix的区别

在这里插入图片描述

1. 隔离策略

假设我们现在有100个请求全部进来要执行, 系统能力不足只能执行50个

  • 线程池隔离
    如果是Hystrix,例如是hello请求,为这个请求做一个线程池,分配50个线程,线程池分配一个线程来执行,如果线程不够了就打回去。
    但是如果有超多请求,每个请求就会对应一个不同的线程池,线程池也会超多,线程池之间的切换也非常浪费时间,这是对性能的一个极大影响。可能线程池用着用着资源都不够了,导致服务宕机都有可能。

  • 信号量隔离
    java8中也有Semaphore,和我们redis的Semaphore是一样的,只要请求一进来,如果限制是50,调用该请求的每一个请求都有对应自己的一个信号量,进来一个请求信号量-1,执行完一个请求信号量+1,如果发现该请求进来50个,下一次进来的就直接给打回。
    不用为每一个请求单独创建线程池,造成资源的耗费。

线程池隔离也有它的优点,每一个请求都是用自己的线程池,自己的线程池里炸了,和别人没有任何关系。
信号量则是一旦有些人炸了,我们整个服务都会出现一些问题。

2. 熔断降级策略

  • 基于响应时间
    例如每个请求只要超过1s就不去执行

  • 异常比率
    一个请求请求一百次,百分之九十都出现了异常,以后我就不请求了。

  • 异常数
    一百个请求中有五个出现异常,也不请求了。

我们有很多的策略来限制要不要熔断、降级后边的调用链(服务)。

3. 动态规则配置

以上的策略都可以通过数据源来做动态配置,就是把配置持久化到数据库中,服务即使重新启动,还能用之前的配置。

4. 系统自适应保护

系统的能力它知道以后,低峰期把流量都放进来,高峰期限制一些。

简介

Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard(web可视化界面,有了可视化界面,调节监控就非常方便了)。核心库不依赖 Dashboard,但是结合 Dashboard 可以取得最好的效果。

我们说的资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。使用 Sentinel 来进行资源保护,主要分为几个步骤:

  1. 定义资源(哪些资源要进行保护)
  2. 定义规则(定义保护规则,比如每秒请求超过多少次就不让访问了,cpu负载超过多少就给它降级了)
  3. 检验规则是否生效

定义资源

以下是最常用的定义资源的三种方式

方式一:主流框架的默认适配

为了减少开发的复杂程度,我们对大部分的主流框架,例如 Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor 等都做了适配。您只需要引入对应的依赖即可方便地整合 Sentinel。

主流的web框架,所有的请求是默认全部进来适配,所有的请求都是要受保护的资源。

方式二:抛出异常的方式定义资源

SphU 包含了 try-catch 风格的 API。用这种方式,当资源发生了限流之后会抛出 BlockException。这个时候可以捕捉异常,进行限流之后的逻辑处理。示例代码如下:

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

方式四:注解方式定义资源

Sentinel 支持通过 @SentinelResource 注解定义资源并配置 blockHandler 和 fallback 函数来进行限流之后的处理。示例:

// 原本的业务方法.
@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");
}

规则的种类

Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效。同时 Sentinel 也提供相关 API,供您来定制自己的规则策略。

Sentinel 支持以下几种规则:流量控制规则、熔断降级规则、系统保护规则(cpu、内存超过多少占用率)、来源访问控制规则(设置黑、白名单) 和 热点参数规则

流量控制规则

这是一段json格式的配置
在这里插入图片描述

在这里插入图片描述
上图是一个经典的调用树,如果对nodeA做了限流

  • stratege 调用关系限流策略

    • 直接
      只限制nodeA的流量

    • 链路
      可以在链路上指定入口资源,如果指定root为入口,那么只有从root调用到nodeA的限流才生效,如果是Entrance2(或者别的链路)调用nodeA则不生效

    • 关联
      当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。
      比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,
      举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为 RuleConstant.STRATEGY_RELATE 同时设置 refResource 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。

  • controlBehavior
    慢启动模式:流量缓慢增加,最终增加到系统的负载能力。

  • controlBehavior

    • 直接拒绝
      直接拒绝,抛出异常

    • 排队等待
      超出了系统并发的请求,就排队等待,同时我们也可以设置超时时间。

    • warm up 慢启动模式 预热模式
      系统最大承受并发100,我们可以设置预热时间,在规定时间内慢慢提高并发,在规定时间到达后并发最终到达100

通过代码定义流量控制规则
理解上面规则的定义之后,我们可以通过调用 FlowRuleManager.loadRules() 方法来用硬编码的方式定义流量控制规则,比如:

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);
}

同时这些控制规则也可以用可视化的控制台(sentinel 的 dashboard)来设置。

控制台下载

控制台的版本(是一个jar包)和sentinel-core:1.8.0的版本保持一致。使用java -jar直接启动就可以。

账密都是sentinel,登录进去之后界面中没有任何信息,这个页面是懒加载,我们发了请求如果是受保护资源的话,页面才会显示。

Sentinel整合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);
	}

}

解决依赖的问题

<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

对普通请求进行限流

普通请求是相对于远程调用来说的。

此时我们请求http://seckill.gulimall.com/currentSeckillSkus,sentinel控制台就会显示秒杀服务,在簇点链路菜单栏就会有/currentSeckillSkus请求,我们可以直接在控制台对该请求进行流控,设置QPS为1,我们在页面中快速刷新该页面,就会出现Blocked by Sentinel (flow limiting)的提示,并且在方法中打印log也不会执行。
这是最简单的流控。

解决控制台实时监控没有数据的问题

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

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

请求限流之后默认返回的数据默认是sentinel官方的

在这儿我们使用自己定义的

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));
    }

}

接下来我们把每个微服务都引入spring-boot-starter-actuator,然后我们在商城进行搜索、详情页、加入购物车、提交订单等一系列操作,再在sentinel控制台就可以看到我们的所有服务。

现在就可以在控制台很方便的调整每一个微服务,每一个请求的流量控制。

对远程调用进行熔断保护机制

这是在调用方配置的,也就是a调用b的话,对a进行的配置。

启用熔断保护机制之前,如果b服务宕掉/超时,那么页面会直接报错
启用了的话,页面返回正常,只是b服务原本返回的信息,被我们指定的熔断方法代替了。

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());
    }
}

调用方手动指定远程服务的降级策略

熔断和降级的关系
我们在以上代码已经写了熔断之后默认调用的方法,不过那是feign远程调用超时/服务器宕机之后,才会执行熔断的代码。
我们使用降级,例如指定RT为1,如果超过它,则调用熔断的代码,等设置的时间窗口过期,则又会尝试调用原方法。

在没有启用熔断降级之前,如果a服务调用b服务,b服务挂掉了,那么a服务会直接抛出异常。

在我们的项目中,商品服务查询商品详情页的时候会远程调用秒杀服务确认该商品是否存在秒杀。

我们就在商品服务中开启熔断降级,这样如果感知到秒杀服务调用失败的话,我们就开启熔断,我们的商品详情页面还是正常的,不会因为秒杀服务的调用失败导致页面直接报错。

降级规则
在这里插入图片描述

  • RT,平均响应时间
    当1s内持续进入5个请求,平均响应时间均超过阈值(上图,我们指定的1ms),那么在接下来的时间窗口之内,对这个方法的调用都会自动熔断。
    超出时间窗口之后,就会再次尝试调用该方法。

当我们的降级策略生效后,还是调用的我们自定义的熔断方法。

自定义受保护资源

基于代码

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;
    }

基于注解

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() {
    
    

网关流控

前面我们使用sentinel对每一个微服务的请求以及我们自定义的资源可以进行流控/降级等操作,但是在这种操作情况下,请求已经进入了微服务才由sentinel进行控制的。
如果我们在网关层加入sentinel的控制,请求就会直接在网关层被拦断,都不需要转发给微服务,这样效果就会更明显。
我们现在来整合网关层的sentinel。

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

在这里插入图片描述

  • API名称
    默认gateway中配置的路由规则的id就是sentinel的资源名

  • 间隔
    间隔多长时间统计一次

  • Burst size
    应对突发请求时额外允许的请求数目。

  • 请求属性
    可以根据请求属性进行限制

限流之后自定义返回数据

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;
            }
        });
    }
}

每次和学校谈话结束,苏迎澜就第一时间分享给小逸,“过来,我和你汇报下工作”。她省略了具体谈判的过程,以一个孩子能理解的语言把事情总结出来。

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

一个妈妈的反校园暴力“战斗”

猜你喜欢

转载自blog.csdn.net/weixin_44431371/article/details/130501172